ActivityPub: Fix non-federating blocks.
[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.Config
10 alias Pleroma.Notification
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.AdminAPI.AccountView
18 alias Pleroma.Web.Endpoint
19 alias Pleroma.Web.Router.Helpers
20
21 import Ecto.Query
22
23 require Logger
24 require Pleroma.Constants
25
26 @supported_object_types [
27 "Article",
28 "Note",
29 "Event",
30 "Video",
31 "Page",
32 "Question",
33 "Answer",
34 "Audio"
35 ]
36 @strip_status_report_states ~w(closed resolved)
37 @supported_report_states ~w(open closed resolved)
38 @valid_visibilities ~w(public unlisted private direct)
39
40 # Some implementations send the actor URI as the actor field, others send the entire actor object,
41 # so figure out what the actor's URI is based on what we have.
42 def get_ap_id(%{"id" => id} = _), do: id
43 def get_ap_id(id), do: id
44
45 def normalize_params(params) do
46 Map.put(params, "actor", get_ap_id(params["actor"]))
47 end
48
49 @spec determine_explicit_mentions(map()) :: [any]
50 def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
51 Enum.flat_map(tag, fn
52 %{"type" => "Mention", "href" => href} -> [href]
53 _ -> []
54 end)
55 end
56
57 def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
58 object
59 |> Map.put("tag", [tag])
60 |> determine_explicit_mentions()
61 end
62
63 def determine_explicit_mentions(_), do: []
64
65 @spec label_in_collection?(any(), any()) :: boolean()
66 defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
67 defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
68 defp label_in_collection?(_, _), do: false
69
70 @spec label_in_message?(String.t(), map()) :: boolean()
71 def label_in_message?(label, params),
72 do:
73 [params["to"], params["cc"], params["bto"], params["bcc"]]
74 |> Enum.any?(&label_in_collection?(label, &1))
75
76 @spec unaddressed_message?(map()) :: boolean()
77 def unaddressed_message?(params),
78 do:
79 [params["to"], params["cc"], params["bto"], params["bcc"]]
80 |> Enum.all?(&is_nil(&1))
81
82 @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
83 def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
84 do:
85 label_in_message?(ap_id, params) || unaddressed_message?(params) ||
86 User.following?(recipient, actor)
87
88 defp extract_list(target) when is_binary(target), do: [target]
89 defp extract_list(lst) when is_list(lst), do: lst
90 defp extract_list(_), do: []
91
92 def maybe_splice_recipient(ap_id, params) do
93 need_splice? =
94 !label_in_collection?(ap_id, params["to"]) &&
95 !label_in_collection?(ap_id, params["cc"])
96
97 if need_splice? do
98 cc_list = extract_list(params["cc"])
99 Map.put(params, "cc", [ap_id | cc_list])
100 else
101 params
102 end
103 end
104
105 def make_json_ld_header do
106 %{
107 "@context" => [
108 "https://www.w3.org/ns/activitystreams",
109 "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
110 %{
111 "@language" => "und"
112 }
113 ]
114 }
115 end
116
117 def make_date do
118 DateTime.utc_now() |> DateTime.to_iso8601()
119 end
120
121 def generate_activity_id do
122 generate_id("activities")
123 end
124
125 def generate_context_id do
126 generate_id("contexts")
127 end
128
129 def generate_object_id do
130 Helpers.o_status_url(Endpoint, :object, UUID.generate())
131 end
132
133 def generate_id(type) do
134 "#{Web.base_url()}/#{type}/#{UUID.generate()}"
135 end
136
137 def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
138 fake_create_activity = %{
139 "to" => object["to"],
140 "cc" => object["cc"],
141 "type" => "Create",
142 "object" => object
143 }
144
145 get_notified_from_object(fake_create_activity)
146 end
147
148 def get_notified_from_object(object) do
149 Notification.get_notified_from_activity(%Activity{data: object}, false)
150 end
151
152 def create_context(context) do
153 context = context || generate_id("contexts")
154
155 # Ecto has problems accessing the constraint inside the jsonb,
156 # so we explicitly check for the existed object before insert
157 object = Object.get_cached_by_ap_id(context)
158
159 with true <- is_nil(object),
160 changeset <- Object.context_mapping(context),
161 {:ok, inserted_object} <- Repo.insert(changeset) do
162 inserted_object
163 else
164 _ ->
165 object
166 end
167 end
168
169 @doc """
170 Enqueues an activity for federation if it's local
171 """
172 @spec maybe_federate(any()) :: :ok
173 def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
174 outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
175
176 with true <- Config.get!([:instance, :federating]),
177 true <- type != "Block" || outgoing_blocks do
178 Pleroma.Web.Federator.publish(activity)
179 end
180
181 :ok
182 end
183
184 def maybe_federate(_), do: :ok
185
186 @doc """
187 Adds an id and a published data if they aren't there,
188 also adds it to an included object
189 """
190 @spec lazy_put_activity_defaults(map(), boolean) :: map()
191 def lazy_put_activity_defaults(map, fake? \\ false)
192
193 def lazy_put_activity_defaults(map, true) do
194 map
195 |> Map.put_new("id", "pleroma:fakeid")
196 |> Map.put_new_lazy("published", &make_date/0)
197 |> Map.put_new("context", "pleroma:fakecontext")
198 |> Map.put_new("context_id", -1)
199 |> lazy_put_object_defaults(true)
200 end
201
202 def lazy_put_activity_defaults(map, _fake?) do
203 %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
204
205 map
206 |> Map.put_new_lazy("id", &generate_activity_id/0)
207 |> Map.put_new_lazy("published", &make_date/0)
208 |> Map.put_new("context", context)
209 |> Map.put_new("context_id", context_id)
210 |> lazy_put_object_defaults(false)
211 end
212
213 # Adds an id and published date if they aren't there.
214 #
215 @spec lazy_put_object_defaults(map(), boolean()) :: map()
216 defp lazy_put_object_defaults(%{"object" => map} = activity, true)
217 when is_map(map) do
218 object =
219 map
220 |> Map.put_new("id", "pleroma:fake_object_id")
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 |> Map.put_new("fake", true)
225
226 %{activity | "object" => object}
227 end
228
229 defp lazy_put_object_defaults(%{"object" => map} = activity, _)
230 when is_map(map) do
231 object =
232 map
233 |> Map.put_new_lazy("id", &generate_object_id/0)
234 |> Map.put_new_lazy("published", &make_date/0)
235 |> Map.put_new("context", activity["context"])
236 |> Map.put_new("context_id", activity["context_id"])
237
238 %{activity | "object" => object}
239 end
240
241 defp lazy_put_object_defaults(activity, _), do: activity
242
243 @doc """
244 Inserts a full object if it is contained in an activity.
245 """
246 def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
247 when is_map(object_data) and type in @supported_object_types do
248 with {:ok, object} <- Object.create(object_data) do
249 map = Map.put(map, "object", object.data["id"])
250
251 {:ok, map, object}
252 end
253 end
254
255 def insert_full_object(map), do: {:ok, map, nil}
256
257 #### Like-related helpers
258
259 @doc """
260 Returns an existing like if a user already liked an object
261 """
262 @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
263 def get_existing_like(actor, %{data: %{"id" => id}}) do
264 actor
265 |> Activity.Queries.by_actor()
266 |> Activity.Queries.by_object_id(id)
267 |> Activity.Queries.by_type("Like")
268 |> limit(1)
269 |> Repo.one()
270 end
271
272 @doc """
273 Returns like activities targeting an object
274 """
275 def get_object_likes(%{data: %{"id" => id}}) do
276 id
277 |> Activity.Queries.by_object_id()
278 |> Activity.Queries.by_type("Like")
279 |> Repo.all()
280 end
281
282 @spec make_like_data(User.t(), map(), String.t()) :: map()
283 def make_like_data(
284 %User{ap_id: ap_id} = actor,
285 %{data: %{"actor" => object_actor_id, "id" => id}} = object,
286 activity_id
287 ) do
288 object_actor = User.get_cached_by_ap_id(object_actor_id)
289
290 to =
291 if Visibility.is_public?(object) do
292 [actor.follower_address, object.data["actor"]]
293 else
294 [object.data["actor"]]
295 end
296
297 cc =
298 (object.data["to"] ++ (object.data["cc"] || []))
299 |> List.delete(actor.ap_id)
300 |> List.delete(object_actor.follower_address)
301
302 %{
303 "type" => "Like",
304 "actor" => ap_id,
305 "object" => id,
306 "to" => to,
307 "cc" => cc,
308 "context" => object.data["context"]
309 }
310 |> maybe_put("id", activity_id)
311 end
312
313 def make_emoji_reaction_data(user, object, emoji, activity_id) do
314 make_like_data(user, object, activity_id)
315 |> Map.put("type", "EmojiReact")
316 |> Map.put("content", emoji)
317 end
318
319 @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
320 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
321 def update_element_in_object(property, element, object, count \\ nil) do
322 length =
323 count ||
324 length(element)
325
326 data =
327 Map.merge(
328 object.data,
329 %{"#{property}_count" => length, "#{property}s" => element}
330 )
331
332 object
333 |> Changeset.change(data: data)
334 |> Object.update_and_set_cache()
335 end
336
337 @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
338 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
339
340 def add_emoji_reaction_to_object(
341 %Activity{data: %{"content" => emoji, "actor" => actor}},
342 object
343 ) do
344 reactions = get_cached_emoji_reactions(object)
345
346 new_reactions =
347 case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
348 nil ->
349 reactions ++ [[emoji, [actor]]]
350
351 index ->
352 List.update_at(
353 reactions,
354 index,
355 fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
356 )
357 end
358
359 count = emoji_count(new_reactions)
360
361 update_element_in_object("reaction", new_reactions, object, count)
362 end
363
364 def emoji_count(reactions_list) do
365 Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
366 end
367
368 def remove_emoji_reaction_from_object(
369 %Activity{data: %{"content" => emoji, "actor" => actor}},
370 object
371 ) do
372 reactions = get_cached_emoji_reactions(object)
373
374 new_reactions =
375 case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
376 nil ->
377 reactions
378
379 index ->
380 List.update_at(
381 reactions,
382 index,
383 fn [emoji, users] -> [emoji, List.delete(users, actor)] end
384 )
385 |> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
386 end
387
388 count = emoji_count(new_reactions)
389 update_element_in_object("reaction", new_reactions, object, count)
390 end
391
392 def get_cached_emoji_reactions(object) do
393 if is_list(object.data["reactions"]) do
394 object.data["reactions"]
395 else
396 []
397 end
398 end
399
400 @spec add_like_to_object(Activity.t(), Object.t()) ::
401 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
402 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
403 [actor | fetch_likes(object)]
404 |> Enum.uniq()
405 |> update_likes_in_object(object)
406 end
407
408 @spec remove_like_from_object(Activity.t(), Object.t()) ::
409 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
410 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
411 object
412 |> fetch_likes()
413 |> List.delete(actor)
414 |> update_likes_in_object(object)
415 end
416
417 defp update_likes_in_object(likes, object) do
418 update_element_in_object("like", likes, object)
419 end
420
421 defp fetch_likes(object) do
422 if is_list(object.data["likes"]) do
423 object.data["likes"]
424 else
425 []
426 end
427 end
428
429 #### Follow-related helpers
430
431 @doc """
432 Updates a follow activity's state (for locked accounts).
433 """
434 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
435 def update_follow_state_for_all(
436 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
437 state
438 ) do
439 "Follow"
440 |> Activity.Queries.by_type()
441 |> Activity.Queries.by_actor(actor)
442 |> Activity.Queries.by_object_id(object)
443 |> where(fragment("data->>'state' = 'pending'"))
444 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
445 |> Repo.update_all([])
446
447 User.set_follow_state_cache(actor, object, state)
448
449 activity = Activity.get_by_id(activity.id)
450
451 {:ok, activity}
452 end
453
454 def update_follow_state(
455 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
456 state
457 ) do
458 new_data = Map.put(activity.data, "state", state)
459 changeset = Changeset.change(activity, data: new_data)
460
461 with {:ok, activity} <- Repo.update(changeset) do
462 User.set_follow_state_cache(actor, object, state)
463 {:ok, activity}
464 end
465 end
466
467 @doc """
468 Makes a follow activity data for the given follower and followed
469 """
470 def make_follow_data(
471 %User{ap_id: follower_id},
472 %User{ap_id: followed_id} = _followed,
473 activity_id
474 ) do
475 %{
476 "type" => "Follow",
477 "actor" => follower_id,
478 "to" => [followed_id],
479 "cc" => [Pleroma.Constants.as_public()],
480 "object" => followed_id,
481 "state" => "pending"
482 }
483 |> maybe_put("id", activity_id)
484 end
485
486 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
487 "Follow"
488 |> Activity.Queries.by_type()
489 |> where(actor: ^follower_id)
490 # this is to use the index
491 |> Activity.Queries.by_object_id(followed_id)
492 |> order_by([activity], fragment("? desc nulls last", activity.id))
493 |> limit(1)
494 |> Repo.one()
495 end
496
497 def fetch_latest_undo(%User{ap_id: ap_id}) do
498 "Undo"
499 |> Activity.Queries.by_type()
500 |> where(actor: ^ap_id)
501 |> order_by([activity], fragment("? desc nulls last", activity.id))
502 |> limit(1)
503 |> Repo.one()
504 end
505
506 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
507 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
508
509 "EmojiReact"
510 |> Activity.Queries.by_type()
511 |> where(actor: ^ap_id)
512 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
513 |> Activity.Queries.by_object_id(object_ap_id)
514 |> order_by([activity], fragment("? desc nulls last", activity.id))
515 |> limit(1)
516 |> Repo.one()
517 end
518
519 #### Announce-related helpers
520
521 @doc """
522 Retruns an existing announce activity if the notice has already been announced
523 """
524 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
525 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
526 "Announce"
527 |> Activity.Queries.by_type()
528 |> where(actor: ^actor)
529 # this is to use the index
530 |> Activity.Queries.by_object_id(ap_id)
531 |> Repo.one()
532 end
533
534 @doc """
535 Make announce activity data for the given actor and object
536 """
537 # for relayed messages, we only want to send to subscribers
538 def make_announce_data(
539 %User{ap_id: ap_id} = user,
540 %Object{data: %{"id" => id}} = object,
541 activity_id,
542 false
543 ) do
544 %{
545 "type" => "Announce",
546 "actor" => ap_id,
547 "object" => id,
548 "to" => [user.follower_address],
549 "cc" => [],
550 "context" => object.data["context"]
551 }
552 |> maybe_put("id", activity_id)
553 end
554
555 def make_announce_data(
556 %User{ap_id: ap_id} = user,
557 %Object{data: %{"id" => id}} = object,
558 activity_id,
559 true
560 ) do
561 %{
562 "type" => "Announce",
563 "actor" => ap_id,
564 "object" => id,
565 "to" => [user.follower_address, object.data["actor"]],
566 "cc" => [Pleroma.Constants.as_public()],
567 "context" => object.data["context"]
568 }
569 |> maybe_put("id", activity_id)
570 end
571
572 @doc """
573 Make unannounce activity data for the given actor and object
574 """
575 def make_unannounce_data(
576 %User{ap_id: ap_id} = user,
577 %Activity{data: %{"context" => context, "object" => object}} = activity,
578 activity_id
579 ) do
580 object = Object.normalize(object)
581
582 %{
583 "type" => "Undo",
584 "actor" => ap_id,
585 "object" => activity.data,
586 "to" => [user.follower_address, object.data["actor"]],
587 "cc" => [Pleroma.Constants.as_public()],
588 "context" => context
589 }
590 |> maybe_put("id", activity_id)
591 end
592
593 def make_unlike_data(
594 %User{ap_id: ap_id} = user,
595 %Activity{data: %{"context" => context, "object" => object}} = activity,
596 activity_id
597 ) do
598 object = Object.normalize(object)
599
600 %{
601 "type" => "Undo",
602 "actor" => ap_id,
603 "object" => activity.data,
604 "to" => [user.follower_address, object.data["actor"]],
605 "cc" => [Pleroma.Constants.as_public()],
606 "context" => context
607 }
608 |> maybe_put("id", activity_id)
609 end
610
611 def make_undo_data(
612 %User{ap_id: actor, follower_address: follower_address},
613 %Activity{
614 data: %{"id" => undone_activity_id, "context" => context},
615 actor: undone_activity_actor
616 },
617 activity_id \\ nil
618 ) do
619 %{
620 "type" => "Undo",
621 "actor" => actor,
622 "object" => undone_activity_id,
623 "to" => [follower_address, undone_activity_actor],
624 "cc" => [Pleroma.Constants.as_public()],
625 "context" => context
626 }
627 |> maybe_put("id", activity_id)
628 end
629
630 @spec add_announce_to_object(Activity.t(), Object.t()) ::
631 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
632 def add_announce_to_object(
633 %Activity{data: %{"actor" => actor}},
634 object
635 ) do
636 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
637 announcements = take_announcements(object)
638
639 with announcements <- Enum.uniq([actor | announcements]) do
640 update_element_in_object("announcement", announcements, object)
641 end
642 else
643 {:ok, object}
644 end
645 end
646
647 def add_announce_to_object(_, object), do: {:ok, object}
648
649 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
650 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
651 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
652 with announcements <- List.delete(take_announcements(object), actor) do
653 update_element_in_object("announcement", announcements, object)
654 end
655 end
656
657 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
658 when is_list(announcements),
659 do: announcements
660
661 defp take_announcements(_), do: []
662
663 #### Unfollow-related helpers
664
665 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
666 %{
667 "type" => "Undo",
668 "actor" => follower.ap_id,
669 "to" => [followed.ap_id],
670 "object" => follow_activity.data
671 }
672 |> maybe_put("id", activity_id)
673 end
674
675 #### Block-related helpers
676 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
677 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
678 "Block"
679 |> Activity.Queries.by_type()
680 |> where(actor: ^blocker_id)
681 # this is to use the index
682 |> Activity.Queries.by_object_id(blocked_id)
683 |> order_by([activity], fragment("? desc nulls last", activity.id))
684 |> limit(1)
685 |> Repo.one()
686 end
687
688 def make_block_data(blocker, blocked, activity_id) do
689 %{
690 "type" => "Block",
691 "actor" => blocker.ap_id,
692 "to" => [blocked.ap_id],
693 "object" => blocked.ap_id
694 }
695 |> maybe_put("id", activity_id)
696 end
697
698 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
699 %{
700 "type" => "Undo",
701 "actor" => blocker.ap_id,
702 "to" => [blocked.ap_id],
703 "object" => block_activity.data
704 }
705 |> maybe_put("id", activity_id)
706 end
707
708 #### Create-related helpers
709
710 def make_create_data(params, additional) do
711 published = params.published || make_date()
712
713 %{
714 "type" => "Create",
715 "to" => params.to |> Enum.uniq(),
716 "actor" => params.actor.ap_id,
717 "object" => params.object,
718 "published" => published,
719 "context" => params.context
720 }
721 |> Map.merge(additional)
722 end
723
724 #### Listen-related helpers
725 def make_listen_data(params, additional) do
726 published = params.published || make_date()
727
728 %{
729 "type" => "Listen",
730 "to" => params.to |> Enum.uniq(),
731 "actor" => params.actor.ap_id,
732 "object" => params.object,
733 "published" => published,
734 "context" => params.context
735 }
736 |> Map.merge(additional)
737 end
738
739 #### Flag-related helpers
740 @spec make_flag_data(map(), map()) :: map()
741 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
742 %{
743 "type" => "Flag",
744 "actor" => actor.ap_id,
745 "content" => content,
746 "object" => build_flag_object(params),
747 "context" => context,
748 "state" => "open"
749 }
750 |> Map.merge(additional)
751 end
752
753 def make_flag_data(_, _), do: %{}
754
755 defp build_flag_object(%{account: account, statuses: statuses} = _) do
756 [account.ap_id] ++ build_flag_object(%{statuses: statuses})
757 end
758
759 defp build_flag_object(%{statuses: statuses}) do
760 Enum.map(statuses || [], &build_flag_object/1)
761 end
762
763 defp build_flag_object(act) when is_map(act) or is_binary(act) do
764 id =
765 case act do
766 %Activity{} = act -> act.data["id"]
767 act when is_map(act) -> act["id"]
768 act when is_binary(act) -> act
769 end
770
771 case Activity.get_by_ap_id_with_object(id) do
772 %Activity{} = activity ->
773 %{
774 "type" => "Note",
775 "id" => activity.data["id"],
776 "content" => activity.object.data["content"],
777 "published" => activity.object.data["published"],
778 "actor" =>
779 AccountView.render("show.json", %{
780 user: User.get_by_ap_id(activity.object.data["actor"])
781 })
782 }
783
784 _ ->
785 %{"id" => id, "deleted" => true}
786 end
787 end
788
789 defp build_flag_object(_), do: []
790
791 #### Report-related helpers
792 def get_reports(params, page, page_size) do
793 params =
794 params
795 |> Map.put("type", "Flag")
796 |> Map.put("skip_preload", true)
797 |> Map.put("preload_report_notes", true)
798 |> Map.put("total", true)
799 |> Map.put("limit", page_size)
800 |> Map.put("offset", (page - 1) * page_size)
801
802 ActivityPub.fetch_activities([], params, :offset)
803 end
804
805 def parse_report_group(activity) do
806 reports = get_reports_by_status_id(activity["id"])
807 max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
808 actors = Enum.map(reports, & &1.user_actor)
809 [%{data: %{"object" => [account_id | _]}} | _] = reports
810
811 account =
812 AccountView.render("show.json", %{
813 user: User.get_by_ap_id(account_id)
814 })
815
816 status = get_status_data(activity)
817
818 %{
819 date: max_date.data["published"],
820 account: account,
821 status: status,
822 actors: Enum.uniq(actors),
823 reports: reports
824 }
825 end
826
827 defp get_status_data(status) do
828 case status["deleted"] do
829 true ->
830 %{
831 "id" => status["id"],
832 "deleted" => true
833 }
834
835 _ ->
836 Activity.get_by_ap_id(status["id"])
837 end
838 end
839
840 def get_reports_by_status_id(ap_id) do
841 from(a in Activity,
842 where: fragment("(?)->>'type' = 'Flag'", a.data),
843 where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
844 or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
845 )
846 |> Activity.with_preloaded_user_actor()
847 |> Repo.all()
848 end
849
850 @spec get_reports_grouped_by_status([String.t()]) :: %{
851 required(:groups) => [
852 %{
853 required(:date) => String.t(),
854 required(:account) => %{},
855 required(:status) => %{},
856 required(:actors) => [%User{}],
857 required(:reports) => [%Activity{}]
858 }
859 ]
860 }
861 def get_reports_grouped_by_status(activity_ids) do
862 parsed_groups =
863 activity_ids
864 |> Enum.map(fn id ->
865 id
866 |> build_flag_object()
867 |> parse_report_group()
868 end)
869
870 %{
871 groups: parsed_groups
872 }
873 end
874
875 @spec get_reported_activities() :: [
876 %{
877 required(:activity) => String.t(),
878 required(:date) => String.t()
879 }
880 ]
881 def get_reported_activities do
882 reported_activities_query =
883 from(a in Activity,
884 where: fragment("(?)->>'type' = 'Flag'", a.data),
885 select: %{
886 activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
887 },
888 group_by: fragment("activity")
889 )
890
891 from(a in subquery(reported_activities_query),
892 distinct: true,
893 select: %{
894 id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
895 }
896 )
897 |> Repo.all()
898 |> Enum.map(& &1.id)
899 end
900
901 def update_report_state(%Activity{} = activity, state)
902 when state in @strip_status_report_states do
903 {:ok, stripped_activity} = strip_report_status_data(activity)
904
905 new_data =
906 activity.data
907 |> Map.put("state", state)
908 |> Map.put("object", stripped_activity.data["object"])
909
910 activity
911 |> Changeset.change(data: new_data)
912 |> Repo.update()
913 end
914
915 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
916 new_data = Map.put(activity.data, "state", state)
917
918 activity
919 |> Changeset.change(data: new_data)
920 |> Repo.update()
921 end
922
923 def update_report_state(activity_ids, state) when state in @supported_report_states do
924 activities_num = length(activity_ids)
925
926 from(a in Activity, where: a.id in ^activity_ids)
927 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
928 |> Repo.update_all([])
929 |> case do
930 {^activities_num, _} -> :ok
931 _ -> {:error, activity_ids}
932 end
933 end
934
935 def update_report_state(_, _), do: {:error, "Unsupported state"}
936
937 def strip_report_status_data(activity) do
938 [actor | reported_activities] = activity.data["object"]
939
940 stripped_activities =
941 Enum.map(reported_activities, fn
942 act when is_map(act) -> act["id"]
943 act when is_binary(act) -> act
944 end)
945
946 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
947
948 {:ok, %{activity | data: new_data}}
949 end
950
951 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
952 [to, cc, recipients] =
953 activity
954 |> get_updated_targets(visibility)
955 |> Enum.map(&Enum.uniq/1)
956
957 object_data =
958 activity.object.data
959 |> Map.put("to", to)
960 |> Map.put("cc", cc)
961
962 {:ok, object} =
963 activity.object
964 |> Object.change(%{data: object_data})
965 |> Object.update_and_set_cache()
966
967 activity_data =
968 activity.data
969 |> Map.put("to", to)
970 |> Map.put("cc", cc)
971
972 activity
973 |> Map.put(:object, object)
974 |> Activity.change(%{data: activity_data, recipients: recipients})
975 |> Repo.update()
976 end
977
978 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
979
980 defp get_updated_targets(
981 %Activity{data: %{"to" => to} = data, recipients: recipients},
982 visibility
983 ) do
984 cc = Map.get(data, "cc", [])
985 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
986 public = Pleroma.Constants.as_public()
987
988 case visibility do
989 "public" ->
990 to = [public | List.delete(to, follower_address)]
991 cc = [follower_address | List.delete(cc, public)]
992 recipients = [public | recipients]
993 [to, cc, recipients]
994
995 "private" ->
996 to = [follower_address | List.delete(to, public)]
997 cc = List.delete(cc, public)
998 recipients = List.delete(recipients, public)
999 [to, cc, recipients]
1000
1001 "unlisted" ->
1002 to = [follower_address | List.delete(to, public)]
1003 cc = [public | List.delete(cc, follower_address)]
1004 recipients = recipients ++ [follower_address, public]
1005 [to, cc, recipients]
1006
1007 _ ->
1008 [to, cc, recipients]
1009 end
1010 end
1011
1012 def get_existing_votes(actor, %{data: %{"id" => id}}) do
1013 actor
1014 |> Activity.Queries.by_actor()
1015 |> Activity.Queries.by_type("Create")
1016 |> Activity.with_preloaded_object()
1017 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
1018 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
1019 |> Repo.all()
1020 end
1021
1022 def maybe_put(map, _key, nil), do: map
1023 def maybe_put(map, key, value), do: Map.put(map, key, value)
1024 end