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