Transmogrifier: Handle misskey likes with reactions like EmojiReactions.
[akkoma] / lib / pleroma / web / activity_pub / utils.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Utils do
6 alias Ecto.Changeset
7 alias Ecto.UUID
8 alias Pleroma.Activity
9 alias Pleroma.Notification
10 alias Pleroma.Object
11 alias Pleroma.Repo
12 alias Pleroma.User
13 alias Pleroma.Web
14 alias Pleroma.Web.ActivityPub.Visibility
15 alias Pleroma.Web.Endpoint
16 alias Pleroma.Web.Router.Helpers
17
18 import Ecto.Query
19
20 require Logger
21 require Pleroma.Constants
22
23 @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"]
24 @supported_report_states ~w(open closed resolved)
25 @valid_visibilities ~w(public unlisted private direct)
26
27 # Some implementations send the actor URI as the actor field, others send the entire actor object,
28 # so figure out what the actor's URI is based on what we have.
29 def get_ap_id(%{"id" => id} = _), do: id
30 def get_ap_id(id), do: id
31
32 def normalize_params(params) do
33 Map.put(params, "actor", get_ap_id(params["actor"]))
34 end
35
36 @spec determine_explicit_mentions(map()) :: map()
37 def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
38 Enum.flat_map(tag, fn
39 %{"type" => "Mention", "href" => href} -> [href]
40 _ -> []
41 end)
42 end
43
44 def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
45 object
46 |> Map.put("tag", [tag])
47 |> determine_explicit_mentions()
48 end
49
50 def determine_explicit_mentions(_), do: []
51
52 @spec recipient_in_collection(any(), any()) :: boolean()
53 defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
54 defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
55 defp recipient_in_collection(_, _), do: false
56
57 @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
58 def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
59 addresses = [params["to"], params["cc"], params["bto"], params["bcc"]]
60
61 cond do
62 Enum.any?(addresses, &recipient_in_collection(ap_id, &1)) -> true
63 # if the message is unaddressed at all, then assume it is directly addressed
64 # to the recipient
65 Enum.all?(addresses, &is_nil(&1)) -> true
66 # if the message is sent from somebody the user is following, then assume it
67 # is addressed to the recipient
68 User.following?(recipient, actor) -> true
69 true -> false
70 end
71 end
72
73 defp extract_list(target) when is_binary(target), do: [target]
74 defp extract_list(lst) when is_list(lst), do: lst
75 defp extract_list(_), do: []
76
77 def maybe_splice_recipient(ap_id, params) do
78 need_splice? =
79 !recipient_in_collection(ap_id, params["to"]) &&
80 !recipient_in_collection(ap_id, params["cc"])
81
82 if need_splice? do
83 cc_list = extract_list(params["cc"])
84 Map.put(params, "cc", [ap_id | cc_list])
85 else
86 params
87 end
88 end
89
90 def make_json_ld_header do
91 %{
92 "@context" => [
93 "https://www.w3.org/ns/activitystreams",
94 "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
95 %{
96 "@language" => "und"
97 }
98 ]
99 }
100 end
101
102 def make_date do
103 DateTime.utc_now() |> DateTime.to_iso8601()
104 end
105
106 def generate_activity_id do
107 generate_id("activities")
108 end
109
110 def generate_context_id do
111 generate_id("contexts")
112 end
113
114 def generate_object_id do
115 Helpers.o_status_url(Endpoint, :object, UUID.generate())
116 end
117
118 def generate_id(type) do
119 "#{Web.base_url()}/#{type}/#{UUID.generate()}"
120 end
121
122 def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
123 fake_create_activity = %{
124 "to" => object["to"],
125 "cc" => object["cc"],
126 "type" => "Create",
127 "object" => object
128 }
129
130 get_notified_from_object(fake_create_activity)
131 end
132
133 def get_notified_from_object(object) do
134 Notification.get_notified_from_activity(%Activity{data: object}, false)
135 end
136
137 def create_context(context) do
138 context = context || generate_id("contexts")
139
140 # Ecto has problems accessing the constraint inside the jsonb,
141 # so we explicitly check for the existed object before insert
142 object = Object.get_cached_by_ap_id(context)
143
144 with true <- is_nil(object),
145 changeset <- Object.context_mapping(context),
146 {:ok, inserted_object} <- Repo.insert(changeset) do
147 inserted_object
148 else
149 _ ->
150 object
151 end
152 end
153
154 @doc """
155 Enqueues an activity for federation if it's local
156 """
157 @spec maybe_federate(any()) :: :ok
158 def maybe_federate(%Activity{local: true} = activity) do
159 if Pleroma.Config.get!([:instance, :federating]) do
160 Pleroma.Web.Federator.publish(activity)
161 end
162
163 :ok
164 end
165
166 def maybe_federate(_), do: :ok
167
168 @doc """
169 Adds an id and a published data if they aren't there,
170 also adds it to an included object
171 """
172 @spec lazy_put_activity_defaults(map(), boolean) :: map()
173 def lazy_put_activity_defaults(map, fake? \\ false)
174
175 def lazy_put_activity_defaults(map, true) do
176 map
177 |> Map.put_new("id", "pleroma:fakeid")
178 |> Map.put_new_lazy("published", &make_date/0)
179 |> Map.put_new("context", "pleroma:fakecontext")
180 |> Map.put_new("context_id", -1)
181 |> lazy_put_object_defaults(true)
182 end
183
184 def lazy_put_activity_defaults(map, _fake?) do
185 %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
186
187 map
188 |> Map.put_new_lazy("id", &generate_activity_id/0)
189 |> Map.put_new_lazy("published", &make_date/0)
190 |> Map.put_new("context", context)
191 |> Map.put_new("context_id", context_id)
192 |> lazy_put_object_defaults(false)
193 end
194
195 # Adds an id and published date if they aren't there.
196 #
197 @spec lazy_put_object_defaults(map(), boolean()) :: map()
198 defp lazy_put_object_defaults(%{"object" => map} = activity, true)
199 when is_map(map) do
200 object =
201 map
202 |> Map.put_new("id", "pleroma:fake_object_id")
203 |> Map.put_new_lazy("published", &make_date/0)
204 |> Map.put_new("context", activity["context"])
205 |> Map.put_new("context_id", activity["context_id"])
206 |> Map.put_new("fake", true)
207
208 %{activity | "object" => object}
209 end
210
211 defp lazy_put_object_defaults(%{"object" => map} = activity, _)
212 when is_map(map) do
213 object =
214 map
215 |> Map.put_new_lazy("id", &generate_object_id/0)
216 |> Map.put_new_lazy("published", &make_date/0)
217 |> Map.put_new("context", activity["context"])
218 |> Map.put_new("context_id", activity["context_id"])
219
220 %{activity | "object" => object}
221 end
222
223 defp lazy_put_object_defaults(activity, _), do: activity
224
225 @doc """
226 Inserts a full object if it is contained in an activity.
227 """
228 def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
229 when is_map(object_data) and type in @supported_object_types do
230 with {:ok, object} <- Object.create(object_data) do
231 map = Map.put(map, "object", object.data["id"])
232
233 {:ok, map, object}
234 end
235 end
236
237 def insert_full_object(map), do: {:ok, map, nil}
238
239 #### Like-related helpers
240
241 @doc """
242 Returns an existing like if a user already liked an object
243 """
244 @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
245 def get_existing_like(actor, %{data: %{"id" => id}}) do
246 actor
247 |> Activity.Queries.by_actor()
248 |> Activity.Queries.by_object_id(id)
249 |> Activity.Queries.by_type("Like")
250 |> limit(1)
251 |> Repo.one()
252 end
253
254 @doc """
255 Returns like activities targeting an object
256 """
257 def get_object_likes(%{data: %{"id" => id}}) do
258 id
259 |> Activity.Queries.by_object_id()
260 |> Activity.Queries.by_type("Like")
261 |> Repo.all()
262 end
263
264 def make_emoji_reaction_data(user, object, emoji, activity_id) do
265 make_like_data(user, object, activity_id)
266 |> Map.put("type", "EmojiReaction")
267 |> Map.put("content", emoji)
268 end
269
270 @spec make_like_data(User.t(), map(), String.t()) :: map()
271 def make_like_data(
272 %User{ap_id: ap_id} = actor,
273 %{data: %{"actor" => object_actor_id, "id" => id}} = object,
274 activity_id
275 ) do
276 object_actor = User.get_cached_by_ap_id(object_actor_id)
277
278 to =
279 if Visibility.is_public?(object) do
280 [actor.follower_address, object.data["actor"]]
281 else
282 [object.data["actor"]]
283 end
284
285 cc =
286 (object.data["to"] ++ (object.data["cc"] || []))
287 |> List.delete(actor.ap_id)
288 |> List.delete(object_actor.follower_address)
289
290 %{
291 "type" => "Like",
292 "actor" => ap_id,
293 "object" => id,
294 "to" => to,
295 "cc" => cc,
296 "context" => object.data["context"]
297 }
298 |> maybe_put("id", activity_id)
299 end
300
301 @spec update_element_in_object(String.t(), list(any), Object.t()) ::
302 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
303 def update_element_in_object(property, element, object) do
304 length =
305 if is_map(element) do
306 element
307 |> Map.values()
308 |> List.flatten()
309 |> length()
310 else
311 element
312 |> length()
313 end
314
315 data =
316 Map.merge(
317 object.data,
318 %{"#{property}_count" => length, "#{property}s" => element}
319 )
320
321 object
322 |> Changeset.change(data: data)
323 |> Object.update_and_set_cache()
324 end
325
326 @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
327 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
328
329 def add_emoji_reaction_to_object(
330 %Activity{data: %{"content" => emoji, "actor" => actor}},
331 object
332 ) do
333 reactions = object.data["reactions"] || %{}
334 emoji_actors = reactions[emoji] || []
335 new_emoji_actors = [actor | emoji_actors] |> Enum.uniq()
336 new_reactions = Map.put(reactions, emoji, new_emoji_actors)
337 update_element_in_object("reaction", new_reactions, object)
338 end
339
340 def remove_emoji_reaction_from_object(
341 %Activity{data: %{"content" => emoji, "actor" => actor}},
342 object
343 ) do
344 reactions = object.data["reactions"] || %{}
345 emoji_actors = reactions[emoji] || []
346 new_emoji_actors = List.delete(emoji_actors, actor)
347
348 new_reactions =
349 if new_emoji_actors == [] do
350 Map.delete(reactions, emoji)
351 else
352 Map.put(reactions, emoji, new_emoji_actors)
353 end
354
355 update_element_in_object("reaction", new_reactions, object)
356 end
357
358 @spec add_like_to_object(Activity.t(), Object.t()) ::
359 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
360 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
361 [actor | fetch_likes(object)]
362 |> Enum.uniq()
363 |> update_likes_in_object(object)
364 end
365
366 @spec remove_like_from_object(Activity.t(), Object.t()) ::
367 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
368 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
369 object
370 |> fetch_likes()
371 |> List.delete(actor)
372 |> update_likes_in_object(object)
373 end
374
375 defp update_likes_in_object(likes, object) do
376 update_element_in_object("like", likes, object)
377 end
378
379 defp fetch_likes(object) do
380 if is_list(object.data["likes"]) do
381 object.data["likes"]
382 else
383 []
384 end
385 end
386
387 #### Follow-related helpers
388
389 @doc """
390 Updates a follow activity's state (for locked accounts).
391 """
392 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
393 def update_follow_state_for_all(
394 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
395 state
396 ) do
397 "Follow"
398 |> Activity.Queries.by_type()
399 |> Activity.Queries.by_actor(actor)
400 |> Activity.Queries.by_object_id(object)
401 |> where(fragment("data->>'state' = 'pending'"))
402 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
403 |> Repo.update_all([])
404
405 User.set_follow_state_cache(actor, object, state)
406
407 activity = Activity.get_by_id(activity.id)
408
409 {:ok, activity}
410 end
411
412 def update_follow_state(
413 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
414 state
415 ) do
416 new_data = Map.put(activity.data, "state", state)
417 changeset = Changeset.change(activity, data: new_data)
418
419 with {:ok, activity} <- Repo.update(changeset) do
420 User.set_follow_state_cache(actor, object, state)
421 {:ok, activity}
422 end
423 end
424
425 @doc """
426 Makes a follow activity data for the given follower and followed
427 """
428 def make_follow_data(
429 %User{ap_id: follower_id},
430 %User{ap_id: followed_id} = _followed,
431 activity_id
432 ) do
433 %{
434 "type" => "Follow",
435 "actor" => follower_id,
436 "to" => [followed_id],
437 "cc" => [Pleroma.Constants.as_public()],
438 "object" => followed_id,
439 "state" => "pending"
440 }
441 |> maybe_put("id", activity_id)
442 end
443
444 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
445 "Follow"
446 |> Activity.Queries.by_type()
447 |> where(actor: ^follower_id)
448 # this is to use the index
449 |> Activity.Queries.by_object_id(followed_id)
450 |> order_by([activity], fragment("? desc nulls last", activity.id))
451 |> limit(1)
452 |> Repo.one()
453 end
454
455 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
456 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
457
458 "EmojiReaction"
459 |> Activity.Queries.by_type()
460 |> where(actor: ^ap_id)
461 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
462 |> Activity.Queries.by_object_id(object_ap_id)
463 |> order_by([activity], fragment("? desc nulls last", activity.id))
464 |> limit(1)
465 |> Repo.one()
466 end
467
468 #### Announce-related helpers
469
470 @doc """
471 Retruns an existing announce activity if the notice has already been announced
472 """
473 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
474 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
475 "Announce"
476 |> Activity.Queries.by_type()
477 |> where(actor: ^actor)
478 # this is to use the index
479 |> Activity.Queries.by_object_id(ap_id)
480 |> Repo.one()
481 end
482
483 @doc """
484 Make announce activity data for the given actor and object
485 """
486 # for relayed messages, we only want to send to subscribers
487 def make_announce_data(
488 %User{ap_id: ap_id} = user,
489 %Object{data: %{"id" => id}} = object,
490 activity_id,
491 false
492 ) do
493 %{
494 "type" => "Announce",
495 "actor" => ap_id,
496 "object" => id,
497 "to" => [user.follower_address],
498 "cc" => [],
499 "context" => object.data["context"]
500 }
501 |> maybe_put("id", activity_id)
502 end
503
504 def make_announce_data(
505 %User{ap_id: ap_id} = user,
506 %Object{data: %{"id" => id}} = object,
507 activity_id,
508 true
509 ) do
510 %{
511 "type" => "Announce",
512 "actor" => ap_id,
513 "object" => id,
514 "to" => [user.follower_address, object.data["actor"]],
515 "cc" => [Pleroma.Constants.as_public()],
516 "context" => object.data["context"]
517 }
518 |> maybe_put("id", activity_id)
519 end
520
521 @doc """
522 Make unannounce activity data for the given actor and object
523 """
524 def make_unannounce_data(
525 %User{ap_id: ap_id} = user,
526 %Activity{data: %{"context" => context}} = activity,
527 activity_id
528 ) do
529 %{
530 "type" => "Undo",
531 "actor" => ap_id,
532 "object" => activity.data,
533 "to" => [user.follower_address, activity.data["actor"]],
534 "cc" => [Pleroma.Constants.as_public()],
535 "context" => context
536 }
537 |> maybe_put("id", activity_id)
538 end
539
540 def make_unlike_data(
541 %User{ap_id: ap_id} = user,
542 %Activity{data: %{"context" => context}} = activity,
543 activity_id
544 ) do
545 %{
546 "type" => "Undo",
547 "actor" => ap_id,
548 "object" => activity.data,
549 "to" => [user.follower_address, activity.data["actor"]],
550 "cc" => [Pleroma.Constants.as_public()],
551 "context" => context
552 }
553 |> maybe_put("id", activity_id)
554 end
555
556 def make_undo_data(
557 %User{ap_id: actor, follower_address: follower_address},
558 %Activity{
559 data: %{"id" => undone_activity_id, "context" => context},
560 actor: undone_activity_actor
561 },
562 activity_id \\ nil
563 ) do
564 %{
565 "type" => "Undo",
566 "actor" => actor,
567 "object" => undone_activity_id,
568 "to" => [follower_address, undone_activity_actor],
569 "cc" => [Pleroma.Constants.as_public()],
570 "context" => context
571 }
572 |> maybe_put("id", activity_id)
573 end
574
575 @spec add_announce_to_object(Activity.t(), Object.t()) ::
576 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
577 def add_announce_to_object(
578 %Activity{data: %{"actor" => actor}},
579 object
580 ) do
581 announcements = take_announcements(object)
582
583 with announcements <- Enum.uniq([actor | announcements]) do
584 update_element_in_object("announcement", announcements, object)
585 end
586 end
587
588 def add_announce_to_object(_, object), do: {:ok, object}
589
590 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
591 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
592 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
593 with announcements <- List.delete(take_announcements(object), actor) do
594 update_element_in_object("announcement", announcements, object)
595 end
596 end
597
598 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
599 when is_list(announcements),
600 do: announcements
601
602 defp take_announcements(_), do: []
603
604 #### Unfollow-related helpers
605
606 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
607 %{
608 "type" => "Undo",
609 "actor" => follower.ap_id,
610 "to" => [followed.ap_id],
611 "object" => follow_activity.data
612 }
613 |> maybe_put("id", activity_id)
614 end
615
616 #### Block-related helpers
617 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
618 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
619 "Block"
620 |> Activity.Queries.by_type()
621 |> where(actor: ^blocker_id)
622 # this is to use the index
623 |> Activity.Queries.by_object_id(blocked_id)
624 |> order_by([activity], fragment("? desc nulls last", activity.id))
625 |> limit(1)
626 |> Repo.one()
627 end
628
629 def make_block_data(blocker, blocked, activity_id) do
630 %{
631 "type" => "Block",
632 "actor" => blocker.ap_id,
633 "to" => [blocked.ap_id],
634 "object" => blocked.ap_id
635 }
636 |> maybe_put("id", activity_id)
637 end
638
639 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
640 %{
641 "type" => "Undo",
642 "actor" => blocker.ap_id,
643 "to" => [blocked.ap_id],
644 "object" => block_activity.data
645 }
646 |> maybe_put("id", activity_id)
647 end
648
649 #### Create-related helpers
650
651 def make_create_data(params, additional) do
652 published = params.published || make_date()
653
654 %{
655 "type" => "Create",
656 "to" => params.to |> Enum.uniq(),
657 "actor" => params.actor.ap_id,
658 "object" => params.object,
659 "published" => published,
660 "context" => params.context
661 }
662 |> Map.merge(additional)
663 end
664
665 #### Listen-related helpers
666 def make_listen_data(params, additional) do
667 published = params.published || make_date()
668
669 %{
670 "type" => "Listen",
671 "to" => params.to |> Enum.uniq(),
672 "actor" => params.actor.ap_id,
673 "object" => params.object,
674 "published" => published,
675 "context" => params.context
676 }
677 |> Map.merge(additional)
678 end
679
680 #### Flag-related helpers
681 @spec make_flag_data(map(), map()) :: map()
682 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
683 %{
684 "type" => "Flag",
685 "actor" => actor.ap_id,
686 "content" => content,
687 "object" => build_flag_object(params),
688 "context" => context,
689 "state" => "open"
690 }
691 |> Map.merge(additional)
692 end
693
694 def make_flag_data(_, _), do: %{}
695
696 defp build_flag_object(%{account: account, statuses: statuses} = _) do
697 [account.ap_id] ++
698 Enum.map(statuses || [], fn
699 %Activity{} = act -> act.data["id"]
700 act when is_map(act) -> act["id"]
701 act when is_binary(act) -> act
702 end)
703 end
704
705 defp build_flag_object(_), do: []
706
707 @doc """
708 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
709 the first one to `pages_left` pages.
710 If the amount of pages is higher than the collection has, it returns whatever was there.
711 """
712 def fetch_ordered_collection(from, pages_left, acc \\ []) do
713 with {:ok, response} <- Tesla.get(from),
714 {:ok, collection} <- Jason.decode(response.body) do
715 case collection["type"] do
716 "OrderedCollection" ->
717 # If we've encountered the OrderedCollection and not the page,
718 # just call the same function on the page address
719 fetch_ordered_collection(collection["first"], pages_left)
720
721 "OrderedCollectionPage" ->
722 if pages_left > 0 do
723 # There are still more pages
724 if Map.has_key?(collection, "next") do
725 # There are still more pages, go deeper saving what we have into the accumulator
726 fetch_ordered_collection(
727 collection["next"],
728 pages_left - 1,
729 acc ++ collection["orderedItems"]
730 )
731 else
732 # No more pages left, just return whatever we already have
733 acc ++ collection["orderedItems"]
734 end
735 else
736 # Got the amount of pages needed, add them all to the accumulator
737 acc ++ collection["orderedItems"]
738 end
739
740 _ ->
741 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
742 end
743 end
744 end
745
746 #### Report-related helpers
747
748 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
749 new_data = Map.put(activity.data, "state", state)
750
751 activity
752 |> Changeset.change(data: new_data)
753 |> Repo.update()
754 end
755
756 def update_report_state(_, _), do: {:error, "Unsupported state"}
757
758 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
759 [to, cc, recipients] =
760 activity
761 |> get_updated_targets(visibility)
762 |> Enum.map(&Enum.uniq/1)
763
764 object_data =
765 activity.object.data
766 |> Map.put("to", to)
767 |> Map.put("cc", cc)
768
769 {:ok, object} =
770 activity.object
771 |> Object.change(%{data: object_data})
772 |> Object.update_and_set_cache()
773
774 activity_data =
775 activity.data
776 |> Map.put("to", to)
777 |> Map.put("cc", cc)
778
779 activity
780 |> Map.put(:object, object)
781 |> Activity.change(%{data: activity_data, recipients: recipients})
782 |> Repo.update()
783 end
784
785 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
786
787 defp get_updated_targets(
788 %Activity{data: %{"to" => to} = data, recipients: recipients},
789 visibility
790 ) do
791 cc = Map.get(data, "cc", [])
792 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
793 public = Pleroma.Constants.as_public()
794
795 case visibility do
796 "public" ->
797 to = [public | List.delete(to, follower_address)]
798 cc = [follower_address | List.delete(cc, public)]
799 recipients = [public | recipients]
800 [to, cc, recipients]
801
802 "private" ->
803 to = [follower_address | List.delete(to, public)]
804 cc = List.delete(cc, public)
805 recipients = List.delete(recipients, public)
806 [to, cc, recipients]
807
808 "unlisted" ->
809 to = [follower_address | List.delete(to, public)]
810 cc = [public | List.delete(cc, follower_address)]
811 recipients = recipients ++ [follower_address, public]
812 [to, cc, recipients]
813
814 _ ->
815 [to, cc, recipients]
816 end
817 end
818
819 def get_existing_votes(actor, %{data: %{"id" => id}}) do
820 actor
821 |> Activity.Queries.by_actor()
822 |> Activity.Queries.by_type("Create")
823 |> Activity.with_preloaded_object()
824 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
825 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
826 |> Repo.all()
827 end
828
829 defp maybe_put(map, _key, nil), do: map
830 defp maybe_put(map, key, value), do: Map.put(map, key, value)
831 end