37f7a9173254cb4ed30c6ab25613680e3eea0e90
[akkoma] / lib / pleroma / web / activity_pub / utils.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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 create_context(context) do
158 context = context || generate_id("contexts")
159
160 # Ecto has problems accessing the constraint inside the jsonb,
161 # so we explicitly check for the existed object before insert
162 object = Object.get_cached_by_ap_id(context)
163
164 with true <- is_nil(object),
165 changeset <- Object.context_mapping(context),
166 {:ok, inserted_object} <- Repo.insert(changeset) do
167 inserted_object
168 else
169 _ ->
170 object
171 end
172 end
173
174 @doc """
175 Enqueues an activity for federation if it's local
176 """
177 @spec maybe_federate(any()) :: :ok
178 def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
179 outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
180
181 with true <- Config.get!([:instance, :federating]),
182 true <- type != "Block" || outgoing_blocks,
183 false <- Visibility.is_local_public?(activity) do
184 Pleroma.Web.Federator.publish(activity)
185 end
186
187 :ok
188 end
189
190 def maybe_federate(_), do: :ok
191
192 @doc """
193 Adds an id and a published data if they aren't there,
194 also adds it to an included object
195 """
196 @spec lazy_put_activity_defaults(map(), boolean) :: map()
197 def lazy_put_activity_defaults(map, fake? \\ false)
198
199 def lazy_put_activity_defaults(map, true) do
200 map
201 |> Map.put_new("id", "pleroma:fakeid")
202 |> Map.put_new_lazy("published", &make_date/0)
203 |> Map.put_new("context", "pleroma:fakecontext")
204 |> Map.put_new("context_id", -1)
205 |> lazy_put_object_defaults(true)
206 end
207
208 def lazy_put_activity_defaults(map, _fake?) do
209 %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
210
211 map
212 |> Map.put_new_lazy("id", &generate_activity_id/0)
213 |> Map.put_new_lazy("published", &make_date/0)
214 |> Map.put_new("context", context)
215 |> Map.put_new("context_id", context_id)
216 |> lazy_put_object_defaults(false)
217 end
218
219 # Adds an id and published date if they aren't there.
220 #
221 @spec lazy_put_object_defaults(map(), boolean()) :: map()
222 defp lazy_put_object_defaults(%{"object" => map} = activity, true)
223 when is_map(map) do
224 object =
225 map
226 |> Map.put_new("id", "pleroma:fake_object_id")
227 |> Map.put_new_lazy("published", &make_date/0)
228 |> Map.put_new("context", activity["context"])
229 |> Map.put_new("context_id", activity["context_id"])
230 |> Map.put_new("fake", true)
231
232 %{activity | "object" => object}
233 end
234
235 defp lazy_put_object_defaults(%{"object" => map} = activity, _)
236 when is_map(map) do
237 object =
238 map
239 |> Map.put_new_lazy("id", &generate_object_id/0)
240 |> Map.put_new_lazy("published", &make_date/0)
241 |> Map.put_new("context", activity["context"])
242 |> Map.put_new("context_id", activity["context_id"])
243
244 %{activity | "object" => object}
245 end
246
247 defp lazy_put_object_defaults(activity, _), do: activity
248
249 @doc """
250 Inserts a full object if it is contained in an activity.
251 """
252 def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
253 when type in @supported_object_types do
254 with {:ok, object} <- Object.create(object_data) do
255 map = Map.put(map, "object", object.data["id"])
256
257 {:ok, map, object}
258 end
259 end
260
261 def insert_full_object(map), do: {:ok, map, nil}
262
263 #### Like-related helpers
264
265 @doc """
266 Returns an existing like if a user already liked an object
267 """
268 @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
269 def get_existing_like(actor, %{data: %{"id" => id}}) do
270 actor
271 |> Activity.Queries.by_actor()
272 |> Activity.Queries.by_object_id(id)
273 |> Activity.Queries.by_type("Like")
274 |> limit(1)
275 |> Repo.one()
276 end
277
278 @doc """
279 Returns like activities targeting an object
280 """
281 def get_object_likes(%{data: %{"id" => id}}) do
282 id
283 |> Activity.Queries.by_object_id()
284 |> Activity.Queries.by_type("Like")
285 |> Repo.all()
286 end
287
288 @spec make_like_data(User.t(), map(), String.t()) :: map()
289 def make_like_data(
290 %User{ap_id: ap_id} = actor,
291 %{data: %{"actor" => object_actor_id, "id" => id}} = object,
292 activity_id
293 ) do
294 object_actor = User.get_cached_by_ap_id(object_actor_id)
295
296 to =
297 if Visibility.is_public?(object) do
298 [actor.follower_address, object.data["actor"]]
299 else
300 [object.data["actor"]]
301 end
302
303 cc =
304 (object.data["to"] ++ (object.data["cc"] || []))
305 |> List.delete(actor.ap_id)
306 |> List.delete(object_actor.follower_address)
307
308 %{
309 "type" => "Like",
310 "actor" => ap_id,
311 "object" => id,
312 "to" => to,
313 "cc" => cc,
314 "context" => object.data["context"]
315 }
316 |> Maps.put_if_present("id", activity_id)
317 end
318
319 def make_emoji_reaction_data(user, object, emoji, activity_id) do
320 make_like_data(user, object, activity_id)
321 |> Map.put("type", "EmojiReact")
322 |> Map.put("content", emoji)
323 end
324
325 @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
326 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
327 def update_element_in_object(property, element, object, count \\ nil) do
328 length =
329 count ||
330 length(element)
331
332 data =
333 Map.merge(
334 object.data,
335 %{"#{property}_count" => length, "#{property}s" => element}
336 )
337
338 object
339 |> Changeset.change(data: data)
340 |> Object.update_and_set_cache()
341 end
342
343 @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
344 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
345
346 def add_emoji_reaction_to_object(
347 %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
348 object
349 ) do
350 reactions = get_cached_emoji_reactions(object)
351 emoji = stripped_emoji_name(emoji)
352 url = emoji_url(emoji, activity)
353
354 new_reactions =
355 case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
356 if is_nil(candidate_url) do
357 emoji == candidate
358 else
359 url == candidate_url
360 end
361 end) do
362 nil ->
363 reactions ++ [[emoji, [actor], url]]
364
365 index ->
366 List.update_at(
367 reactions,
368 index,
369 fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
370 )
371 end
372
373 count = emoji_count(new_reactions)
374
375 update_element_in_object("reaction", new_reactions, object, count)
376 end
377
378 defp stripped_emoji_name(name) do
379 name
380 |> String.replace_leading(":", "")
381 |> String.replace_trailing(":", "")
382 end
383
384 defp emoji_url(
385 name,
386 %Activity{
387 data: %{
388 "tag" => [
389 %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
390 ]
391 }
392 }
393 ),
394 do: url
395
396 defp emoji_url(_, _), do: nil
397
398 def emoji_count(reactions_list) do
399 Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
400 end
401
402 def remove_emoji_reaction_from_object(
403 %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
404 object
405 ) do
406 emoji = stripped_emoji_name(emoji)
407 reactions = get_cached_emoji_reactions(object)
408 url = emoji_url(emoji, activity)
409
410 new_reactions =
411 case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
412 if is_nil(candidate_url) do
413 emoji == candidate
414 else
415 url == candidate_url
416 end
417 end) do
418 nil ->
419 reactions
420
421 index ->
422 List.update_at(
423 reactions,
424 index,
425 fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
426 )
427 |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
428 end
429
430 count = emoji_count(new_reactions)
431 update_element_in_object("reaction", new_reactions, object, count)
432 end
433
434 def get_cached_emoji_reactions(object) do
435 if is_list(object.data["reactions"]) do
436 object.data["reactions"]
437 else
438 []
439 end
440 end
441
442 @spec add_like_to_object(Activity.t(), Object.t()) ::
443 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
444 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
445 [actor | fetch_likes(object)]
446 |> Enum.uniq()
447 |> update_likes_in_object(object)
448 end
449
450 @spec remove_like_from_object(Activity.t(), Object.t()) ::
451 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
452 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
453 object
454 |> fetch_likes()
455 |> List.delete(actor)
456 |> update_likes_in_object(object)
457 end
458
459 defp update_likes_in_object(likes, object) do
460 update_element_in_object("like", likes, object)
461 end
462
463 defp fetch_likes(object) do
464 if is_list(object.data["likes"]) do
465 object.data["likes"]
466 else
467 []
468 end
469 end
470
471 #### Follow-related helpers
472
473 @doc """
474 Updates a follow activity's state (for locked accounts).
475 """
476 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
477 def update_follow_state_for_all(
478 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
479 state
480 ) do
481 "Follow"
482 |> Activity.Queries.by_type()
483 |> Activity.Queries.by_actor(actor)
484 |> Activity.Queries.by_object_id(object)
485 |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
486 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
487 |> Repo.update_all([])
488
489 activity = Activity.get_by_id(activity.id)
490
491 {:ok, activity}
492 end
493
494 def update_follow_state(
495 %Activity{} = activity,
496 state
497 ) do
498 new_data = Map.put(activity.data, "state", state)
499 changeset = Changeset.change(activity, data: new_data)
500
501 with {:ok, activity} <- Repo.update(changeset) do
502 {:ok, activity}
503 end
504 end
505
506 @doc """
507 Makes a follow activity data for the given follower and followed
508 """
509 def make_follow_data(
510 %User{ap_id: follower_id},
511 %User{ap_id: followed_id} = _followed,
512 activity_id
513 ) do
514 %{
515 "type" => "Follow",
516 "actor" => follower_id,
517 "to" => [followed_id],
518 "cc" => [Pleroma.Constants.as_public()],
519 "object" => followed_id,
520 "state" => "pending"
521 }
522 |> Maps.put_if_present("id", activity_id)
523 end
524
525 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
526 "Follow"
527 |> Activity.Queries.by_type()
528 |> where(actor: ^follower_id)
529 # this is to use the index
530 |> Activity.Queries.by_object_id(followed_id)
531 |> order_by([activity], fragment("? desc nulls last", activity.id))
532 |> limit(1)
533 |> Repo.one()
534 end
535
536 def fetch_latest_undo(%User{ap_id: ap_id}) do
537 "Undo"
538 |> Activity.Queries.by_type()
539 |> where(actor: ^ap_id)
540 |> order_by([activity], fragment("? desc nulls last", activity.id))
541 |> limit(1)
542 |> Repo.one()
543 end
544
545 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
546 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
547
548 emoji =
549 if String.starts_with?(emoji, ":") do
550 emoji
551 else
552 ":#{emoji}:"
553 end
554
555 "EmojiReact"
556 |> Activity.Queries.by_type()
557 |> where(actor: ^ap_id)
558 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
559 |> Activity.Queries.by_object_id(object_ap_id)
560 |> order_by([activity], fragment("? desc nulls last", activity.id))
561 |> limit(1)
562 |> Repo.one()
563 end
564
565 #### Announce-related helpers
566
567 @doc """
568 Returns an existing announce activity if the notice has already been announced
569 """
570 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
571 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
572 "Announce"
573 |> Activity.Queries.by_type()
574 |> where(actor: ^actor)
575 # this is to use the index
576 |> Activity.Queries.by_object_id(ap_id)
577 |> Repo.one()
578 end
579
580 @doc """
581 Make announce activity data for the given actor and object
582 """
583 # for relayed messages, we only want to send to subscribers
584 def make_announce_data(
585 %User{ap_id: ap_id} = user,
586 %Object{data: %{"id" => id}} = object,
587 activity_id,
588 false
589 ) do
590 %{
591 "type" => "Announce",
592 "actor" => ap_id,
593 "object" => id,
594 "to" => [user.follower_address],
595 "cc" => [],
596 "context" => object.data["context"]
597 }
598 |> Maps.put_if_present("id", activity_id)
599 end
600
601 def make_announce_data(
602 %User{ap_id: ap_id} = user,
603 %Object{data: %{"id" => id}} = object,
604 activity_id,
605 true
606 ) do
607 %{
608 "type" => "Announce",
609 "actor" => ap_id,
610 "object" => id,
611 "to" => [user.follower_address, object.data["actor"]],
612 "cc" => [Pleroma.Constants.as_public()],
613 "context" => object.data["context"]
614 }
615 |> Maps.put_if_present("id", activity_id)
616 end
617
618 def make_undo_data(
619 %User{ap_id: actor, follower_address: follower_address},
620 %Activity{
621 data: %{"id" => undone_activity_id, "context" => context},
622 actor: undone_activity_actor
623 },
624 activity_id \\ nil
625 ) do
626 %{
627 "type" => "Undo",
628 "actor" => actor,
629 "object" => undone_activity_id,
630 "to" => [follower_address, undone_activity_actor],
631 "cc" => [Pleroma.Constants.as_public()],
632 "context" => context
633 }
634 |> Maps.put_if_present("id", activity_id)
635 end
636
637 @spec add_announce_to_object(Activity.t(), Object.t()) ::
638 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
639 def add_announce_to_object(
640 %Activity{data: %{"actor" => actor}},
641 object
642 ) do
643 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
644 announcements = take_announcements(object)
645
646 with announcements <- Enum.uniq([actor | announcements]) do
647 update_element_in_object("announcement", announcements, object)
648 end
649 else
650 {:ok, object}
651 end
652 end
653
654 def add_announce_to_object(_, object), do: {:ok, object}
655
656 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
657 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
658 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
659 with announcements <- List.delete(take_announcements(object), actor) do
660 update_element_in_object("announcement", announcements, object)
661 end
662 end
663
664 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
665 when is_list(announcements),
666 do: announcements
667
668 defp take_announcements(_), do: []
669
670 #### Unfollow-related helpers
671
672 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
673 %{
674 "type" => "Undo",
675 "actor" => follower.ap_id,
676 "to" => [followed.ap_id],
677 "object" => follow_activity.data
678 }
679 |> Maps.put_if_present("id", activity_id)
680 end
681
682 #### Block-related helpers
683 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
684 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
685 "Block"
686 |> Activity.Queries.by_type()
687 |> where(actor: ^blocker_id)
688 # this is to use the index
689 |> Activity.Queries.by_object_id(blocked_id)
690 |> order_by([activity], fragment("? desc nulls last", activity.id))
691 |> limit(1)
692 |> Repo.one()
693 end
694
695 def make_block_data(blocker, blocked, activity_id) do
696 %{
697 "type" => "Block",
698 "actor" => blocker.ap_id,
699 "to" => [blocked.ap_id],
700 "object" => blocked.ap_id
701 }
702 |> Maps.put_if_present("id", activity_id)
703 end
704
705 #### Create-related helpers
706
707 def make_create_data(params, additional) do
708 published = params.published || make_date()
709
710 %{
711 "type" => "Create",
712 "to" => params.to |> Enum.uniq(),
713 "actor" => params.actor.ap_id,
714 "object" => params.object,
715 "published" => published,
716 "context" => params.context
717 }
718 |> Map.merge(additional)
719 end
720
721 #### Listen-related helpers
722 def make_listen_data(params, additional) do
723 published = params.published || make_date()
724
725 %{
726 "type" => "Listen",
727 "to" => params.to |> Enum.uniq(),
728 "actor" => params.actor.ap_id,
729 "object" => params.object,
730 "published" => published,
731 "context" => params.context
732 }
733 |> Map.merge(additional)
734 end
735
736 #### Flag-related helpers
737 @spec make_flag_data(map(), map()) :: map()
738 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
739 %{
740 "type" => "Flag",
741 "actor" => actor.ap_id,
742 "content" => content,
743 "object" => build_flag_object(params),
744 "context" => context,
745 "state" => "open"
746 }
747 |> Map.merge(additional)
748 end
749
750 def make_flag_data(_, _), do: %{}
751
752 defp build_flag_object(%{account: account, statuses: statuses}) do
753 [account.ap_id | build_flag_object(%{statuses: statuses})]
754 end
755
756 defp build_flag_object(%{statuses: statuses}) do
757 Enum.map(statuses || [], &build_flag_object/1)
758 end
759
760 defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do
761 activity_actor = User.get_by_ap_id(data["actor"])
762
763 %{
764 "type" => "Note",
765 "id" => id,
766 "content" => data["content"],
767 "published" => data["published"],
768 "actor" =>
769 AccountView.render(
770 "show.json",
771 %{user: activity_actor, skip_visibility_check: true}
772 )
773 }
774 end
775
776 defp build_flag_object(act) when is_map(act) or is_binary(act) do
777 id =
778 case act do
779 %Activity{} = act -> act.data["id"]
780 act when is_map(act) -> act["id"]
781 act when is_binary(act) -> act
782 end
783
784 case Activity.get_by_ap_id_with_object(id) do
785 %Activity{} = activity ->
786 build_flag_object(activity)
787
788 nil ->
789 if activity = Activity.get_by_object_ap_id_with_object(id) do
790 build_flag_object(activity)
791 else
792 %{"id" => id, "deleted" => true}
793 end
794 end
795 end
796
797 defp build_flag_object(_), do: []
798
799 #### Report-related helpers
800 def get_reports(params, page, page_size) do
801 params =
802 params
803 |> Map.put(:type, "Flag")
804 |> Map.put(:skip_preload, true)
805 |> Map.put(:preload_report_notes, true)
806 |> Map.put(:total, true)
807 |> Map.put(:limit, page_size)
808 |> Map.put(:offset, (page - 1) * page_size)
809
810 ActivityPub.fetch_activities([], params, :offset)
811 end
812
813 def update_report_state(%Activity{} = activity, state)
814 when state in @strip_status_report_states do
815 {:ok, stripped_activity} = strip_report_status_data(activity)
816
817 new_data =
818 activity.data
819 |> Map.put("state", state)
820 |> Map.put("object", stripped_activity.data["object"])
821
822 activity
823 |> Changeset.change(data: new_data)
824 |> Repo.update()
825 end
826
827 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
828 new_data = Map.put(activity.data, "state", state)
829
830 activity
831 |> Changeset.change(data: new_data)
832 |> Repo.update()
833 end
834
835 def update_report_state(activity_ids, state) when state in @supported_report_states do
836 activities_num = length(activity_ids)
837
838 from(a in Activity, where: a.id in ^activity_ids)
839 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
840 |> Repo.update_all([])
841 |> case do
842 {^activities_num, _} -> :ok
843 _ -> {:error, activity_ids}
844 end
845 end
846
847 def update_report_state(_, _), do: {:error, "Unsupported state"}
848
849 def strip_report_status_data(activity) do
850 [actor | reported_activities] = activity.data["object"]
851
852 stripped_activities =
853 Enum.map(reported_activities, fn
854 act when is_map(act) -> act["id"]
855 act when is_binary(act) -> act
856 end)
857
858 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
859
860 {:ok, %{activity | data: new_data}}
861 end
862
863 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
864 [to, cc, recipients] =
865 activity
866 |> get_updated_targets(visibility)
867 |> Enum.map(&Enum.uniq/1)
868
869 object_data =
870 activity.object.data
871 |> Map.put("to", to)
872 |> Map.put("cc", cc)
873
874 {:ok, object} =
875 activity.object
876 |> Object.change(%{data: object_data})
877 |> Object.update_and_set_cache()
878
879 activity_data =
880 activity.data
881 |> Map.put("to", to)
882 |> Map.put("cc", cc)
883
884 activity
885 |> Map.put(:object, object)
886 |> Activity.change(%{data: activity_data, recipients: recipients})
887 |> Repo.update()
888 end
889
890 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
891
892 defp get_updated_targets(
893 %Activity{data: %{"to" => to} = data, recipients: recipients},
894 visibility
895 ) do
896 cc = Map.get(data, "cc", [])
897 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
898 public = Pleroma.Constants.as_public()
899
900 case visibility do
901 "public" ->
902 to = [public | List.delete(to, follower_address)]
903 cc = [follower_address | List.delete(cc, public)]
904 recipients = [public | recipients]
905 [to, cc, recipients]
906
907 "private" ->
908 to = [follower_address | List.delete(to, public)]
909 cc = List.delete(cc, public)
910 recipients = List.delete(recipients, public)
911 [to, cc, recipients]
912
913 "unlisted" ->
914 to = [follower_address | List.delete(to, public)]
915 cc = [public | List.delete(cc, follower_address)]
916 recipients = recipients ++ [follower_address, public]
917 [to, cc, recipients]
918
919 _ ->
920 [to, cc, recipients]
921 end
922 end
923
924 def get_existing_votes(actor, %{data: %{"id" => id}}) do
925 actor
926 |> Activity.Queries.by_actor()
927 |> Activity.Queries.by_type("Create")
928 |> Activity.with_preloaded_object()
929 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
930 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
931 |> Repo.all()
932 end
933 end