Merge branch 'feature/custom-runtime-modules' into 'develop'
[akkoma] / lib / pleroma / web / activity_pub / utils.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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()) :: map()
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", "EmojiReaction")
312 |> Map.put("content", emoji)
313 end
314
315 @spec update_element_in_object(String.t(), list(any), Object.t()) ::
316 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
317 def update_element_in_object(property, element, object) do
318 length =
319 if is_map(element) do
320 element
321 |> Map.values()
322 |> List.flatten()
323 |> length()
324 else
325 element
326 |> length()
327 end
328
329 data =
330 Map.merge(
331 object.data,
332 %{"#{property}_count" => length, "#{property}s" => element}
333 )
334
335 object
336 |> Changeset.change(data: data)
337 |> Object.update_and_set_cache()
338 end
339
340 @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
341 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
342
343 def add_emoji_reaction_to_object(
344 %Activity{data: %{"content" => emoji, "actor" => actor}},
345 object
346 ) do
347 reactions = object.data["reactions"] || %{}
348 emoji_actors = reactions[emoji] || []
349 new_emoji_actors = [actor | emoji_actors] |> Enum.uniq()
350 new_reactions = Map.put(reactions, emoji, new_emoji_actors)
351 update_element_in_object("reaction", new_reactions, object)
352 end
353
354 def remove_emoji_reaction_from_object(
355 %Activity{data: %{"content" => emoji, "actor" => actor}},
356 object
357 ) do
358 reactions = object.data["reactions"] || %{}
359 emoji_actors = reactions[emoji] || []
360 new_emoji_actors = List.delete(emoji_actors, actor)
361
362 new_reactions =
363 if new_emoji_actors == [] do
364 Map.delete(reactions, emoji)
365 else
366 Map.put(reactions, emoji, new_emoji_actors)
367 end
368
369 update_element_in_object("reaction", new_reactions, object)
370 end
371
372 @spec add_like_to_object(Activity.t(), Object.t()) ::
373 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
374 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
375 [actor | fetch_likes(object)]
376 |> Enum.uniq()
377 |> update_likes_in_object(object)
378 end
379
380 @spec remove_like_from_object(Activity.t(), Object.t()) ::
381 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
382 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
383 object
384 |> fetch_likes()
385 |> List.delete(actor)
386 |> update_likes_in_object(object)
387 end
388
389 defp update_likes_in_object(likes, object) do
390 update_element_in_object("like", likes, object)
391 end
392
393 defp fetch_likes(object) do
394 if is_list(object.data["likes"]) do
395 object.data["likes"]
396 else
397 []
398 end
399 end
400
401 #### Follow-related helpers
402
403 @doc """
404 Updates a follow activity's state (for locked accounts).
405 """
406 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
407 def update_follow_state_for_all(
408 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
409 state
410 ) do
411 "Follow"
412 |> Activity.Queries.by_type()
413 |> Activity.Queries.by_actor(actor)
414 |> Activity.Queries.by_object_id(object)
415 |> where(fragment("data->>'state' = 'pending'"))
416 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
417 |> Repo.update_all([])
418
419 User.set_follow_state_cache(actor, object, state)
420
421 activity = Activity.get_by_id(activity.id)
422
423 {:ok, activity}
424 end
425
426 def update_follow_state(
427 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
428 state
429 ) do
430 new_data = Map.put(activity.data, "state", state)
431 changeset = Changeset.change(activity, data: new_data)
432
433 with {:ok, activity} <- Repo.update(changeset) do
434 User.set_follow_state_cache(actor, object, state)
435 {:ok, activity}
436 end
437 end
438
439 @doc """
440 Makes a follow activity data for the given follower and followed
441 """
442 def make_follow_data(
443 %User{ap_id: follower_id},
444 %User{ap_id: followed_id} = _followed,
445 activity_id
446 ) do
447 %{
448 "type" => "Follow",
449 "actor" => follower_id,
450 "to" => [followed_id],
451 "cc" => [Pleroma.Constants.as_public()],
452 "object" => followed_id,
453 "state" => "pending"
454 }
455 |> maybe_put("id", activity_id)
456 end
457
458 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
459 "Follow"
460 |> Activity.Queries.by_type()
461 |> where(actor: ^follower_id)
462 # this is to use the index
463 |> Activity.Queries.by_object_id(followed_id)
464 |> order_by([activity], fragment("? desc nulls last", activity.id))
465 |> limit(1)
466 |> Repo.one()
467 end
468
469 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
470 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
471
472 "EmojiReaction"
473 |> Activity.Queries.by_type()
474 |> where(actor: ^ap_id)
475 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
476 |> Activity.Queries.by_object_id(object_ap_id)
477 |> order_by([activity], fragment("? desc nulls last", activity.id))
478 |> limit(1)
479 |> Repo.one()
480 end
481
482 #### Announce-related helpers
483
484 @doc """
485 Retruns an existing announce activity if the notice has already been announced
486 """
487 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
488 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
489 "Announce"
490 |> Activity.Queries.by_type()
491 |> where(actor: ^actor)
492 # this is to use the index
493 |> Activity.Queries.by_object_id(ap_id)
494 |> Repo.one()
495 end
496
497 @doc """
498 Make announce activity data for the given actor and object
499 """
500 # for relayed messages, we only want to send to subscribers
501 def make_announce_data(
502 %User{ap_id: ap_id} = user,
503 %Object{data: %{"id" => id}} = object,
504 activity_id,
505 false
506 ) do
507 %{
508 "type" => "Announce",
509 "actor" => ap_id,
510 "object" => id,
511 "to" => [user.follower_address],
512 "cc" => [],
513 "context" => object.data["context"]
514 }
515 |> maybe_put("id", activity_id)
516 end
517
518 def make_announce_data(
519 %User{ap_id: ap_id} = user,
520 %Object{data: %{"id" => id}} = object,
521 activity_id,
522 true
523 ) do
524 %{
525 "type" => "Announce",
526 "actor" => ap_id,
527 "object" => id,
528 "to" => [user.follower_address, object.data["actor"]],
529 "cc" => [Pleroma.Constants.as_public()],
530 "context" => object.data["context"]
531 }
532 |> maybe_put("id", activity_id)
533 end
534
535 @doc """
536 Make unannounce activity data for the given actor and object
537 """
538 def make_unannounce_data(
539 %User{ap_id: ap_id} = user,
540 %Activity{data: %{"context" => context, "object" => object}} = activity,
541 activity_id
542 ) do
543 object = Object.normalize(object)
544
545 %{
546 "type" => "Undo",
547 "actor" => ap_id,
548 "object" => activity.data,
549 "to" => [user.follower_address, object.data["actor"]],
550 "cc" => [Pleroma.Constants.as_public()],
551 "context" => context
552 }
553 |> maybe_put("id", activity_id)
554 end
555
556 def make_unlike_data(
557 %User{ap_id: ap_id} = user,
558 %Activity{data: %{"context" => context, "object" => object}} = activity,
559 activity_id
560 ) do
561 object = Object.normalize(object)
562
563 %{
564 "type" => "Undo",
565 "actor" => ap_id,
566 "object" => activity.data,
567 "to" => [user.follower_address, object.data["actor"]],
568 "cc" => [Pleroma.Constants.as_public()],
569 "context" => context
570 }
571 |> maybe_put("id", activity_id)
572 end
573
574 def make_undo_data(
575 %User{ap_id: actor, follower_address: follower_address},
576 %Activity{
577 data: %{"id" => undone_activity_id, "context" => context},
578 actor: undone_activity_actor
579 },
580 activity_id \\ nil
581 ) do
582 %{
583 "type" => "Undo",
584 "actor" => actor,
585 "object" => undone_activity_id,
586 "to" => [follower_address, undone_activity_actor],
587 "cc" => [Pleroma.Constants.as_public()],
588 "context" => context
589 }
590 |> maybe_put("id", activity_id)
591 end
592
593 @spec add_announce_to_object(Activity.t(), Object.t()) ::
594 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
595 def add_announce_to_object(
596 %Activity{data: %{"actor" => actor}},
597 object
598 ) do
599 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
600 announcements = take_announcements(object)
601
602 with announcements <- Enum.uniq([actor | announcements]) do
603 update_element_in_object("announcement", announcements, object)
604 end
605 else
606 {:ok, object}
607 end
608 end
609
610 def add_announce_to_object(_, object), do: {:ok, object}
611
612 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
613 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
614 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
615 with announcements <- List.delete(take_announcements(object), actor) do
616 update_element_in_object("announcement", announcements, object)
617 end
618 end
619
620 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
621 when is_list(announcements),
622 do: announcements
623
624 defp take_announcements(_), do: []
625
626 #### Unfollow-related helpers
627
628 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
629 %{
630 "type" => "Undo",
631 "actor" => follower.ap_id,
632 "to" => [followed.ap_id],
633 "object" => follow_activity.data
634 }
635 |> maybe_put("id", activity_id)
636 end
637
638 #### Block-related helpers
639 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
640 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
641 "Block"
642 |> Activity.Queries.by_type()
643 |> where(actor: ^blocker_id)
644 # this is to use the index
645 |> Activity.Queries.by_object_id(blocked_id)
646 |> order_by([activity], fragment("? desc nulls last", activity.id))
647 |> limit(1)
648 |> Repo.one()
649 end
650
651 def make_block_data(blocker, blocked, activity_id) do
652 %{
653 "type" => "Block",
654 "actor" => blocker.ap_id,
655 "to" => [blocked.ap_id],
656 "object" => blocked.ap_id
657 }
658 |> maybe_put("id", activity_id)
659 end
660
661 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
662 %{
663 "type" => "Undo",
664 "actor" => blocker.ap_id,
665 "to" => [blocked.ap_id],
666 "object" => block_activity.data
667 }
668 |> maybe_put("id", activity_id)
669 end
670
671 #### Create-related helpers
672
673 def make_create_data(params, additional) do
674 published = params.published || make_date()
675
676 %{
677 "type" => "Create",
678 "to" => params.to |> Enum.uniq(),
679 "actor" => params.actor.ap_id,
680 "object" => params.object,
681 "published" => published,
682 "context" => params.context
683 }
684 |> Map.merge(additional)
685 end
686
687 #### Listen-related helpers
688 def make_listen_data(params, additional) do
689 published = params.published || make_date()
690
691 %{
692 "type" => "Listen",
693 "to" => params.to |> Enum.uniq(),
694 "actor" => params.actor.ap_id,
695 "object" => params.object,
696 "published" => published,
697 "context" => params.context
698 }
699 |> Map.merge(additional)
700 end
701
702 #### Flag-related helpers
703 @spec make_flag_data(map(), map()) :: map()
704 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
705 %{
706 "type" => "Flag",
707 "actor" => actor.ap_id,
708 "content" => content,
709 "object" => build_flag_object(params),
710 "context" => context,
711 "state" => "open"
712 }
713 |> Map.merge(additional)
714 end
715
716 def make_flag_data(_, _), do: %{}
717
718 defp build_flag_object(%{account: account, statuses: statuses} = _) do
719 [account.ap_id] ++ build_flag_object(%{statuses: statuses})
720 end
721
722 defp build_flag_object(%{statuses: statuses}) do
723 Enum.map(statuses || [], &build_flag_object/1)
724 end
725
726 defp build_flag_object(act) when is_map(act) or is_binary(act) do
727 id =
728 case act do
729 %Activity{} = act -> act.data["id"]
730 act when is_map(act) -> act["id"]
731 act when is_binary(act) -> act
732 end
733
734 case Activity.get_by_ap_id_with_object(id) do
735 %Activity{} = activity ->
736 %{
737 "type" => "Note",
738 "id" => activity.data["id"],
739 "content" => activity.object.data["content"],
740 "published" => activity.object.data["published"],
741 "actor" =>
742 AccountView.render("show.json", %{
743 user: User.get_by_ap_id(activity.object.data["actor"])
744 })
745 }
746
747 _ ->
748 %{"id" => id, "deleted" => true}
749 end
750 end
751
752 defp build_flag_object(_), do: []
753
754 @doc """
755 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
756 the first one to `pages_left` pages.
757 If the amount of pages is higher than the collection has, it returns whatever was there.
758 """
759 def fetch_ordered_collection(from, pages_left, acc \\ []) do
760 with {:ok, response} <- Tesla.get(from),
761 {:ok, collection} <- Jason.decode(response.body) do
762 case collection["type"] do
763 "OrderedCollection" ->
764 # If we've encountered the OrderedCollection and not the page,
765 # just call the same function on the page address
766 fetch_ordered_collection(collection["first"], pages_left)
767
768 "OrderedCollectionPage" ->
769 if pages_left > 0 do
770 # There are still more pages
771 if Map.has_key?(collection, "next") do
772 # There are still more pages, go deeper saving what we have into the accumulator
773 fetch_ordered_collection(
774 collection["next"],
775 pages_left - 1,
776 acc ++ collection["orderedItems"]
777 )
778 else
779 # No more pages left, just return whatever we already have
780 acc ++ collection["orderedItems"]
781 end
782 else
783 # Got the amount of pages needed, add them all to the accumulator
784 acc ++ collection["orderedItems"]
785 end
786
787 _ ->
788 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
789 end
790 end
791 end
792
793 #### Report-related helpers
794 def get_reports(params, page, page_size) do
795 params =
796 params
797 |> Map.put("type", "Flag")
798 |> Map.put("skip_preload", true)
799 |> Map.put("preload_report_notes", true)
800 |> Map.put("total", true)
801 |> Map.put("limit", page_size)
802 |> Map.put("offset", (page - 1) * page_size)
803
804 ActivityPub.fetch_activities([], params, :offset)
805 end
806
807 def parse_report_group(activity) do
808 reports = get_reports_by_status_id(activity["id"])
809 max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
810 actors = Enum.map(reports, & &1.user_actor)
811 [%{data: %{"object" => [account_id | _]}} | _] = reports
812
813 account =
814 AccountView.render("show.json", %{
815 user: User.get_by_ap_id(account_id)
816 })
817
818 status = get_status_data(activity)
819
820 %{
821 date: max_date.data["published"],
822 account: account,
823 status: status,
824 actors: Enum.uniq(actors),
825 reports: reports
826 }
827 end
828
829 defp get_status_data(status) do
830 case status["deleted"] do
831 true ->
832 %{
833 "id" => status["id"],
834 "deleted" => true
835 }
836
837 _ ->
838 Activity.get_by_ap_id(status["id"])
839 end
840 end
841
842 def get_reports_by_status_id(ap_id) do
843 from(a in Activity,
844 where: fragment("(?)->>'type' = 'Flag'", a.data),
845 where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
846 or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
847 )
848 |> Activity.with_preloaded_user_actor()
849 |> Repo.all()
850 end
851
852 @spec get_reports_grouped_by_status([String.t()]) :: %{
853 required(:groups) => [
854 %{
855 required(:date) => String.t(),
856 required(:account) => %{},
857 required(:status) => %{},
858 required(:actors) => [%User{}],
859 required(:reports) => [%Activity{}]
860 }
861 ]
862 }
863 def get_reports_grouped_by_status(activity_ids) do
864 parsed_groups =
865 activity_ids
866 |> Enum.map(fn id ->
867 id
868 |> build_flag_object()
869 |> parse_report_group()
870 end)
871
872 %{
873 groups: parsed_groups
874 }
875 end
876
877 @spec get_reported_activities() :: [
878 %{
879 required(:activity) => String.t(),
880 required(:date) => String.t()
881 }
882 ]
883 def get_reported_activities do
884 reported_activities_query =
885 from(a in Activity,
886 where: fragment("(?)->>'type' = 'Flag'", a.data),
887 select: %{
888 activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
889 },
890 group_by: fragment("activity")
891 )
892
893 from(a in subquery(reported_activities_query),
894 distinct: true,
895 select: %{
896 id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
897 }
898 )
899 |> Repo.all()
900 |> Enum.map(& &1.id)
901 end
902
903 def update_report_state(%Activity{} = activity, state)
904 when state in @strip_status_report_states do
905 {:ok, stripped_activity} = strip_report_status_data(activity)
906
907 new_data =
908 activity.data
909 |> Map.put("state", state)
910 |> Map.put("object", stripped_activity.data["object"])
911
912 activity
913 |> Changeset.change(data: new_data)
914 |> Repo.update()
915 end
916
917 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
918 new_data = Map.put(activity.data, "state", state)
919
920 activity
921 |> Changeset.change(data: new_data)
922 |> Repo.update()
923 end
924
925 def update_report_state(activity_ids, state) when state in @supported_report_states do
926 activities_num = length(activity_ids)
927
928 from(a in Activity, where: a.id in ^activity_ids)
929 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
930 |> Repo.update_all([])
931 |> case do
932 {^activities_num, _} -> :ok
933 _ -> {:error, activity_ids}
934 end
935 end
936
937 def update_report_state(_, _), do: {:error, "Unsupported state"}
938
939 def strip_report_status_data(activity) do
940 [actor | reported_activities] = activity.data["object"]
941
942 stripped_activities =
943 Enum.map(reported_activities, fn
944 act when is_map(act) -> act["id"]
945 act when is_binary(act) -> act
946 end)
947
948 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
949
950 {:ok, %{activity | data: new_data}}
951 end
952
953 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
954 [to, cc, recipients] =
955 activity
956 |> get_updated_targets(visibility)
957 |> Enum.map(&Enum.uniq/1)
958
959 object_data =
960 activity.object.data
961 |> Map.put("to", to)
962 |> Map.put("cc", cc)
963
964 {:ok, object} =
965 activity.object
966 |> Object.change(%{data: object_data})
967 |> Object.update_and_set_cache()
968
969 activity_data =
970 activity.data
971 |> Map.put("to", to)
972 |> Map.put("cc", cc)
973
974 activity
975 |> Map.put(:object, object)
976 |> Activity.change(%{data: activity_data, recipients: recipients})
977 |> Repo.update()
978 end
979
980 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
981
982 defp get_updated_targets(
983 %Activity{data: %{"to" => to} = data, recipients: recipients},
984 visibility
985 ) do
986 cc = Map.get(data, "cc", [])
987 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
988 public = Pleroma.Constants.as_public()
989
990 case visibility do
991 "public" ->
992 to = [public | List.delete(to, follower_address)]
993 cc = [follower_address | List.delete(cc, public)]
994 recipients = [public | recipients]
995 [to, cc, recipients]
996
997 "private" ->
998 to = [follower_address | List.delete(to, public)]
999 cc = List.delete(cc, public)
1000 recipients = List.delete(recipients, public)
1001 [to, cc, recipients]
1002
1003 "unlisted" ->
1004 to = [follower_address | List.delete(to, public)]
1005 cc = [public | List.delete(cc, follower_address)]
1006 recipients = recipients ++ [follower_address, public]
1007 [to, cc, recipients]
1008
1009 _ ->
1010 [to, cc, recipients]
1011 end
1012 end
1013
1014 def get_existing_votes(actor, %{data: %{"id" => id}}) do
1015 actor
1016 |> Activity.Queries.by_actor()
1017 |> Activity.Queries.by_type("Create")
1018 |> Activity.with_preloaded_object()
1019 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
1020 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
1021 |> Repo.all()
1022 end
1023
1024 def maybe_put(map, _key, nil), do: map
1025 def maybe_put(map, key, value), do: Map.put(map, key, value)
1026 end