add custom emoji reaction support
[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 IO.inspect(emoji)
351 reactions = get_cached_emoji_reactions(object)
352 emoji = stripped_emoji_name(emoji)
353 new_reactions =
354 case Enum.find_index(reactions, fn [candidate, _, _] -> emoji == candidate end) do
355 nil ->
356 reactions ++ [[emoji, [actor], emoji_url(emoji, activity)]]
357
358 index ->
359 List.update_at(
360 reactions,
361 index,
362 fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
363 )
364 end
365
366 count = emoji_count(new_reactions)
367
368 update_element_in_object("reaction", new_reactions, object, count)
369 end
370
371 defp stripped_emoji_name(name) do
372 name
373 |> String.replace_leading(":", "")
374 |> String.replace_trailing(":", "")
375 end
376
377 defp emoji_url(name,
378 %Activity{
379 data: %{"tag" => [
380 %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
381 ]}
382 }), do: url
383 defp emoji_url(_, _), do: nil
384
385 def emoji_count(reactions_list) do
386 Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
387 end
388
389 def remove_emoji_reaction_from_object(
390 %Activity{data: %{"content" => emoji, "actor" => actor}},
391 object
392 ) do
393 reactions = get_cached_emoji_reactions(object)
394 new_reactions =
395 case Enum.find_index(reactions, fn [candidate, _, _] -> emoji == candidate end) do
396 nil ->
397 reactions
398
399 index ->
400 List.update_at(
401 reactions,
402 index,
403 fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
404 )
405 |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
406 end
407
408 count = emoji_count(new_reactions)
409 update_element_in_object("reaction", new_reactions, object, count)
410 end
411
412 def get_cached_emoji_reactions(object) do
413 if is_list(object.data["reactions"]) do
414 object.data["reactions"]
415 else
416 []
417 end
418 end
419
420 @spec add_like_to_object(Activity.t(), Object.t()) ::
421 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
422 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
423 [actor | fetch_likes(object)]
424 |> Enum.uniq()
425 |> update_likes_in_object(object)
426 end
427
428 @spec remove_like_from_object(Activity.t(), Object.t()) ::
429 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
430 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
431 object
432 |> fetch_likes()
433 |> List.delete(actor)
434 |> update_likes_in_object(object)
435 end
436
437 defp update_likes_in_object(likes, object) do
438 update_element_in_object("like", likes, object)
439 end
440
441 defp fetch_likes(object) do
442 if is_list(object.data["likes"]) do
443 object.data["likes"]
444 else
445 []
446 end
447 end
448
449 #### Follow-related helpers
450
451 @doc """
452 Updates a follow activity's state (for locked accounts).
453 """
454 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
455 def update_follow_state_for_all(
456 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
457 state
458 ) do
459 "Follow"
460 |> Activity.Queries.by_type()
461 |> Activity.Queries.by_actor(actor)
462 |> Activity.Queries.by_object_id(object)
463 |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
464 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
465 |> Repo.update_all([])
466
467 activity = Activity.get_by_id(activity.id)
468
469 {:ok, activity}
470 end
471
472 def update_follow_state(
473 %Activity{} = activity,
474 state
475 ) do
476 new_data = Map.put(activity.data, "state", state)
477 changeset = Changeset.change(activity, data: new_data)
478
479 with {:ok, activity} <- Repo.update(changeset) do
480 {:ok, activity}
481 end
482 end
483
484 @doc """
485 Makes a follow activity data for the given follower and followed
486 """
487 def make_follow_data(
488 %User{ap_id: follower_id},
489 %User{ap_id: followed_id} = _followed,
490 activity_id
491 ) do
492 %{
493 "type" => "Follow",
494 "actor" => follower_id,
495 "to" => [followed_id],
496 "cc" => [Pleroma.Constants.as_public()],
497 "object" => followed_id,
498 "state" => "pending"
499 }
500 |> Maps.put_if_present("id", activity_id)
501 end
502
503 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
504 "Follow"
505 |> Activity.Queries.by_type()
506 |> where(actor: ^follower_id)
507 # this is to use the index
508 |> Activity.Queries.by_object_id(followed_id)
509 |> order_by([activity], fragment("? desc nulls last", activity.id))
510 |> limit(1)
511 |> Repo.one()
512 end
513
514 def fetch_latest_undo(%User{ap_id: ap_id}) do
515 "Undo"
516 |> Activity.Queries.by_type()
517 |> where(actor: ^ap_id)
518 |> order_by([activity], fragment("? desc nulls last", activity.id))
519 |> limit(1)
520 |> Repo.one()
521 end
522
523 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
524 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
525
526 "EmojiReact"
527 |> Activity.Queries.by_type()
528 |> where(actor: ^ap_id)
529 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
530 |> Activity.Queries.by_object_id(object_ap_id)
531 |> order_by([activity], fragment("? desc nulls last", activity.id))
532 |> limit(1)
533 |> Repo.one()
534 end
535
536 #### Announce-related helpers
537
538 @doc """
539 Returns an existing announce activity if the notice has already been announced
540 """
541 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
542 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
543 "Announce"
544 |> Activity.Queries.by_type()
545 |> where(actor: ^actor)
546 # this is to use the index
547 |> Activity.Queries.by_object_id(ap_id)
548 |> Repo.one()
549 end
550
551 @doc """
552 Make announce activity data for the given actor and object
553 """
554 # for relayed messages, we only want to send to subscribers
555 def make_announce_data(
556 %User{ap_id: ap_id} = user,
557 %Object{data: %{"id" => id}} = object,
558 activity_id,
559 false
560 ) do
561 %{
562 "type" => "Announce",
563 "actor" => ap_id,
564 "object" => id,
565 "to" => [user.follower_address],
566 "cc" => [],
567 "context" => object.data["context"]
568 }
569 |> Maps.put_if_present("id", activity_id)
570 end
571
572 def make_announce_data(
573 %User{ap_id: ap_id} = user,
574 %Object{data: %{"id" => id}} = object,
575 activity_id,
576 true
577 ) do
578 %{
579 "type" => "Announce",
580 "actor" => ap_id,
581 "object" => id,
582 "to" => [user.follower_address, object.data["actor"]],
583 "cc" => [Pleroma.Constants.as_public()],
584 "context" => object.data["context"]
585 }
586 |> Maps.put_if_present("id", activity_id)
587 end
588
589 def make_undo_data(
590 %User{ap_id: actor, follower_address: follower_address},
591 %Activity{
592 data: %{"id" => undone_activity_id, "context" => context},
593 actor: undone_activity_actor
594 },
595 activity_id \\ nil
596 ) do
597 %{
598 "type" => "Undo",
599 "actor" => actor,
600 "object" => undone_activity_id,
601 "to" => [follower_address, undone_activity_actor],
602 "cc" => [Pleroma.Constants.as_public()],
603 "context" => context
604 }
605 |> Maps.put_if_present("id", activity_id)
606 end
607
608 @spec add_announce_to_object(Activity.t(), Object.t()) ::
609 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
610 def add_announce_to_object(
611 %Activity{data: %{"actor" => actor}},
612 object
613 ) do
614 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
615 announcements = take_announcements(object)
616
617 with announcements <- Enum.uniq([actor | announcements]) do
618 update_element_in_object("announcement", announcements, object)
619 end
620 else
621 {:ok, object}
622 end
623 end
624
625 def add_announce_to_object(_, object), do: {:ok, object}
626
627 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
628 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
629 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
630 with announcements <- List.delete(take_announcements(object), actor) do
631 update_element_in_object("announcement", announcements, object)
632 end
633 end
634
635 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
636 when is_list(announcements),
637 do: announcements
638
639 defp take_announcements(_), do: []
640
641 #### Unfollow-related helpers
642
643 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
644 %{
645 "type" => "Undo",
646 "actor" => follower.ap_id,
647 "to" => [followed.ap_id],
648 "object" => follow_activity.data
649 }
650 |> Maps.put_if_present("id", activity_id)
651 end
652
653 #### Block-related helpers
654 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
655 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
656 "Block"
657 |> Activity.Queries.by_type()
658 |> where(actor: ^blocker_id)
659 # this is to use the index
660 |> Activity.Queries.by_object_id(blocked_id)
661 |> order_by([activity], fragment("? desc nulls last", activity.id))
662 |> limit(1)
663 |> Repo.one()
664 end
665
666 def make_block_data(blocker, blocked, activity_id) do
667 %{
668 "type" => "Block",
669 "actor" => blocker.ap_id,
670 "to" => [blocked.ap_id],
671 "object" => blocked.ap_id
672 }
673 |> Maps.put_if_present("id", activity_id)
674 end
675
676 #### Create-related helpers
677
678 def make_create_data(params, additional) do
679 published = params.published || make_date()
680
681 %{
682 "type" => "Create",
683 "to" => params.to |> Enum.uniq(),
684 "actor" => params.actor.ap_id,
685 "object" => params.object,
686 "published" => published,
687 "context" => params.context
688 }
689 |> Map.merge(additional)
690 end
691
692 #### Listen-related helpers
693 def make_listen_data(params, additional) do
694 published = params.published || make_date()
695
696 %{
697 "type" => "Listen",
698 "to" => params.to |> Enum.uniq(),
699 "actor" => params.actor.ap_id,
700 "object" => params.object,
701 "published" => published,
702 "context" => params.context
703 }
704 |> Map.merge(additional)
705 end
706
707 #### Flag-related helpers
708 @spec make_flag_data(map(), map()) :: map()
709 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
710 %{
711 "type" => "Flag",
712 "actor" => actor.ap_id,
713 "content" => content,
714 "object" => build_flag_object(params),
715 "context" => context,
716 "state" => "open"
717 }
718 |> Map.merge(additional)
719 end
720
721 def make_flag_data(_, _), do: %{}
722
723 defp build_flag_object(%{account: account, statuses: statuses}) do
724 [account.ap_id | build_flag_object(%{statuses: statuses})]
725 end
726
727 defp build_flag_object(%{statuses: statuses}) do
728 Enum.map(statuses || [], &build_flag_object/1)
729 end
730
731 defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do
732 activity_actor = User.get_by_ap_id(data["actor"])
733
734 %{
735 "type" => "Note",
736 "id" => id,
737 "content" => data["content"],
738 "published" => data["published"],
739 "actor" =>
740 AccountView.render(
741 "show.json",
742 %{user: activity_actor, skip_visibility_check: true}
743 )
744 }
745 end
746
747 defp build_flag_object(act) when is_map(act) or is_binary(act) do
748 id =
749 case act do
750 %Activity{} = act -> act.data["id"]
751 act when is_map(act) -> act["id"]
752 act when is_binary(act) -> act
753 end
754
755 case Activity.get_by_ap_id_with_object(id) do
756 %Activity{} = activity ->
757 build_flag_object(activity)
758
759 nil ->
760 if activity = Activity.get_by_object_ap_id_with_object(id) do
761 build_flag_object(activity)
762 else
763 %{"id" => id, "deleted" => true}
764 end
765 end
766 end
767
768 defp build_flag_object(_), do: []
769
770 #### Report-related helpers
771 def get_reports(params, page, page_size) do
772 params =
773 params
774 |> Map.put(:type, "Flag")
775 |> Map.put(:skip_preload, true)
776 |> Map.put(:preload_report_notes, true)
777 |> Map.put(:total, true)
778 |> Map.put(:limit, page_size)
779 |> Map.put(:offset, (page - 1) * page_size)
780
781 ActivityPub.fetch_activities([], params, :offset)
782 end
783
784 def update_report_state(%Activity{} = activity, state)
785 when state in @strip_status_report_states do
786 {:ok, stripped_activity} = strip_report_status_data(activity)
787
788 new_data =
789 activity.data
790 |> Map.put("state", state)
791 |> Map.put("object", stripped_activity.data["object"])
792
793 activity
794 |> Changeset.change(data: new_data)
795 |> Repo.update()
796 end
797
798 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
799 new_data = Map.put(activity.data, "state", state)
800
801 activity
802 |> Changeset.change(data: new_data)
803 |> Repo.update()
804 end
805
806 def update_report_state(activity_ids, state) when state in @supported_report_states do
807 activities_num = length(activity_ids)
808
809 from(a in Activity, where: a.id in ^activity_ids)
810 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
811 |> Repo.update_all([])
812 |> case do
813 {^activities_num, _} -> :ok
814 _ -> {:error, activity_ids}
815 end
816 end
817
818 def update_report_state(_, _), do: {:error, "Unsupported state"}
819
820 def strip_report_status_data(activity) do
821 [actor | reported_activities] = activity.data["object"]
822
823 stripped_activities =
824 Enum.map(reported_activities, fn
825 act when is_map(act) -> act["id"]
826 act when is_binary(act) -> act
827 end)
828
829 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
830
831 {:ok, %{activity | data: new_data}}
832 end
833
834 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
835 [to, cc, recipients] =
836 activity
837 |> get_updated_targets(visibility)
838 |> Enum.map(&Enum.uniq/1)
839
840 object_data =
841 activity.object.data
842 |> Map.put("to", to)
843 |> Map.put("cc", cc)
844
845 {:ok, object} =
846 activity.object
847 |> Object.change(%{data: object_data})
848 |> Object.update_and_set_cache()
849
850 activity_data =
851 activity.data
852 |> Map.put("to", to)
853 |> Map.put("cc", cc)
854
855 activity
856 |> Map.put(:object, object)
857 |> Activity.change(%{data: activity_data, recipients: recipients})
858 |> Repo.update()
859 end
860
861 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
862
863 defp get_updated_targets(
864 %Activity{data: %{"to" => to} = data, recipients: recipients},
865 visibility
866 ) do
867 cc = Map.get(data, "cc", [])
868 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
869 public = Pleroma.Constants.as_public()
870
871 case visibility do
872 "public" ->
873 to = [public | List.delete(to, follower_address)]
874 cc = [follower_address | List.delete(cc, public)]
875 recipients = [public | recipients]
876 [to, cc, recipients]
877
878 "private" ->
879 to = [follower_address | List.delete(to, public)]
880 cc = List.delete(cc, public)
881 recipients = List.delete(recipients, public)
882 [to, cc, recipients]
883
884 "unlisted" ->
885 to = [follower_address | List.delete(to, public)]
886 cc = [public | List.delete(cc, follower_address)]
887 recipients = recipients ++ [follower_address, public]
888 [to, cc, recipients]
889
890 _ ->
891 [to, cc, recipients]
892 end
893 end
894
895 def get_existing_votes(actor, %{data: %{"id" => id}}) do
896 actor
897 |> Activity.Queries.by_actor()
898 |> Activity.Queries.by_type("Create")
899 |> Activity.with_preloaded_object()
900 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
901 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
902 |> Repo.all()
903 end
904 end