total rebase
[anni] / lib / pleroma / web / activity_pub / utils.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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.Config
10   alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
11   alias Pleroma.Maps
12   alias Pleroma.Notification
13   alias Pleroma.Object
14   alias Pleroma.Repo
15   alias Pleroma.User
16   alias Pleroma.Web.ActivityPub.ActivityPub
17   alias Pleroma.Web.ActivityPub.Visibility
18   alias Pleroma.Web.AdminAPI.AccountView
19   alias Pleroma.Web.Endpoint
20   alias Pleroma.Web.Router.Helpers
21
22   import Ecto.Query
23
24   require Logger
25   require Pleroma.Constants
26
27   @supported_object_types [
28     "Article",
29     "Note",
30     "Event",
31     "Video",
32     "Page",
33     "Question",
34     "Answer",
35     "Audio",
36     "Image"
37   ]
38   @strip_status_report_states ~w(closed resolved)
39   @supported_report_states ~w(open closed resolved)
40   @valid_visibilities ~w(public unlisted private direct)
41
42   def as_local_public, do: Endpoint.url() <> "/#Public"
43
44   # Some implementations send the actor URI as the actor field, others send the entire actor object,
45   # so figure out what the actor's URI is based on what we have.
46   def get_ap_id(%{"id" => id} = _), do: id
47   def get_ap_id(id), do: id
48
49   def normalize_params(params) do
50     Map.put(params, "actor", get_ap_id(params["actor"]))
51   end
52
53   @spec determine_explicit_mentions(map()) :: [any]
54   def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
55     Enum.flat_map(tag, fn
56       %{"type" => "Mention", "href" => href} -> [href]
57       _ -> []
58     end)
59   end
60
61   def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
62     object
63     |> Map.put("tag", [tag])
64     |> determine_explicit_mentions()
65   end
66
67   def determine_explicit_mentions(_), do: []
68
69   @spec label_in_collection?(any(), any()) :: boolean()
70   defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
71   defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
72   defp label_in_collection?(_, _), do: false
73
74   @spec label_in_message?(String.t(), map()) :: boolean()
75   def label_in_message?(label, params),
76     do:
77       [params["to"], params["cc"], params["bto"], params["bcc"]]
78       |> Enum.any?(&label_in_collection?(label, &1))
79
80   @spec unaddressed_message?(map()) :: boolean()
81   def unaddressed_message?(params),
82     do:
83       [params["to"], params["cc"], params["bto"], params["bcc"]]
84       |> Enum.all?(&is_nil(&1))
85
86   @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
87   def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
88     do:
89       label_in_message?(ap_id, params) || unaddressed_message?(params) ||
90         User.following?(recipient, actor)
91
92   defp extract_list(target) when is_binary(target), do: [target]
93   defp extract_list(lst) when is_list(lst), do: lst
94   defp extract_list(_), do: []
95
96   def maybe_splice_recipient(ap_id, params) do
97     need_splice? =
98       !label_in_collection?(ap_id, params["to"]) &&
99         !label_in_collection?(ap_id, params["cc"])
100
101     if need_splice? do
102       cc = [ap_id | extract_list(params["cc"])]
103
104       params
105       |> Map.put("cc", cc)
106       |> Maps.safe_put_in(["object", "cc"], cc)
107     else
108       params
109     end
110   end
111
112   def make_json_ld_header do
113     %{
114       "@context" => [
115         "https://www.w3.org/ns/activitystreams",
116         "#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
117         %{
118           "@language" => "und"
119         }
120       ]
121     }
122   end
123
124   def make_date do
125     DateTime.utc_now() |> DateTime.to_iso8601()
126   end
127
128   def generate_activity_id do
129     generate_id("activities")
130   end
131
132   def generate_context_id do
133     generate_id("contexts")
134   end
135
136   def generate_object_id do
137     Helpers.o_status_url(Endpoint, :object, UUID.generate())
138   end
139
140   def generate_id(type) do
141     "#{Endpoint.url()}/#{type}/#{UUID.generate()}"
142   end
143
144   def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
145     fake_create_activity = %{
146       "to" => object["to"],
147       "cc" => object["cc"],
148       "type" => "Create",
149       "object" => object
150     }
151
152     get_notified_from_object(fake_create_activity)
153   end
154
155   def get_notified_from_object(object) do
156     Notification.get_notified_from_activity(%Activity{data: object}, false)
157   end
158
159   def maybe_create_context(context), do: context || generate_id("contexts")
160
161   @doc """
162   Enqueues an activity for federation if it's local
163   """
164   @spec maybe_federate(any()) :: :ok
165   def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
166     outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
167
168     with true <- Config.get!([:instance, :federating]),
169          true <- type != "Block" || outgoing_blocks,
170          false <- Visibility.local_public?(activity) do
171       Pleroma.Web.Federator.publish(activity)
172     end
173
174     :ok
175   end
176
177   def maybe_federate(_), do: :ok
178
179   @doc """
180   Adds an id and a published data if they aren't there,
181   also adds it to an included object
182   """
183   @spec lazy_put_activity_defaults(map(), boolean) :: map()
184   def lazy_put_activity_defaults(map, fake? \\ false)
185
186   def lazy_put_activity_defaults(map, true) do
187     map
188     |> Map.put_new("id", "pleroma:fakeid")
189     |> Map.put_new_lazy("published", &make_date/0)
190     |> Map.put_new("context", "pleroma:fakecontext")
191     |> lazy_put_object_defaults(true)
192   end
193
194   def lazy_put_activity_defaults(map, _fake?) do
195     context = maybe_create_context(map["context"])
196
197     map
198     |> Map.put_new_lazy("id", &generate_activity_id/0)
199     |> Map.put_new_lazy("published", &make_date/0)
200     |> Map.put_new("context", context)
201     |> lazy_put_object_defaults(false)
202   end
203
204   # Adds an id and published date if they aren't there.
205   #
206   @spec lazy_put_object_defaults(map(), boolean()) :: map()
207   defp lazy_put_object_defaults(%{"object" => map} = activity, true)
208        when is_map(map) do
209     object =
210       map
211       |> Map.put_new("id", "pleroma:fake_object_id")
212       |> Map.put_new_lazy("published", &make_date/0)
213       |> Map.put_new("context", activity["context"])
214       |> Map.put_new("fake", true)
215
216     %{activity | "object" => object}
217   end
218
219   defp lazy_put_object_defaults(%{"object" => map} = activity, _)
220        when is_map(map) do
221     object =
222       map
223       |> Map.put_new_lazy("id", &generate_object_id/0)
224       |> Map.put_new_lazy("published", &make_date/0)
225       |> Map.put_new("context", activity["context"])
226
227     %{activity | "object" => object}
228   end
229
230   defp lazy_put_object_defaults(activity, _), do: activity
231
232   @doc """
233   Inserts a full object if it is contained in an activity.
234   """
235   def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
236       when type in @supported_object_types do
237     with {:ok, object} <- Object.create(object_data) do
238       map = Map.put(map, "object", object.data["id"])
239
240       {:ok, map, object}
241     end
242   end
243
244   def insert_full_object(map), do: {:ok, map, nil}
245
246   #### Like-related helpers
247
248   @doc """
249   Returns an existing like if a user already liked an object
250   """
251   @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
252   def get_existing_like(actor, %{data: %{"id" => id}}) do
253     actor
254     |> Activity.Queries.by_actor()
255     |> Activity.Queries.by_object_id(id)
256     |> Activity.Queries.by_type("Like")
257     |> limit(1)
258     |> Repo.one()
259   end
260
261   @doc """
262   Returns like activities targeting an object
263   """
264   def get_object_likes(%{data: %{"id" => id}}) do
265     id
266     |> Activity.Queries.by_object_id()
267     |> Activity.Queries.by_type("Like")
268     |> Repo.all()
269   end
270
271   @spec make_like_data(User.t(), map(), String.t()) :: map()
272   def make_like_data(
273         %User{ap_id: ap_id} = actor,
274         %{data: %{"actor" => object_actor_id, "id" => id}} = object,
275         activity_id
276       ) do
277     object_actor = User.get_cached_by_ap_id(object_actor_id)
278
279     to =
280       if Visibility.public?(object) do
281         [actor.follower_address, object.data["actor"]]
282       else
283         [object.data["actor"]]
284       end
285
286     cc =
287       (object.data["to"] ++ (object.data["cc"] || []))
288       |> List.delete(actor.ap_id)
289       |> List.delete(object_actor.follower_address)
290
291     %{
292       "type" => "Like",
293       "actor" => ap_id,
294       "object" => id,
295       "to" => to,
296       "cc" => cc,
297       "context" => object.data["context"]
298     }
299     |> Maps.put_if_present("id", activity_id)
300   end
301
302   def make_emoji_reaction_data(user, object, emoji, activity_id) do
303     make_like_data(user, object, activity_id)
304     |> Map.put("type", "EmojiReact")
305     |> Map.put("content", emoji)
306   end
307
308   @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
309           {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
310   def update_element_in_object(property, element, object, count \\ nil) do
311     length =
312       count ||
313         length(element)
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}} = activity,
331         object
332       ) do
333     reactions = get_cached_emoji_reactions(object)
334     emoji = Pleroma.Emoji.maybe_strip_name(emoji)
335     url = maybe_emoji_url(emoji, activity)
336
337     new_reactions =
338       case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
339              if is_nil(candidate_url) do
340                emoji == candidate
341              else
342                url == candidate_url
343              end
344            end) do
345         nil ->
346           reactions ++ [[emoji, [actor], url]]
347
348         index ->
349           List.update_at(
350             reactions,
351             index,
352             fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
353           )
354       end
355
356     count = emoji_count(new_reactions)
357
358     update_element_in_object("reaction", new_reactions, object, count)
359   end
360
361   defp maybe_emoji_url(
362          name,
363          %Activity{
364            data: %{
365              "tag" => [
366                %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
367              ]
368            }
369          }
370        ),
371        do: url
372
373   defp maybe_emoji_url(_, _), do: nil
374
375   def emoji_count(reactions_list) do
376     Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
377   end
378
379   def remove_emoji_reaction_from_object(
380         %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
381         object
382       ) do
383     emoji = Pleroma.Emoji.maybe_strip_name(emoji)
384     reactions = get_cached_emoji_reactions(object)
385     url = maybe_emoji_url(emoji, activity)
386
387     new_reactions =
388       case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
389              if is_nil(candidate_url) do
390                emoji == candidate
391              else
392                url == candidate_url
393              end
394            end) do
395         nil ->
396           reactions
397
398         index ->
399           List.update_at(
400             reactions,
401             index,
402             fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
403           )
404           |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
405       end
406
407     count = emoji_count(new_reactions)
408     update_element_in_object("reaction", new_reactions, object, count)
409   end
410
411   def get_cached_emoji_reactions(object) do
412     Object.get_emoji_reactions(object)
413   end
414
415   @spec add_like_to_object(Activity.t(), Object.t()) ::
416           {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
417   def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
418     [actor | fetch_likes(object)]
419     |> Enum.uniq()
420     |> update_likes_in_object(object)
421   end
422
423   @spec remove_like_from_object(Activity.t(), Object.t()) ::
424           {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
425   def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
426     object
427     |> fetch_likes()
428     |> List.delete(actor)
429     |> update_likes_in_object(object)
430   end
431
432   defp update_likes_in_object(likes, object) do
433     update_element_in_object("like", likes, object)
434   end
435
436   defp fetch_likes(object) do
437     if is_list(object.data["likes"]) do
438       object.data["likes"]
439     else
440       []
441     end
442   end
443
444   #### Follow-related helpers
445
446   @doc """
447   Updates a follow activity's state (for locked accounts).
448   """
449   @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
450   def update_follow_state_for_all(
451         %Activity{data: %{"actor" => actor, "object" => object}} = activity,
452         state
453       ) do
454     "Follow"
455     |> Activity.Queries.by_type()
456     |> Activity.Queries.by_actor(actor)
457     |> Activity.Queries.by_object_id(object)
458     |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
459     |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
460     |> Repo.update_all([])
461
462     activity = Activity.get_by_id(activity.id)
463
464     {:ok, activity}
465   end
466
467   def update_follow_state(
468         %Activity{} = activity,
469         state
470       ) do
471     new_data = Map.put(activity.data, "state", state)
472     changeset = Changeset.change(activity, data: new_data)
473
474     with {:ok, activity} <- Repo.update(changeset) do
475       {:ok, activity}
476     end
477   end
478
479   @doc """
480   Makes a follow activity data for the given follower and followed
481   """
482   def make_follow_data(
483         %User{ap_id: follower_id},
484         %User{ap_id: followed_id} = _followed,
485         activity_id
486       ) do
487     %{
488       "type" => "Follow",
489       "actor" => follower_id,
490       "to" => [followed_id],
491       "cc" => [Pleroma.Constants.as_public()],
492       "object" => followed_id,
493       "state" => "pending"
494     }
495     |> Maps.put_if_present("id", activity_id)
496   end
497
498   def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
499     "Follow"
500     |> Activity.Queries.by_type()
501     |> where(actor: ^follower_id)
502     # this is to use the index
503     |> Activity.Queries.by_object_id(followed_id)
504     |> order_by([activity], fragment("? desc nulls last", activity.id))
505     |> limit(1)
506     |> Repo.one()
507   end
508
509   def fetch_latest_undo(%User{ap_id: ap_id}) do
510     "Undo"
511     |> Activity.Queries.by_type()
512     |> where(actor: ^ap_id)
513     |> order_by([activity], fragment("? desc nulls last", activity.id))
514     |> limit(1)
515     |> Repo.one()
516   end
517
518   def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
519     %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
520     emoji = Pleroma.Emoji.maybe_quote(emoji)
521
522     "EmojiReact"
523     |> Activity.Queries.by_type()
524     |> where(actor: ^ap_id)
525     |> custom_emoji_discriminator(emoji)
526     |> Activity.Queries.by_object_id(object_ap_id)
527     |> order_by([activity], fragment("? desc nulls last", activity.id))
528     |> limit(1)
529     |> Repo.one()
530   end
531
532   defp custom_emoji_discriminator(query, emoji) do
533     if String.contains?(emoji, "@") do
534       stripped = Pleroma.Emoji.maybe_strip_name(emoji)
535       [name, domain] = String.split(stripped, "@")
536       domain_pattern = "%/" <> domain <> "/%"
537       emoji_pattern = Pleroma.Emoji.maybe_quote(name)
538
539       query
540       |> where([activity], fragment("?->>'content' = ?
541         AND EXISTS (
542           SELECT FROM jsonb_array_elements(?->'tag') elem
543           WHERE elem->>'id' ILIKE ?
544         )", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
545     else
546       query
547       |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
548     end
549   end
550
551   #### Announce-related helpers
552
553   @doc """
554   Returns an existing announce activity if the notice has already been announced
555   """
556   @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
557   def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
558     "Announce"
559     |> Activity.Queries.by_type()
560     |> where(actor: ^actor)
561     # this is to use the index
562     |> Activity.Queries.by_object_id(ap_id)
563     |> Repo.one()
564   end
565
566   @doc """
567   Make announce activity data for the given actor and object
568   """
569   # for relayed messages, we only want to send to subscribers
570   def make_announce_data(
571         %User{ap_id: ap_id} = user,
572         %Object{data: %{"id" => id}} = object,
573         activity_id,
574         false
575       ) do
576     %{
577       "type" => "Announce",
578       "actor" => ap_id,
579       "object" => id,
580       "to" => [user.follower_address],
581       "cc" => [],
582       "context" => object.data["context"]
583     }
584     |> Maps.put_if_present("id", activity_id)
585   end
586
587   def make_announce_data(
588         %User{ap_id: ap_id} = user,
589         %Object{data: %{"id" => id}} = object,
590         activity_id,
591         true
592       ) do
593     %{
594       "type" => "Announce",
595       "actor" => ap_id,
596       "object" => id,
597       "to" => [user.follower_address, object.data["actor"]],
598       "cc" => [Pleroma.Constants.as_public()],
599       "context" => object.data["context"]
600     }
601     |> Maps.put_if_present("id", activity_id)
602   end
603
604   def make_undo_data(
605         %User{ap_id: actor, follower_address: follower_address},
606         %Activity{
607           data: %{"id" => undone_activity_id, "context" => context},
608           actor: undone_activity_actor
609         },
610         activity_id \\ nil
611       ) do
612     %{
613       "type" => "Undo",
614       "actor" => actor,
615       "object" => undone_activity_id,
616       "to" => [follower_address, undone_activity_actor],
617       "cc" => [Pleroma.Constants.as_public()],
618       "context" => context
619     }
620     |> Maps.put_if_present("id", activity_id)
621   end
622
623   @spec add_announce_to_object(Activity.t(), Object.t()) ::
624           {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
625   def add_announce_to_object(
626         %Activity{data: %{"actor" => actor}},
627         object
628       ) do
629     unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
630       announcements = take_announcements(object)
631
632       with announcements <- Enum.uniq([actor | announcements]) do
633         update_element_in_object("announcement", announcements, object)
634       end
635     else
636       {:ok, object}
637     end
638   end
639
640   def add_announce_to_object(_, object), do: {:ok, object}
641
642   @spec remove_announce_from_object(Activity.t(), Object.t()) ::
643           {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
644   def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
645     with announcements <- List.delete(take_announcements(object), actor) do
646       update_element_in_object("announcement", announcements, object)
647     end
648   end
649
650   defp take_announcements(%{data: %{"announcements" => announcements}} = _)
651        when is_list(announcements),
652        do: announcements
653
654   defp take_announcements(_), do: []
655
656   #### Unfollow-related helpers
657
658   def make_unfollow_data(follower, followed, follow_activity, activity_id) do
659     %{
660       "type" => "Undo",
661       "actor" => follower.ap_id,
662       "to" => [followed.ap_id],
663       "object" => follow_activity.data
664     }
665     |> Maps.put_if_present("id", activity_id)
666   end
667
668   #### Block-related helpers
669   @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
670   def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
671     "Block"
672     |> Activity.Queries.by_type()
673     |> where(actor: ^blocker_id)
674     # this is to use the index
675     |> Activity.Queries.by_object_id(blocked_id)
676     |> order_by([activity], fragment("? desc nulls last", activity.id))
677     |> limit(1)
678     |> Repo.one()
679   end
680
681   def make_block_data(blocker, blocked, activity_id) do
682     %{
683       "type" => "Block",
684       "actor" => blocker.ap_id,
685       "to" => [blocked.ap_id],
686       "object" => blocked.ap_id
687     }
688     |> Maps.put_if_present("id", activity_id)
689   end
690
691   #### Create-related helpers
692
693   def make_create_data(params, additional) do
694     published = params.published || make_date()
695
696     %{
697       "type" => "Create",
698       "to" => params.to |> Enum.uniq(),
699       "actor" => params.actor.ap_id,
700       "object" => params.object,
701       "published" => published,
702       "context" => params.context
703     }
704     |> Map.merge(additional)
705   end
706
707   #### Listen-related helpers
708   def make_listen_data(params, additional) do
709     published = params.published || make_date()
710
711     %{
712       "type" => "Listen",
713       "to" => params.to |> Enum.uniq(),
714       "actor" => params.actor.ap_id,
715       "object" => params.object,
716       "published" => published,
717       "context" => params.context
718     }
719     |> Map.merge(additional)
720   end
721
722   #### Flag-related helpers
723   @spec make_flag_data(map(), map()) :: map()
724   def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
725     %{
726       "type" => "Flag",
727       "actor" => actor.ap_id,
728       "content" => content,
729       "object" => build_flag_object(params),
730       "context" => context,
731       "state" => "open"
732     }
733     |> Map.merge(additional)
734   end
735
736   def make_flag_data(_, _), do: %{}
737
738   defp build_flag_object(%{account: account, statuses: statuses}) do
739     [account.ap_id | build_flag_object(%{statuses: statuses})]
740   end
741
742   defp build_flag_object(%{statuses: statuses}) do
743     Enum.map(statuses || [], &build_flag_object/1)
744   end
745
746   defp build_flag_object(%Activity{} = activity) do
747     object = Object.normalize(activity, fetch: false)
748
749     # Do not allow people to report Creates. Instead, report the Object that is Created.
750     if activity.data["type"] != "Create" do
751       build_flag_object_with_actor_and_id(
752         object,
753         User.get_by_ap_id(activity.data["actor"]),
754         activity.data["id"]
755       )
756     else
757       build_flag_object(object)
758     end
759   end
760
761   defp build_flag_object(%Object{} = object) do
762     actor = User.get_by_ap_id(object.data["actor"])
763     build_flag_object_with_actor_and_id(object, actor, object.data["id"])
764   end
765
766   defp build_flag_object(act) when is_map(act) or is_binary(act) do
767     id =
768       case act do
769         %Activity{} = act -> act.data["id"]
770         act when is_map(act) -> act["id"]
771         act when is_binary(act) -> act
772       end
773
774     case Activity.get_by_ap_id_with_object(id) do
775       %Activity{object: object} = _ ->
776         build_flag_object(object)
777
778       nil ->
779         case Object.get_by_ap_id(id) do
780           %Object{} = object -> build_flag_object(object)
781           _ -> %{"id" => id, "deleted" => true}
782         end
783     end
784   end
785
786   defp build_flag_object(_), do: []
787
788   defp build_flag_object_with_actor_and_id(%Object{data: data}, actor, id) do
789     %{
790       "type" => "Note",
791       "id" => id,
792       "content" => data["content"],
793       "published" => data["published"],
794       "actor" =>
795         AccountView.render(
796           "show.json",
797           %{user: actor, skip_visibility_check: true}
798         )
799     }
800   end
801
802   #### Report-related helpers
803   def get_reports(params, page, page_size) do
804     params =
805       params
806       |> Map.put(:type, "Flag")
807       |> Map.put(:skip_preload, true)
808       |> Map.put(:preload_report_notes, true)
809       |> Map.put(:total, true)
810       |> Map.put(:limit, page_size)
811       |> Map.put(:offset, (page - 1) * page_size)
812
813     ActivityPub.fetch_activities([], params, :offset)
814   end
815
816   defp maybe_strip_report_status(data, state) do
817     with true <- Config.get([:instance, :report_strip_status]),
818          true <- state in @strip_status_report_states,
819          {:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do
820       data |> Map.put("object", stripped_activity.data["object"])
821     else
822       _ -> data
823     end
824   end
825
826   def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
827     new_data =
828       activity.data
829       |> Map.put("state", state)
830       |> maybe_strip_report_status(state)
831
832     activity
833     |> Changeset.change(data: new_data)
834     |> Repo.update()
835   end
836
837   def update_report_state(activity_ids, state) when state in @supported_report_states do
838     activities_num = length(activity_ids)
839
840     from(a in Activity, where: a.id in ^activity_ids)
841     |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
842     |> Repo.update_all([])
843     |> case do
844       {^activities_num, _} -> :ok
845       _ -> {:error, activity_ids}
846     end
847   end
848
849   def update_report_state(_, _), do: {:error, "Unsupported state"}
850
851   def strip_report_status_data(activity) do
852     [actor | reported_activities] = activity.data["object"]
853
854     stripped_activities =
855       Enum.reduce(reported_activities, [], fn act, acc ->
856         case ObjectID.cast(act) do
857           {:ok, act} -> [act | acc]
858           _ -> acc
859         end
860       end)
861
862     new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
863
864     {:ok, %{activity | data: new_data}}
865   end
866
867   def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
868     [to, cc, recipients] =
869       activity
870       |> get_updated_targets(visibility)
871       |> Enum.map(&Enum.uniq/1)
872
873     object_data =
874       activity.object.data
875       |> Map.put("to", to)
876       |> Map.put("cc", cc)
877
878     {:ok, object} =
879       activity.object
880       |> Object.change(%{data: object_data})
881       |> Object.update_and_set_cache()
882
883     activity_data =
884       activity.data
885       |> Map.put("to", to)
886       |> Map.put("cc", cc)
887
888     activity
889     |> Map.put(:object, object)
890     |> Activity.change(%{data: activity_data, recipients: recipients})
891     |> Repo.update()
892   end
893
894   def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
895
896   defp get_updated_targets(
897          %Activity{data: %{"to" => to} = data, recipients: recipients},
898          visibility
899        ) do
900     cc = Map.get(data, "cc", [])
901     follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
902     public = Pleroma.Constants.as_public()
903
904     case visibility do
905       "public" ->
906         to = [public | List.delete(to, follower_address)]
907         cc = [follower_address | List.delete(cc, public)]
908         recipients = [public | recipients]
909         [to, cc, recipients]
910
911       "private" ->
912         to = [follower_address | List.delete(to, public)]
913         cc = List.delete(cc, public)
914         recipients = List.delete(recipients, public)
915         [to, cc, recipients]
916
917       "unlisted" ->
918         to = [follower_address | List.delete(to, public)]
919         cc = [public | List.delete(cc, follower_address)]
920         recipients = recipients ++ [follower_address, public]
921         [to, cc, recipients]
922
923       _ ->
924         [to, cc, recipients]
925     end
926   end
927
928   def get_existing_votes(actor, %{data: %{"id" => id}}) do
929     actor
930     |> Activity.Queries.by_actor()
931     |> Activity.Queries.by_type("Create")
932     |> Activity.with_preloaded_object()
933     |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
934     |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
935     |> Repo.all()
936   end
937
938   def maybe_handle_group_posts(activity) do
939     poster = User.get_cached_by_ap_id(activity.actor)
940
941     mentions =
942       activity.data["to"]
943       |> Enum.filter(&(&1 != activity.actor))
944
945     mentioned_local_groups =
946       User.get_all_by_ap_id(mentions)
947       |> Enum.filter(fn user ->
948         user.actor_type == "Group" and
949           user.local and
950           not User.blocks?(user, poster)
951       end)
952
953     mentioned_local_groups
954     |> Enum.each(fn group ->
955       Pleroma.Web.CommonAPI.repeat(activity.id, group)
956     end)
957
958     :ok
959   end
960 end