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