[#2332] Misc. improvements per code change requests.
[akkoma] / lib / pleroma / web / activity_pub / utils.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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()) :: [any]
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", "EmojiReact")
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 | nil}
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 activity = Activity.get_by_id(activity.id)
444
445 {:ok, activity}
446 end
447
448 def update_follow_state(
449 %Activity{} = activity,
450 state
451 ) do
452 new_data = Map.put(activity.data, "state", state)
453 changeset = Changeset.change(activity, data: new_data)
454
455 with {:ok, activity} <- Repo.update(changeset) do
456 {:ok, activity}
457 end
458 end
459
460 @doc """
461 Makes a follow activity data for the given follower and followed
462 """
463 def make_follow_data(
464 %User{ap_id: follower_id},
465 %User{ap_id: followed_id} = _followed,
466 activity_id
467 ) do
468 %{
469 "type" => "Follow",
470 "actor" => follower_id,
471 "to" => [followed_id],
472 "cc" => [Pleroma.Constants.as_public()],
473 "object" => followed_id,
474 "state" => "pending"
475 }
476 |> maybe_put("id", activity_id)
477 end
478
479 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
480 "Follow"
481 |> Activity.Queries.by_type()
482 |> where(actor: ^follower_id)
483 # this is to use the index
484 |> Activity.Queries.by_object_id(followed_id)
485 |> order_by([activity], fragment("? desc nulls last", activity.id))
486 |> limit(1)
487 |> Repo.one()
488 end
489
490 def fetch_latest_undo(%User{ap_id: ap_id}) do
491 "Undo"
492 |> Activity.Queries.by_type()
493 |> where(actor: ^ap_id)
494 |> order_by([activity], fragment("? desc nulls last", activity.id))
495 |> limit(1)
496 |> Repo.one()
497 end
498
499 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
500 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
501
502 "EmojiReact"
503 |> Activity.Queries.by_type()
504 |> where(actor: ^ap_id)
505 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
506 |> Activity.Queries.by_object_id(object_ap_id)
507 |> order_by([activity], fragment("? desc nulls last", activity.id))
508 |> limit(1)
509 |> Repo.one()
510 end
511
512 #### Announce-related helpers
513
514 @doc """
515 Retruns an existing announce activity if the notice has already been announced
516 """
517 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
518 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
519 "Announce"
520 |> Activity.Queries.by_type()
521 |> where(actor: ^actor)
522 # this is to use the index
523 |> Activity.Queries.by_object_id(ap_id)
524 |> Repo.one()
525 end
526
527 @doc """
528 Make announce activity data for the given actor and object
529 """
530 # for relayed messages, we only want to send to subscribers
531 def make_announce_data(
532 %User{ap_id: ap_id} = user,
533 %Object{data: %{"id" => id}} = object,
534 activity_id,
535 false
536 ) do
537 %{
538 "type" => "Announce",
539 "actor" => ap_id,
540 "object" => id,
541 "to" => [user.follower_address],
542 "cc" => [],
543 "context" => object.data["context"]
544 }
545 |> maybe_put("id", activity_id)
546 end
547
548 def make_announce_data(
549 %User{ap_id: ap_id} = user,
550 %Object{data: %{"id" => id}} = object,
551 activity_id,
552 true
553 ) do
554 %{
555 "type" => "Announce",
556 "actor" => ap_id,
557 "object" => id,
558 "to" => [user.follower_address, object.data["actor"]],
559 "cc" => [Pleroma.Constants.as_public()],
560 "context" => object.data["context"]
561 }
562 |> maybe_put("id", activity_id)
563 end
564
565 @doc """
566 Make unannounce activity data for the given actor and object
567 """
568 def make_unannounce_data(
569 %User{ap_id: ap_id} = user,
570 %Activity{data: %{"context" => context, "object" => object}} = activity,
571 activity_id
572 ) do
573 object = Object.normalize(object)
574
575 %{
576 "type" => "Undo",
577 "actor" => ap_id,
578 "object" => activity.data,
579 "to" => [user.follower_address, object.data["actor"]],
580 "cc" => [Pleroma.Constants.as_public()],
581 "context" => context
582 }
583 |> maybe_put("id", activity_id)
584 end
585
586 def make_unlike_data(
587 %User{ap_id: ap_id} = user,
588 %Activity{data: %{"context" => context, "object" => object}} = activity,
589 activity_id
590 ) do
591 object = Object.normalize(object)
592
593 %{
594 "type" => "Undo",
595 "actor" => ap_id,
596 "object" => activity.data,
597 "to" => [user.follower_address, object.data["actor"]],
598 "cc" => [Pleroma.Constants.as_public()],
599 "context" => context
600 }
601 |> maybe_put("id", activity_id)
602 end
603
604 def make_undo_data(
605 %User{ap_id: actor, follower_address: follower_address},
606 %Activity{
607 data: %{"id" => undone_activity_id, "context" => context},
608 actor: undone_activity_actor
609 },
610 activity_id \\ nil
611 ) do
612 %{
613 "type" => "Undo",
614 "actor" => actor,
615 "object" => undone_activity_id,
616 "to" => [follower_address, undone_activity_actor],
617 "cc" => [Pleroma.Constants.as_public()],
618 "context" => context
619 }
620 |> maybe_put("id", activity_id)
621 end
622
623 @spec add_announce_to_object(Activity.t(), Object.t()) ::
624 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
625 def add_announce_to_object(
626 %Activity{data: %{"actor" => actor}},
627 object
628 ) do
629 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
630 announcements = take_announcements(object)
631
632 with announcements <- Enum.uniq([actor | announcements]) do
633 update_element_in_object("announcement", announcements, object)
634 end
635 else
636 {:ok, object}
637 end
638 end
639
640 def add_announce_to_object(_, object), do: {:ok, object}
641
642 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
643 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
644 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
645 with announcements <- List.delete(take_announcements(object), actor) do
646 update_element_in_object("announcement", announcements, object)
647 end
648 end
649
650 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
651 when is_list(announcements),
652 do: announcements
653
654 defp take_announcements(_), do: []
655
656 #### Unfollow-related helpers
657
658 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
659 %{
660 "type" => "Undo",
661 "actor" => follower.ap_id,
662 "to" => [followed.ap_id],
663 "object" => follow_activity.data
664 }
665 |> maybe_put("id", activity_id)
666 end
667
668 #### Block-related helpers
669 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
670 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
671 "Block"
672 |> Activity.Queries.by_type()
673 |> where(actor: ^blocker_id)
674 # this is to use the index
675 |> Activity.Queries.by_object_id(blocked_id)
676 |> order_by([activity], fragment("? desc nulls last", activity.id))
677 |> limit(1)
678 |> Repo.one()
679 end
680
681 def make_block_data(blocker, blocked, activity_id) do
682 %{
683 "type" => "Block",
684 "actor" => blocker.ap_id,
685 "to" => [blocked.ap_id],
686 "object" => blocked.ap_id
687 }
688 |> maybe_put("id", activity_id)
689 end
690
691 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
692 %{
693 "type" => "Undo",
694 "actor" => blocker.ap_id,
695 "to" => [blocked.ap_id],
696 "object" => block_activity.data
697 }
698 |> maybe_put("id", activity_id)
699 end
700
701 #### Create-related helpers
702
703 def make_create_data(params, additional) do
704 published = params.published || make_date()
705
706 %{
707 "type" => "Create",
708 "to" => params.to |> Enum.uniq(),
709 "actor" => params.actor.ap_id,
710 "object" => params.object,
711 "published" => published,
712 "context" => params.context
713 }
714 |> Map.merge(additional)
715 end
716
717 #### Listen-related helpers
718 def make_listen_data(params, additional) do
719 published = params.published || make_date()
720
721 %{
722 "type" => "Listen",
723 "to" => params.to |> Enum.uniq(),
724 "actor" => params.actor.ap_id,
725 "object" => params.object,
726 "published" => published,
727 "context" => params.context
728 }
729 |> Map.merge(additional)
730 end
731
732 #### Flag-related helpers
733 @spec make_flag_data(map(), map()) :: map()
734 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
735 %{
736 "type" => "Flag",
737 "actor" => actor.ap_id,
738 "content" => content,
739 "object" => build_flag_object(params),
740 "context" => context,
741 "state" => "open"
742 }
743 |> Map.merge(additional)
744 end
745
746 def make_flag_data(_, _), do: %{}
747
748 defp build_flag_object(%{account: account, statuses: statuses} = _) do
749 [account.ap_id] ++ build_flag_object(%{statuses: statuses})
750 end
751
752 defp build_flag_object(%{statuses: statuses}) do
753 Enum.map(statuses || [], &build_flag_object/1)
754 end
755
756 defp build_flag_object(act) when is_map(act) or is_binary(act) do
757 id =
758 case act do
759 %Activity{} = act -> act.data["id"]
760 act when is_map(act) -> act["id"]
761 act when is_binary(act) -> act
762 end
763
764 case Activity.get_by_ap_id_with_object(id) do
765 %Activity{} = activity ->
766 %{
767 "type" => "Note",
768 "id" => activity.data["id"],
769 "content" => activity.object.data["content"],
770 "published" => activity.object.data["published"],
771 "actor" =>
772 AccountView.render("show.json", %{
773 user: User.get_by_ap_id(activity.object.data["actor"])
774 })
775 }
776
777 _ ->
778 %{"id" => id, "deleted" => true}
779 end
780 end
781
782 defp build_flag_object(_), do: []
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