9e460b6049da4978d8f9dcc26711ae10299bc909
[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 activity = Activity.get_by_ap_id_with_object(id)
726 actor = User.get_by_ap_id(activity.object.data["actor"])
727
728 %{
729 "type" => "Note",
730 "id" => activity.data["id"],
731 "content" => activity.object.data["content"],
732 "published" => activity.object.data["published"],
733 "actor" => AccountView.render("show.json", %{user: actor})
734 }
735 end
736
737 defp build_flag_object(_), do: []
738
739 @doc """
740 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
741 the first one to `pages_left` pages.
742 If the amount of pages is higher than the collection has, it returns whatever was there.
743 """
744 def fetch_ordered_collection(from, pages_left, acc \\ []) do
745 with {:ok, response} <- Tesla.get(from),
746 {:ok, collection} <- Jason.decode(response.body) do
747 case collection["type"] do
748 "OrderedCollection" ->
749 # If we've encountered the OrderedCollection and not the page,
750 # just call the same function on the page address
751 fetch_ordered_collection(collection["first"], pages_left)
752
753 "OrderedCollectionPage" ->
754 if pages_left > 0 do
755 # There are still more pages
756 if Map.has_key?(collection, "next") do
757 # There are still more pages, go deeper saving what we have into the accumulator
758 fetch_ordered_collection(
759 collection["next"],
760 pages_left - 1,
761 acc ++ collection["orderedItems"]
762 )
763 else
764 # No more pages left, just return whatever we already have
765 acc ++ collection["orderedItems"]
766 end
767 else
768 # Got the amount of pages needed, add them all to the accumulator
769 acc ++ collection["orderedItems"]
770 end
771
772 _ ->
773 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
774 end
775 end
776 end
777
778 #### Report-related helpers
779 def get_reports(params, page, page_size) do
780 params =
781 params
782 |> Map.put("type", "Flag")
783 |> Map.put("skip_preload", true)
784 |> Map.put("total", true)
785 |> Map.put("limit", page_size)
786 |> Map.put("offset", (page - 1) * page_size)
787
788 ActivityPub.fetch_activities([], params, :offset)
789 end
790
791 @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{
792 required(:groups) => [
793 %{
794 required(:date) => String.t(),
795 required(:account) => %{},
796 required(:status) => %{},
797 required(:actors) => [%User{}],
798 required(:reports) => [%Activity{}]
799 }
800 ],
801 required(:total) => integer
802 }
803 def get_reports_grouped_by_status(groups) do
804 parsed_groups =
805 groups
806 |> Enum.map(fn entry ->
807 activity =
808 case Jason.decode(entry.activity) do
809 {:ok, activity} -> activity
810 _ -> build_flag_object(entry.activity)
811 end
812
813 parse_report_group(activity)
814 end)
815
816 %{
817 groups: parsed_groups
818 }
819 end
820
821 def parse_report_group(activity) do
822 reports = get_reports_by_status_id(activity["id"])
823 max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
824 actors = Enum.map(reports, & &1.user_actor)
825 {deleted, status} = get_status_data(activity)
826
827 %{
828 date: max_date.data["published"],
829 account: activity["actor"],
830 status: status,
831 status_deleted: deleted,
832 actors: Enum.uniq(actors),
833 reports: reports
834 }
835 end
836
837 defp get_status_data(activity) do
838 case Activity.get_by_ap_id(activity["id"]) do
839 %Activity{} = act ->
840 {false, act}
841
842 _ ->
843 {true,
844 %{
845 id: activity["id"],
846 content: activity["content"],
847 published: activity["published"]
848 }}
849 end
850 end
851
852 def get_reports_by_status_id(ap_id) do
853 from(a in Activity,
854 where: fragment("(?)->>'type' = 'Flag'", a.data),
855 where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
856 or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
857 )
858 |> Activity.with_preloaded_user_actor()
859 |> Repo.all()
860 end
861
862 @spec get_reported_activities() :: [
863 %{
864 required(:activity) => String.t(),
865 required(:date) => String.t()
866 }
867 ]
868 def get_reported_activities do
869 from(a in Activity,
870 where: fragment("(?)->>'type' = 'Flag'", a.data),
871 select: %{
872 date: fragment("max(?->>'published') date", a.data),
873 activity:
874 fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data)
875 },
876 group_by: fragment("activity"),
877 order_by: fragment("date DESC")
878 )
879 |> Repo.all()
880 end
881
882 def update_report_state(%Activity{} = activity, state)
883 when state in @strip_status_report_states do
884 {:ok, stripped_activity} = strip_report_status_data(activity)
885
886 new_data =
887 activity.data
888 |> Map.put("state", state)
889 |> Map.put("object", stripped_activity.data["object"])
890
891 activity
892 |> Changeset.change(data: new_data)
893 |> Repo.update()
894 end
895
896 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
897 new_data = Map.put(activity.data, "state", state)
898
899 activity
900 |> Changeset.change(data: new_data)
901 |> Repo.update()
902 end
903
904 def update_report_state(activity_ids, state) when state in @supported_report_states do
905 activities_num = length(activity_ids)
906
907 from(a in Activity, where: a.id in ^activity_ids)
908 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
909 |> Repo.update_all([])
910 |> case do
911 {^activities_num, _} -> :ok
912 _ -> {:error, activity_ids}
913 end
914 end
915
916 def update_report_state(_, _), do: {:error, "Unsupported state"}
917
918 def strip_report_status_data(activity) do
919 [actor | reported_activities] = activity.data["object"]
920 stripped_activities = Enum.map(reported_activities, & &1["id"])
921 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
922
923 {:ok, %{activity | data: new_data}}
924 end
925
926 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
927 [to, cc, recipients] =
928 activity
929 |> get_updated_targets(visibility)
930 |> Enum.map(&Enum.uniq/1)
931
932 object_data =
933 activity.object.data
934 |> Map.put("to", to)
935 |> Map.put("cc", cc)
936
937 {:ok, object} =
938 activity.object
939 |> Object.change(%{data: object_data})
940 |> Object.update_and_set_cache()
941
942 activity_data =
943 activity.data
944 |> Map.put("to", to)
945 |> Map.put("cc", cc)
946
947 activity
948 |> Map.put(:object, object)
949 |> Activity.change(%{data: activity_data, recipients: recipients})
950 |> Repo.update()
951 end
952
953 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
954
955 defp get_updated_targets(
956 %Activity{data: %{"to" => to} = data, recipients: recipients},
957 visibility
958 ) do
959 cc = Map.get(data, "cc", [])
960 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
961 public = Pleroma.Constants.as_public()
962
963 case visibility do
964 "public" ->
965 to = [public | List.delete(to, follower_address)]
966 cc = [follower_address | List.delete(cc, public)]
967 recipients = [public | recipients]
968 [to, cc, recipients]
969
970 "private" ->
971 to = [follower_address | List.delete(to, public)]
972 cc = List.delete(cc, public)
973 recipients = List.delete(recipients, public)
974 [to, cc, recipients]
975
976 "unlisted" ->
977 to = [follower_address | List.delete(to, public)]
978 cc = [public | List.delete(cc, follower_address)]
979 recipients = recipients ++ [follower_address, public]
980 [to, cc, recipients]
981
982 _ ->
983 [to, cc, recipients]
984 end
985 end
986
987 def get_existing_votes(actor, %{data: %{"id" => id}}) do
988 actor
989 |> Activity.Queries.by_actor()
990 |> Activity.Queries.by_type("Create")
991 |> Activity.with_preloaded_object()
992 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
993 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
994 |> Repo.all()
995 end
996
997 def maybe_put(map, _key, nil), do: map
998 def maybe_put(map, key, value), do: Map.put(map, key, value)
999 end