clients.md: Add Kyclos
[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(), 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} | {:error, any()}
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 get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
494 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
495
496 "EmojiReaction"
497 |> Activity.Queries.by_type()
498 |> where(actor: ^ap_id)
499 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
500 |> Activity.Queries.by_object_id(object_ap_id)
501 |> order_by([activity], fragment("? desc nulls last", activity.id))
502 |> limit(1)
503 |> Repo.one()
504 end
505
506 #### Announce-related helpers
507
508 @doc """
509 Retruns an existing announce activity if the notice has already been announced
510 """
511 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
512 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
513 "Announce"
514 |> Activity.Queries.by_type()
515 |> where(actor: ^actor)
516 # this is to use the index
517 |> Activity.Queries.by_object_id(ap_id)
518 |> Repo.one()
519 end
520
521 @doc """
522 Make announce activity data for the given actor and object
523 """
524 # for relayed messages, we only want to send to subscribers
525 def make_announce_data(
526 %User{ap_id: ap_id} = user,
527 %Object{data: %{"id" => id}} = object,
528 activity_id,
529 false
530 ) do
531 %{
532 "type" => "Announce",
533 "actor" => ap_id,
534 "object" => id,
535 "to" => [user.follower_address],
536 "cc" => [],
537 "context" => object.data["context"]
538 }
539 |> maybe_put("id", activity_id)
540 end
541
542 def make_announce_data(
543 %User{ap_id: ap_id} = user,
544 %Object{data: %{"id" => id}} = object,
545 activity_id,
546 true
547 ) do
548 %{
549 "type" => "Announce",
550 "actor" => ap_id,
551 "object" => id,
552 "to" => [user.follower_address, object.data["actor"]],
553 "cc" => [Pleroma.Constants.as_public()],
554 "context" => object.data["context"]
555 }
556 |> maybe_put("id", activity_id)
557 end
558
559 @doc """
560 Make unannounce activity data for the given actor and object
561 """
562 def make_unannounce_data(
563 %User{ap_id: ap_id} = user,
564 %Activity{data: %{"context" => context, "object" => object}} = activity,
565 activity_id
566 ) do
567 object = Object.normalize(object)
568
569 %{
570 "type" => "Undo",
571 "actor" => ap_id,
572 "object" => activity.data,
573 "to" => [user.follower_address, object.data["actor"]],
574 "cc" => [Pleroma.Constants.as_public()],
575 "context" => context
576 }
577 |> maybe_put("id", activity_id)
578 end
579
580 def make_unlike_data(
581 %User{ap_id: ap_id} = user,
582 %Activity{data: %{"context" => context, "object" => object}} = activity,
583 activity_id
584 ) do
585 object = Object.normalize(object)
586
587 %{
588 "type" => "Undo",
589 "actor" => ap_id,
590 "object" => activity.data,
591 "to" => [user.follower_address, object.data["actor"]],
592 "cc" => [Pleroma.Constants.as_public()],
593 "context" => context
594 }
595 |> maybe_put("id", activity_id)
596 end
597
598 def make_undo_data(
599 %User{ap_id: actor, follower_address: follower_address},
600 %Activity{
601 data: %{"id" => undone_activity_id, "context" => context},
602 actor: undone_activity_actor
603 },
604 activity_id \\ nil
605 ) do
606 %{
607 "type" => "Undo",
608 "actor" => actor,
609 "object" => undone_activity_id,
610 "to" => [follower_address, undone_activity_actor],
611 "cc" => [Pleroma.Constants.as_public()],
612 "context" => context
613 }
614 |> maybe_put("id", activity_id)
615 end
616
617 @spec add_announce_to_object(Activity.t(), Object.t()) ::
618 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
619 def add_announce_to_object(
620 %Activity{data: %{"actor" => actor}},
621 object
622 ) do
623 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
624 announcements = take_announcements(object)
625
626 with announcements <- Enum.uniq([actor | announcements]) do
627 update_element_in_object("announcement", announcements, object)
628 end
629 else
630 {:ok, object}
631 end
632 end
633
634 def add_announce_to_object(_, object), do: {:ok, object}
635
636 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
637 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
638 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
639 with announcements <- List.delete(take_announcements(object), actor) do
640 update_element_in_object("announcement", announcements, object)
641 end
642 end
643
644 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
645 when is_list(announcements),
646 do: announcements
647
648 defp take_announcements(_), do: []
649
650 #### Unfollow-related helpers
651
652 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
653 %{
654 "type" => "Undo",
655 "actor" => follower.ap_id,
656 "to" => [followed.ap_id],
657 "object" => follow_activity.data
658 }
659 |> maybe_put("id", activity_id)
660 end
661
662 #### Block-related helpers
663 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
664 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
665 "Block"
666 |> Activity.Queries.by_type()
667 |> where(actor: ^blocker_id)
668 # this is to use the index
669 |> Activity.Queries.by_object_id(blocked_id)
670 |> order_by([activity], fragment("? desc nulls last", activity.id))
671 |> limit(1)
672 |> Repo.one()
673 end
674
675 def make_block_data(blocker, blocked, activity_id) do
676 %{
677 "type" => "Block",
678 "actor" => blocker.ap_id,
679 "to" => [blocked.ap_id],
680 "object" => blocked.ap_id
681 }
682 |> maybe_put("id", activity_id)
683 end
684
685 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
686 %{
687 "type" => "Undo",
688 "actor" => blocker.ap_id,
689 "to" => [blocked.ap_id],
690 "object" => block_activity.data
691 }
692 |> maybe_put("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(act) when is_map(act) or is_binary(act) do
751 id =
752 case act do
753 %Activity{} = act -> act.data["id"]
754 act when is_map(act) -> act["id"]
755 act when is_binary(act) -> act
756 end
757
758 case Activity.get_by_ap_id_with_object(id) do
759 %Activity{} = activity ->
760 %{
761 "type" => "Note",
762 "id" => activity.data["id"],
763 "content" => activity.object.data["content"],
764 "published" => activity.object.data["published"],
765 "actor" =>
766 AccountView.render("show.json", %{
767 user: User.get_by_ap_id(activity.object.data["actor"])
768 })
769 }
770
771 _ ->
772 %{"id" => id, "deleted" => true}
773 end
774 end
775
776 defp build_flag_object(_), do: []
777
778 @doc """
779 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
780 the first one to `pages_left` pages.
781 If the amount of pages is higher than the collection has, it returns whatever was there.
782 """
783 def fetch_ordered_collection(from, pages_left, acc \\ []) do
784 with {:ok, response} <- Tesla.get(from),
785 {:ok, collection} <- Jason.decode(response.body) do
786 case collection["type"] do
787 "OrderedCollection" ->
788 # If we've encountered the OrderedCollection and not the page,
789 # just call the same function on the page address
790 fetch_ordered_collection(collection["first"], pages_left)
791
792 "OrderedCollectionPage" ->
793 if pages_left > 0 do
794 # There are still more pages
795 if Map.has_key?(collection, "next") do
796 # There are still more pages, go deeper saving what we have into the accumulator
797 fetch_ordered_collection(
798 collection["next"],
799 pages_left - 1,
800 acc ++ collection["orderedItems"]
801 )
802 else
803 # No more pages left, just return whatever we already have
804 acc ++ collection["orderedItems"]
805 end
806 else
807 # Got the amount of pages needed, add them all to the accumulator
808 acc ++ collection["orderedItems"]
809 end
810
811 _ ->
812 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
813 end
814 end
815 end
816
817 #### Report-related helpers
818 def get_reports(params, page, page_size) do
819 params =
820 params
821 |> Map.put("type", "Flag")
822 |> Map.put("skip_preload", true)
823 |> Map.put("preload_report_notes", true)
824 |> Map.put("total", true)
825 |> Map.put("limit", page_size)
826 |> Map.put("offset", (page - 1) * page_size)
827
828 ActivityPub.fetch_activities([], params, :offset)
829 end
830
831 def parse_report_group(activity) do
832 reports = get_reports_by_status_id(activity["id"])
833 max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
834 actors = Enum.map(reports, & &1.user_actor)
835 [%{data: %{"object" => [account_id | _]}} | _] = reports
836
837 account =
838 AccountView.render("show.json", %{
839 user: User.get_by_ap_id(account_id)
840 })
841
842 status = get_status_data(activity)
843
844 %{
845 date: max_date.data["published"],
846 account: account,
847 status: status,
848 actors: Enum.uniq(actors),
849 reports: reports
850 }
851 end
852
853 defp get_status_data(status) do
854 case status["deleted"] do
855 true ->
856 %{
857 "id" => status["id"],
858 "deleted" => true
859 }
860
861 _ ->
862 Activity.get_by_ap_id(status["id"])
863 end
864 end
865
866 def get_reports_by_status_id(ap_id) do
867 from(a in Activity,
868 where: fragment("(?)->>'type' = 'Flag'", a.data),
869 where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
870 or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
871 )
872 |> Activity.with_preloaded_user_actor()
873 |> Repo.all()
874 end
875
876 @spec get_reports_grouped_by_status([String.t()]) :: %{
877 required(:groups) => [
878 %{
879 required(:date) => String.t(),
880 required(:account) => %{},
881 required(:status) => %{},
882 required(:actors) => [%User{}],
883 required(:reports) => [%Activity{}]
884 }
885 ]
886 }
887 def get_reports_grouped_by_status(activity_ids) do
888 parsed_groups =
889 activity_ids
890 |> Enum.map(fn id ->
891 id
892 |> build_flag_object()
893 |> parse_report_group()
894 end)
895
896 %{
897 groups: parsed_groups
898 }
899 end
900
901 @spec get_reported_activities() :: [
902 %{
903 required(:activity) => String.t(),
904 required(:date) => String.t()
905 }
906 ]
907 def get_reported_activities do
908 reported_activities_query =
909 from(a in Activity,
910 where: fragment("(?)->>'type' = 'Flag'", a.data),
911 select: %{
912 activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
913 },
914 group_by: fragment("activity")
915 )
916
917 from(a in subquery(reported_activities_query),
918 distinct: true,
919 select: %{
920 id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
921 }
922 )
923 |> Repo.all()
924 |> Enum.map(& &1.id)
925 end
926
927 def update_report_state(%Activity{} = activity, state)
928 when state in @strip_status_report_states do
929 {:ok, stripped_activity} = strip_report_status_data(activity)
930
931 new_data =
932 activity.data
933 |> Map.put("state", state)
934 |> Map.put("object", stripped_activity.data["object"])
935
936 activity
937 |> Changeset.change(data: new_data)
938 |> Repo.update()
939 end
940
941 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
942 new_data = Map.put(activity.data, "state", state)
943
944 activity
945 |> Changeset.change(data: new_data)
946 |> Repo.update()
947 end
948
949 def update_report_state(activity_ids, state) when state in @supported_report_states do
950 activities_num = length(activity_ids)
951
952 from(a in Activity, where: a.id in ^activity_ids)
953 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
954 |> Repo.update_all([])
955 |> case do
956 {^activities_num, _} -> :ok
957 _ -> {:error, activity_ids}
958 end
959 end
960
961 def update_report_state(_, _), do: {:error, "Unsupported state"}
962
963 def strip_report_status_data(activity) do
964 [actor | reported_activities] = activity.data["object"]
965
966 stripped_activities =
967 Enum.map(reported_activities, fn
968 act when is_map(act) -> act["id"]
969 act when is_binary(act) -> act
970 end)
971
972 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
973
974 {:ok, %{activity | data: new_data}}
975 end
976
977 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
978 [to, cc, recipients] =
979 activity
980 |> get_updated_targets(visibility)
981 |> Enum.map(&Enum.uniq/1)
982
983 object_data =
984 activity.object.data
985 |> Map.put("to", to)
986 |> Map.put("cc", cc)
987
988 {:ok, object} =
989 activity.object
990 |> Object.change(%{data: object_data})
991 |> Object.update_and_set_cache()
992
993 activity_data =
994 activity.data
995 |> Map.put("to", to)
996 |> Map.put("cc", cc)
997
998 activity
999 |> Map.put(:object, object)
1000 |> Activity.change(%{data: activity_data, recipients: recipients})
1001 |> Repo.update()
1002 end
1003
1004 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
1005
1006 defp get_updated_targets(
1007 %Activity{data: %{"to" => to} = data, recipients: recipients},
1008 visibility
1009 ) do
1010 cc = Map.get(data, "cc", [])
1011 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
1012 public = Pleroma.Constants.as_public()
1013
1014 case visibility do
1015 "public" ->
1016 to = [public | List.delete(to, follower_address)]
1017 cc = [follower_address | List.delete(cc, public)]
1018 recipients = [public | recipients]
1019 [to, cc, recipients]
1020
1021 "private" ->
1022 to = [follower_address | List.delete(to, public)]
1023 cc = List.delete(cc, public)
1024 recipients = List.delete(recipients, public)
1025 [to, cc, recipients]
1026
1027 "unlisted" ->
1028 to = [follower_address | List.delete(to, public)]
1029 cc = [public | List.delete(cc, follower_address)]
1030 recipients = recipients ++ [follower_address, public]
1031 [to, cc, recipients]
1032
1033 _ ->
1034 [to, cc, recipients]
1035 end
1036 end
1037
1038 def get_existing_votes(actor, %{data: %{"id" => id}}) do
1039 actor
1040 |> Activity.Queries.by_actor()
1041 |> Activity.Queries.by_type("Create")
1042 |> Activity.with_preloaded_object()
1043 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
1044 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
1045 |> Repo.all()
1046 end
1047
1048 def maybe_put(map, _key, nil), do: map
1049 def maybe_put(map, key, value), do: Map.put(map, key, value)
1050 end