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