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