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