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