cca58e0daecd7447e94264f1fa7622263e152932
[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 new_reactions =
354 case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
355 if is_nil(candidate_url) do
356 emoji == candidate
357 else
358 url == candidate_url
359 end
360 end) do
361 nil ->
362 reactions ++ [[emoji, [actor], url]]
363
364 index ->
365 List.update_at(
366 reactions,
367 index,
368 fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
369 )
370 end
371
372 count = emoji_count(new_reactions)
373
374 update_element_in_object("reaction", new_reactions, object, count)
375 end
376
377 defp stripped_emoji_name(name) do
378 name
379 |> String.replace_leading(":", "")
380 |> String.replace_trailing(":", "")
381 end
382
383 defp emoji_url(name,
384 %Activity{
385 data: %{"tag" => [
386 %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
387 ]}
388 }), do: url
389 defp emoji_url(_, _), do: nil
390
391 def emoji_count(reactions_list) do
392 Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
393 end
394
395 def remove_emoji_reaction_from_object(
396 %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
397 object
398 ) do
399 emoji = stripped_emoji_name(emoji)
400 reactions = get_cached_emoji_reactions(object)
401 url = emoji_url(emoji, activity)
402 new_reactions =
403 case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
404 if is_nil(candidate_url) do
405 emoji == candidate
406 else
407 url == candidate_url
408 end
409 end) do
410 nil ->
411 reactions
412
413 index ->
414 List.update_at(
415 reactions,
416 index,
417 fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
418 )
419 |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
420 end
421
422 count = emoji_count(new_reactions)
423 update_element_in_object("reaction", new_reactions, object, count)
424 end
425
426 def get_cached_emoji_reactions(object) do
427 if is_list(object.data["reactions"]) do
428 object.data["reactions"]
429 else
430 []
431 end
432 end
433
434 @spec add_like_to_object(Activity.t(), Object.t()) ::
435 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
436 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
437 [actor | fetch_likes(object)]
438 |> Enum.uniq()
439 |> update_likes_in_object(object)
440 end
441
442 @spec remove_like_from_object(Activity.t(), Object.t()) ::
443 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
444 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
445 object
446 |> fetch_likes()
447 |> List.delete(actor)
448 |> update_likes_in_object(object)
449 end
450
451 defp update_likes_in_object(likes, object) do
452 update_element_in_object("like", likes, object)
453 end
454
455 defp fetch_likes(object) do
456 if is_list(object.data["likes"]) do
457 object.data["likes"]
458 else
459 []
460 end
461 end
462
463 #### Follow-related helpers
464
465 @doc """
466 Updates a follow activity's state (for locked accounts).
467 """
468 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
469 def update_follow_state_for_all(
470 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
471 state
472 ) do
473 "Follow"
474 |> Activity.Queries.by_type()
475 |> Activity.Queries.by_actor(actor)
476 |> Activity.Queries.by_object_id(object)
477 |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
478 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
479 |> Repo.update_all([])
480
481 activity = Activity.get_by_id(activity.id)
482
483 {:ok, activity}
484 end
485
486 def update_follow_state(
487 %Activity{} = activity,
488 state
489 ) do
490 new_data = Map.put(activity.data, "state", state)
491 changeset = Changeset.change(activity, data: new_data)
492
493 with {:ok, activity} <- Repo.update(changeset) do
494 {:ok, activity}
495 end
496 end
497
498 @doc """
499 Makes a follow activity data for the given follower and followed
500 """
501 def make_follow_data(
502 %User{ap_id: follower_id},
503 %User{ap_id: followed_id} = _followed,
504 activity_id
505 ) do
506 %{
507 "type" => "Follow",
508 "actor" => follower_id,
509 "to" => [followed_id],
510 "cc" => [Pleroma.Constants.as_public()],
511 "object" => followed_id,
512 "state" => "pending"
513 }
514 |> Maps.put_if_present("id", activity_id)
515 end
516
517 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
518 "Follow"
519 |> Activity.Queries.by_type()
520 |> where(actor: ^follower_id)
521 # this is to use the index
522 |> Activity.Queries.by_object_id(followed_id)
523 |> order_by([activity], fragment("? desc nulls last", activity.id))
524 |> limit(1)
525 |> Repo.one()
526 end
527
528 def fetch_latest_undo(%User{ap_id: ap_id}) do
529 "Undo"
530 |> Activity.Queries.by_type()
531 |> where(actor: ^ap_id)
532 |> order_by([activity], fragment("? desc nulls last", activity.id))
533 |> limit(1)
534 |> Repo.one()
535 end
536
537 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
538 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
539 emoji = if String.starts_with?(emoji, ":") do
540 emoji
541 else
542 ":#{emoji}:"
543 end
544
545 "EmojiReact"
546 |> Activity.Queries.by_type()
547 |> where(actor: ^ap_id)
548 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
549 |> Activity.Queries.by_object_id(object_ap_id)
550 |> order_by([activity], fragment("? desc nulls last", activity.id))
551 |> limit(1)
552 |> Repo.one()
553 end
554
555 #### Announce-related helpers
556
557 @doc """
558 Returns an existing announce activity if the notice has already been announced
559 """
560 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
561 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
562 "Announce"
563 |> Activity.Queries.by_type()
564 |> where(actor: ^actor)
565 # this is to use the index
566 |> Activity.Queries.by_object_id(ap_id)
567 |> Repo.one()
568 end
569
570 @doc """
571 Make announce activity data for the given actor and object
572 """
573 # for relayed messages, we only want to send to subscribers
574 def make_announce_data(
575 %User{ap_id: ap_id} = user,
576 %Object{data: %{"id" => id}} = object,
577 activity_id,
578 false
579 ) do
580 %{
581 "type" => "Announce",
582 "actor" => ap_id,
583 "object" => id,
584 "to" => [user.follower_address],
585 "cc" => [],
586 "context" => object.data["context"]
587 }
588 |> Maps.put_if_present("id", activity_id)
589 end
590
591 def make_announce_data(
592 %User{ap_id: ap_id} = user,
593 %Object{data: %{"id" => id}} = object,
594 activity_id,
595 true
596 ) do
597 %{
598 "type" => "Announce",
599 "actor" => ap_id,
600 "object" => id,
601 "to" => [user.follower_address, object.data["actor"]],
602 "cc" => [Pleroma.Constants.as_public()],
603 "context" => object.data["context"]
604 }
605 |> Maps.put_if_present("id", activity_id)
606 end
607
608 def make_undo_data(
609 %User{ap_id: actor, follower_address: follower_address},
610 %Activity{
611 data: %{"id" => undone_activity_id, "context" => context},
612 actor: undone_activity_actor
613 },
614 activity_id \\ nil
615 ) do
616 %{
617 "type" => "Undo",
618 "actor" => actor,
619 "object" => undone_activity_id,
620 "to" => [follower_address, undone_activity_actor],
621 "cc" => [Pleroma.Constants.as_public()],
622 "context" => context
623 }
624 |> Maps.put_if_present("id", activity_id)
625 end
626
627 @spec add_announce_to_object(Activity.t(), Object.t()) ::
628 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
629 def add_announce_to_object(
630 %Activity{data: %{"actor" => actor}},
631 object
632 ) do
633 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
634 announcements = take_announcements(object)
635
636 with announcements <- Enum.uniq([actor | announcements]) do
637 update_element_in_object("announcement", announcements, object)
638 end
639 else
640 {:ok, object}
641 end
642 end
643
644 def add_announce_to_object(_, object), do: {:ok, object}
645
646 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
647 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
648 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
649 with announcements <- List.delete(take_announcements(object), actor) do
650 update_element_in_object("announcement", announcements, object)
651 end
652 end
653
654 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
655 when is_list(announcements),
656 do: announcements
657
658 defp take_announcements(_), do: []
659
660 #### Unfollow-related helpers
661
662 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
663 %{
664 "type" => "Undo",
665 "actor" => follower.ap_id,
666 "to" => [followed.ap_id],
667 "object" => follow_activity.data
668 }
669 |> Maps.put_if_present("id", activity_id)
670 end
671
672 #### Block-related helpers
673 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
674 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
675 "Block"
676 |> Activity.Queries.by_type()
677 |> where(actor: ^blocker_id)
678 # this is to use the index
679 |> Activity.Queries.by_object_id(blocked_id)
680 |> order_by([activity], fragment("? desc nulls last", activity.id))
681 |> limit(1)
682 |> Repo.one()
683 end
684
685 def make_block_data(blocker, blocked, activity_id) do
686 %{
687 "type" => "Block",
688 "actor" => blocker.ap_id,
689 "to" => [blocked.ap_id],
690 "object" => blocked.ap_id
691 }
692 |> Maps.put_if_present("id", activity_id)
693 end
694
695 #### Create-related helpers
696
697 def make_create_data(params, additional) do
698 published = params.published || make_date()
699
700 %{
701 "type" => "Create",
702 "to" => params.to |> Enum.uniq(),
703 "actor" => params.actor.ap_id,
704 "object" => params.object,
705 "published" => published,
706 "context" => params.context
707 }
708 |> Map.merge(additional)
709 end
710
711 #### Listen-related helpers
712 def make_listen_data(params, additional) do
713 published = params.published || make_date()
714
715 %{
716 "type" => "Listen",
717 "to" => params.to |> Enum.uniq(),
718 "actor" => params.actor.ap_id,
719 "object" => params.object,
720 "published" => published,
721 "context" => params.context
722 }
723 |> Map.merge(additional)
724 end
725
726 #### Flag-related helpers
727 @spec make_flag_data(map(), map()) :: map()
728 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
729 %{
730 "type" => "Flag",
731 "actor" => actor.ap_id,
732 "content" => content,
733 "object" => build_flag_object(params),
734 "context" => context,
735 "state" => "open"
736 }
737 |> Map.merge(additional)
738 end
739
740 def make_flag_data(_, _), do: %{}
741
742 defp build_flag_object(%{account: account, statuses: statuses}) do
743 [account.ap_id | build_flag_object(%{statuses: statuses})]
744 end
745
746 defp build_flag_object(%{statuses: statuses}) do
747 Enum.map(statuses || [], &build_flag_object/1)
748 end
749
750 defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do
751 activity_actor = User.get_by_ap_id(data["actor"])
752
753 %{
754 "type" => "Note",
755 "id" => id,
756 "content" => data["content"],
757 "published" => data["published"],
758 "actor" =>
759 AccountView.render(
760 "show.json",
761 %{user: activity_actor, skip_visibility_check: true}
762 )
763 }
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{} = activity ->
776 build_flag_object(activity)
777
778 nil ->
779 if activity = Activity.get_by_object_ap_id_with_object(id) do
780 build_flag_object(activity)
781 else
782 %{"id" => id, "deleted" => true}
783 end
784 end
785 end
786
787 defp build_flag_object(_), do: []
788
789 #### Report-related helpers
790 def get_reports(params, page, page_size) do
791 params =
792 params
793 |> Map.put(:type, "Flag")
794 |> Map.put(:skip_preload, true)
795 |> Map.put(:preload_report_notes, true)
796 |> Map.put(:total, true)
797 |> Map.put(:limit, page_size)
798 |> Map.put(:offset, (page - 1) * page_size)
799
800 ActivityPub.fetch_activities([], params, :offset)
801 end
802
803 def update_report_state(%Activity{} = activity, state)
804 when state in @strip_status_report_states do
805 {:ok, stripped_activity} = strip_report_status_data(activity)
806
807 new_data =
808 activity.data
809 |> Map.put("state", state)
810 |> Map.put("object", stripped_activity.data["object"])
811
812 activity
813 |> Changeset.change(data: new_data)
814 |> Repo.update()
815 end
816
817 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
818 new_data = Map.put(activity.data, "state", state)
819
820 activity
821 |> Changeset.change(data: new_data)
822 |> Repo.update()
823 end
824
825 def update_report_state(activity_ids, state) when state in @supported_report_states do
826 activities_num = length(activity_ids)
827
828 from(a in Activity, where: a.id in ^activity_ids)
829 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
830 |> Repo.update_all([])
831 |> case do
832 {^activities_num, _} -> :ok
833 _ -> {:error, activity_ids}
834 end
835 end
836
837 def update_report_state(_, _), do: {:error, "Unsupported state"}
838
839 def strip_report_status_data(activity) do
840 [actor | reported_activities] = activity.data["object"]
841
842 stripped_activities =
843 Enum.map(reported_activities, fn
844 act when is_map(act) -> act["id"]
845 act when is_binary(act) -> act
846 end)
847
848 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
849
850 {:ok, %{activity | data: new_data}}
851 end
852
853 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
854 [to, cc, recipients] =
855 activity
856 |> get_updated_targets(visibility)
857 |> Enum.map(&Enum.uniq/1)
858
859 object_data =
860 activity.object.data
861 |> Map.put("to", to)
862 |> Map.put("cc", cc)
863
864 {:ok, object} =
865 activity.object
866 |> Object.change(%{data: object_data})
867 |> Object.update_and_set_cache()
868
869 activity_data =
870 activity.data
871 |> Map.put("to", to)
872 |> Map.put("cc", cc)
873
874 activity
875 |> Map.put(:object, object)
876 |> Activity.change(%{data: activity_data, recipients: recipients})
877 |> Repo.update()
878 end
879
880 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
881
882 defp get_updated_targets(
883 %Activity{data: %{"to" => to} = data, recipients: recipients},
884 visibility
885 ) do
886 cc = Map.get(data, "cc", [])
887 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
888 public = Pleroma.Constants.as_public()
889
890 case visibility do
891 "public" ->
892 to = [public | List.delete(to, follower_address)]
893 cc = [follower_address | List.delete(cc, public)]
894 recipients = [public | recipients]
895 [to, cc, recipients]
896
897 "private" ->
898 to = [follower_address | List.delete(to, public)]
899 cc = List.delete(cc, public)
900 recipients = List.delete(recipients, public)
901 [to, cc, recipients]
902
903 "unlisted" ->
904 to = [follower_address | List.delete(to, public)]
905 cc = [public | List.delete(cc, follower_address)]
906 recipients = recipients ++ [follower_address, public]
907 [to, cc, recipients]
908
909 _ ->
910 [to, cc, recipients]
911 end
912 end
913
914 def get_existing_votes(actor, %{data: %{"id" => id}}) do
915 actor
916 |> Activity.Queries.by_actor()
917 |> Activity.Queries.by_type("Create")
918 |> Activity.with_preloaded_object()
919 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
920 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
921 |> Repo.all()
922 end
923 end