Deprecate Pleroma.Web.base_url/0
[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 # 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 "#{Endpoint.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 "#{Endpoint.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,
178 false <- Visibility.is_local_public?(activity) do
179 Pleroma.Web.Federator.publish(activity)
180 end
181
182 :ok
183 end
184
185 def maybe_federate(_), do: :ok
186
187 @doc """
188 Adds an id and a published data if they aren't there,
189 also adds it to an included object
190 """
191 @spec lazy_put_activity_defaults(map(), boolean) :: map()
192 def lazy_put_activity_defaults(map, fake? \\ false)
193
194 def lazy_put_activity_defaults(map, true) do
195 map
196 |> Map.put_new("id", "pleroma:fakeid")
197 |> Map.put_new_lazy("published", &make_date/0)
198 |> Map.put_new("context", "pleroma:fakecontext")
199 |> Map.put_new("context_id", -1)
200 |> lazy_put_object_defaults(true)
201 end
202
203 def lazy_put_activity_defaults(map, _fake?) do
204 %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
205
206 map
207 |> Map.put_new_lazy("id", &generate_activity_id/0)
208 |> Map.put_new_lazy("published", &make_date/0)
209 |> Map.put_new("context", context)
210 |> Map.put_new("context_id", context_id)
211 |> lazy_put_object_defaults(false)
212 end
213
214 # Adds an id and published date if they aren't there.
215 #
216 @spec lazy_put_object_defaults(map(), boolean()) :: map()
217 defp lazy_put_object_defaults(%{"object" => map} = activity, true)
218 when is_map(map) do
219 object =
220 map
221 |> Map.put_new("id", "pleroma:fake_object_id")
222 |> Map.put_new_lazy("published", &make_date/0)
223 |> Map.put_new("context", activity["context"])
224 |> Map.put_new("context_id", activity["context_id"])
225 |> Map.put_new("fake", true)
226
227 %{activity | "object" => object}
228 end
229
230 defp lazy_put_object_defaults(%{"object" => map} = activity, _)
231 when is_map(map) do
232 object =
233 map
234 |> Map.put_new_lazy("id", &generate_object_id/0)
235 |> Map.put_new_lazy("published", &make_date/0)
236 |> Map.put_new("context", activity["context"])
237 |> Map.put_new("context_id", activity["context_id"])
238
239 %{activity | "object" => object}
240 end
241
242 defp lazy_put_object_defaults(activity, _), do: activity
243
244 @doc """
245 Inserts a full object if it is contained in an activity.
246 """
247 def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
248 when type in @supported_object_types do
249 with {:ok, object} <- Object.create(object_data) do
250 map = Map.put(map, "object", object.data["id"])
251
252 {:ok, map, object}
253 end
254 end
255
256 def insert_full_object(map), do: {:ok, map, nil}
257
258 #### Like-related helpers
259
260 @doc """
261 Returns an existing like if a user already liked an object
262 """
263 @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
264 def get_existing_like(actor, %{data: %{"id" => id}}) do
265 actor
266 |> Activity.Queries.by_actor()
267 |> Activity.Queries.by_object_id(id)
268 |> Activity.Queries.by_type("Like")
269 |> limit(1)
270 |> Repo.one()
271 end
272
273 @doc """
274 Returns like activities targeting an object
275 """
276 def get_object_likes(%{data: %{"id" => id}}) do
277 id
278 |> Activity.Queries.by_object_id()
279 |> Activity.Queries.by_type("Like")
280 |> Repo.all()
281 end
282
283 @spec make_like_data(User.t(), map(), String.t()) :: map()
284 def make_like_data(
285 %User{ap_id: ap_id} = actor,
286 %{data: %{"actor" => object_actor_id, "id" => id}} = object,
287 activity_id
288 ) do
289 object_actor = User.get_cached_by_ap_id(object_actor_id)
290
291 to =
292 if Visibility.is_public?(object) do
293 [actor.follower_address, object.data["actor"]]
294 else
295 [object.data["actor"]]
296 end
297
298 cc =
299 (object.data["to"] ++ (object.data["cc"] || []))
300 |> List.delete(actor.ap_id)
301 |> List.delete(object_actor.follower_address)
302
303 %{
304 "type" => "Like",
305 "actor" => ap_id,
306 "object" => id,
307 "to" => to,
308 "cc" => cc,
309 "context" => object.data["context"]
310 }
311 |> Maps.put_if_present("id", activity_id)
312 end
313
314 def make_emoji_reaction_data(user, object, emoji, activity_id) do
315 make_like_data(user, object, activity_id)
316 |> Map.put("type", "EmojiReact")
317 |> Map.put("content", emoji)
318 end
319
320 @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
321 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
322 def update_element_in_object(property, element, object, count \\ nil) do
323 length =
324 count ||
325 length(element)
326
327 data =
328 Map.merge(
329 object.data,
330 %{"#{property}_count" => length, "#{property}s" => element}
331 )
332
333 object
334 |> Changeset.change(data: data)
335 |> Object.update_and_set_cache()
336 end
337
338 @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
339 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
340
341 def add_emoji_reaction_to_object(
342 %Activity{data: %{"content" => emoji, "actor" => actor}},
343 object
344 ) do
345 reactions = get_cached_emoji_reactions(object)
346
347 new_reactions =
348 case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
349 nil ->
350 reactions ++ [[emoji, [actor]]]
351
352 index ->
353 List.update_at(
354 reactions,
355 index,
356 fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
357 )
358 end
359
360 count = emoji_count(new_reactions)
361
362 update_element_in_object("reaction", new_reactions, object, count)
363 end
364
365 def emoji_count(reactions_list) do
366 Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
367 end
368
369 def remove_emoji_reaction_from_object(
370 %Activity{data: %{"content" => emoji, "actor" => actor}},
371 object
372 ) do
373 reactions = get_cached_emoji_reactions(object)
374
375 new_reactions =
376 case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
377 nil ->
378 reactions
379
380 index ->
381 List.update_at(
382 reactions,
383 index,
384 fn [emoji, users] -> [emoji, List.delete(users, actor)] end
385 )
386 |> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
387 end
388
389 count = emoji_count(new_reactions)
390 update_element_in_object("reaction", new_reactions, object, count)
391 end
392
393 def get_cached_emoji_reactions(object) do
394 if is_list(object.data["reactions"]) do
395 object.data["reactions"]
396 else
397 []
398 end
399 end
400
401 @spec add_like_to_object(Activity.t(), Object.t()) ::
402 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
403 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
404 [actor | fetch_likes(object)]
405 |> Enum.uniq()
406 |> update_likes_in_object(object)
407 end
408
409 @spec remove_like_from_object(Activity.t(), Object.t()) ::
410 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
411 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
412 object
413 |> fetch_likes()
414 |> List.delete(actor)
415 |> update_likes_in_object(object)
416 end
417
418 defp update_likes_in_object(likes, object) do
419 update_element_in_object("like", likes, object)
420 end
421
422 defp fetch_likes(object) do
423 if is_list(object.data["likes"]) do
424 object.data["likes"]
425 else
426 []
427 end
428 end
429
430 #### Follow-related helpers
431
432 @doc """
433 Updates a follow activity's state (for locked accounts).
434 """
435 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
436 def update_follow_state_for_all(
437 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
438 state
439 ) do
440 "Follow"
441 |> Activity.Queries.by_type()
442 |> Activity.Queries.by_actor(actor)
443 |> Activity.Queries.by_object_id(object)
444 |> where(fragment("data->>'state' = 'pending'"))
445 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
446 |> Repo.update_all([])
447
448 activity = Activity.get_by_id(activity.id)
449
450 {:ok, activity}
451 end
452
453 def update_follow_state(
454 %Activity{} = activity,
455 state
456 ) do
457 new_data = Map.put(activity.data, "state", state)
458 changeset = Changeset.change(activity, data: new_data)
459
460 with {:ok, activity} <- Repo.update(changeset) do
461 {:ok, activity}
462 end
463 end
464
465 @doc """
466 Makes a follow activity data for the given follower and followed
467 """
468 def make_follow_data(
469 %User{ap_id: follower_id},
470 %User{ap_id: followed_id} = _followed,
471 activity_id
472 ) do
473 %{
474 "type" => "Follow",
475 "actor" => follower_id,
476 "to" => [followed_id],
477 "cc" => [Pleroma.Constants.as_public()],
478 "object" => followed_id,
479 "state" => "pending"
480 }
481 |> Maps.put_if_present("id", activity_id)
482 end
483
484 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
485 "Follow"
486 |> Activity.Queries.by_type()
487 |> where(actor: ^follower_id)
488 # this is to use the index
489 |> Activity.Queries.by_object_id(followed_id)
490 |> order_by([activity], fragment("? desc nulls last", activity.id))
491 |> limit(1)
492 |> Repo.one()
493 end
494
495 def fetch_latest_undo(%User{ap_id: ap_id}) do
496 "Undo"
497 |> Activity.Queries.by_type()
498 |> where(actor: ^ap_id)
499 |> order_by([activity], fragment("? desc nulls last", activity.id))
500 |> limit(1)
501 |> Repo.one()
502 end
503
504 def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
505 %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
506
507 "EmojiReact"
508 |> Activity.Queries.by_type()
509 |> where(actor: ^ap_id)
510 |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
511 |> Activity.Queries.by_object_id(object_ap_id)
512 |> order_by([activity], fragment("? desc nulls last", activity.id))
513 |> limit(1)
514 |> Repo.one()
515 end
516
517 #### Announce-related helpers
518
519 @doc """
520 Returns an existing announce activity if the notice has already been announced
521 """
522 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
523 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
524 "Announce"
525 |> Activity.Queries.by_type()
526 |> where(actor: ^actor)
527 # this is to use the index
528 |> Activity.Queries.by_object_id(ap_id)
529 |> Repo.one()
530 end
531
532 @doc """
533 Make announce activity data for the given actor and object
534 """
535 # for relayed messages, we only want to send to subscribers
536 def make_announce_data(
537 %User{ap_id: ap_id} = user,
538 %Object{data: %{"id" => id}} = object,
539 activity_id,
540 false
541 ) do
542 %{
543 "type" => "Announce",
544 "actor" => ap_id,
545 "object" => id,
546 "to" => [user.follower_address],
547 "cc" => [],
548 "context" => object.data["context"]
549 }
550 |> Maps.put_if_present("id", activity_id)
551 end
552
553 def make_announce_data(
554 %User{ap_id: ap_id} = user,
555 %Object{data: %{"id" => id}} = object,
556 activity_id,
557 true
558 ) do
559 %{
560 "type" => "Announce",
561 "actor" => ap_id,
562 "object" => id,
563 "to" => [user.follower_address, object.data["actor"]],
564 "cc" => [Pleroma.Constants.as_public()],
565 "context" => object.data["context"]
566 }
567 |> Maps.put_if_present("id", activity_id)
568 end
569
570 def make_undo_data(
571 %User{ap_id: actor, follower_address: follower_address},
572 %Activity{
573 data: %{"id" => undone_activity_id, "context" => context},
574 actor: undone_activity_actor
575 },
576 activity_id \\ nil
577 ) do
578 %{
579 "type" => "Undo",
580 "actor" => actor,
581 "object" => undone_activity_id,
582 "to" => [follower_address, undone_activity_actor],
583 "cc" => [Pleroma.Constants.as_public()],
584 "context" => context
585 }
586 |> Maps.put_if_present("id", activity_id)
587 end
588
589 @spec add_announce_to_object(Activity.t(), Object.t()) ::
590 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
591 def add_announce_to_object(
592 %Activity{data: %{"actor" => actor}},
593 object
594 ) do
595 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
596 announcements = take_announcements(object)
597
598 with announcements <- Enum.uniq([actor | announcements]) do
599 update_element_in_object("announcement", announcements, object)
600 end
601 else
602 {:ok, object}
603 end
604 end
605
606 def add_announce_to_object(_, object), do: {:ok, object}
607
608 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
609 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
610 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
611 with announcements <- List.delete(take_announcements(object), actor) do
612 update_element_in_object("announcement", announcements, object)
613 end
614 end
615
616 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
617 when is_list(announcements),
618 do: announcements
619
620 defp take_announcements(_), do: []
621
622 #### Unfollow-related helpers
623
624 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
625 %{
626 "type" => "Undo",
627 "actor" => follower.ap_id,
628 "to" => [followed.ap_id],
629 "object" => follow_activity.data
630 }
631 |> Maps.put_if_present("id", activity_id)
632 end
633
634 #### Block-related helpers
635 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
636 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
637 "Block"
638 |> Activity.Queries.by_type()
639 |> where(actor: ^blocker_id)
640 # this is to use the index
641 |> Activity.Queries.by_object_id(blocked_id)
642 |> order_by([activity], fragment("? desc nulls last", activity.id))
643 |> limit(1)
644 |> Repo.one()
645 end
646
647 def make_block_data(blocker, blocked, activity_id) do
648 %{
649 "type" => "Block",
650 "actor" => blocker.ap_id,
651 "to" => [blocked.ap_id],
652 "object" => blocked.ap_id
653 }
654 |> Maps.put_if_present("id", activity_id)
655 end
656
657 #### Create-related helpers
658
659 def make_create_data(params, additional) do
660 published = params.published || make_date()
661
662 %{
663 "type" => "Create",
664 "to" => params.to |> Enum.uniq(),
665 "actor" => params.actor.ap_id,
666 "object" => params.object,
667 "published" => published,
668 "context" => params.context
669 }
670 |> Map.merge(additional)
671 end
672
673 #### Listen-related helpers
674 def make_listen_data(params, additional) do
675 published = params.published || make_date()
676
677 %{
678 "type" => "Listen",
679 "to" => params.to |> Enum.uniq(),
680 "actor" => params.actor.ap_id,
681 "object" => params.object,
682 "published" => published,
683 "context" => params.context
684 }
685 |> Map.merge(additional)
686 end
687
688 #### Flag-related helpers
689 @spec make_flag_data(map(), map()) :: map()
690 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
691 %{
692 "type" => "Flag",
693 "actor" => actor.ap_id,
694 "content" => content,
695 "object" => build_flag_object(params),
696 "context" => context,
697 "state" => "open"
698 }
699 |> Map.merge(additional)
700 end
701
702 def make_flag_data(_, _), do: %{}
703
704 defp build_flag_object(%{account: account, statuses: statuses}) do
705 [account.ap_id | build_flag_object(%{statuses: statuses})]
706 end
707
708 defp build_flag_object(%{statuses: statuses}) do
709 Enum.map(statuses || [], &build_flag_object/1)
710 end
711
712 defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do
713 activity_actor = User.get_by_ap_id(data["actor"])
714
715 %{
716 "type" => "Note",
717 "id" => id,
718 "content" => data["content"],
719 "published" => data["published"],
720 "actor" =>
721 AccountView.render(
722 "show.json",
723 %{user: activity_actor, skip_visibility_check: true}
724 )
725 }
726 end
727
728 defp build_flag_object(act) when is_map(act) or is_binary(act) do
729 id =
730 case act do
731 %Activity{} = act -> act.data["id"]
732 act when is_map(act) -> act["id"]
733 act when is_binary(act) -> act
734 end
735
736 case Activity.get_by_ap_id_with_object(id) do
737 %Activity{} = activity ->
738 build_flag_object(activity)
739
740 nil ->
741 if activity = Activity.get_by_object_ap_id_with_object(id) do
742 build_flag_object(activity)
743 else
744 %{"id" => id, "deleted" => true}
745 end
746 end
747 end
748
749 defp build_flag_object(_), do: []
750
751 #### Report-related helpers
752 def get_reports(params, page, page_size) do
753 params =
754 params
755 |> Map.put(:type, "Flag")
756 |> Map.put(:skip_preload, true)
757 |> Map.put(:preload_report_notes, true)
758 |> Map.put(:total, true)
759 |> Map.put(:limit, page_size)
760 |> Map.put(:offset, (page - 1) * page_size)
761
762 ActivityPub.fetch_activities([], params, :offset)
763 end
764
765 def update_report_state(%Activity{} = activity, state)
766 when state in @strip_status_report_states do
767 {:ok, stripped_activity} = strip_report_status_data(activity)
768
769 new_data =
770 activity.data
771 |> Map.put("state", state)
772 |> Map.put("object", stripped_activity.data["object"])
773
774 activity
775 |> Changeset.change(data: new_data)
776 |> Repo.update()
777 end
778
779 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
780 new_data = Map.put(activity.data, "state", state)
781
782 activity
783 |> Changeset.change(data: new_data)
784 |> Repo.update()
785 end
786
787 def update_report_state(activity_ids, state) when state in @supported_report_states do
788 activities_num = length(activity_ids)
789
790 from(a in Activity, where: a.id in ^activity_ids)
791 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
792 |> Repo.update_all([])
793 |> case do
794 {^activities_num, _} -> :ok
795 _ -> {:error, activity_ids}
796 end
797 end
798
799 def update_report_state(_, _), do: {:error, "Unsupported state"}
800
801 def strip_report_status_data(activity) do
802 [actor | reported_activities] = activity.data["object"]
803
804 stripped_activities =
805 Enum.map(reported_activities, fn
806 act when is_map(act) -> act["id"]
807 act when is_binary(act) -> act
808 end)
809
810 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
811
812 {:ok, %{activity | data: new_data}}
813 end
814
815 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
816 [to, cc, recipients] =
817 activity
818 |> get_updated_targets(visibility)
819 |> Enum.map(&Enum.uniq/1)
820
821 object_data =
822 activity.object.data
823 |> Map.put("to", to)
824 |> Map.put("cc", cc)
825
826 {:ok, object} =
827 activity.object
828 |> Object.change(%{data: object_data})
829 |> Object.update_and_set_cache()
830
831 activity_data =
832 activity.data
833 |> Map.put("to", to)
834 |> Map.put("cc", cc)
835
836 activity
837 |> Map.put(:object, object)
838 |> Activity.change(%{data: activity_data, recipients: recipients})
839 |> Repo.update()
840 end
841
842 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
843
844 defp get_updated_targets(
845 %Activity{data: %{"to" => to} = data, recipients: recipients},
846 visibility
847 ) do
848 cc = Map.get(data, "cc", [])
849 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
850 public = Pleroma.Constants.as_public()
851
852 case visibility do
853 "public" ->
854 to = [public | List.delete(to, follower_address)]
855 cc = [follower_address | List.delete(cc, public)]
856 recipients = [public | recipients]
857 [to, cc, recipients]
858
859 "private" ->
860 to = [follower_address | List.delete(to, public)]
861 cc = List.delete(cc, public)
862 recipients = List.delete(recipients, public)
863 [to, cc, recipients]
864
865 "unlisted" ->
866 to = [follower_address | List.delete(to, public)]
867 cc = [public | List.delete(cc, follower_address)]
868 recipients = recipients ++ [follower_address, public]
869 [to, cc, recipients]
870
871 _ ->
872 [to, cc, recipients]
873 end
874 end
875
876 def get_existing_votes(actor, %{data: %{"id" => id}}) do
877 actor
878 |> Activity.Queries.by_actor()
879 |> Activity.Queries.by_type("Create")
880 |> Activity.with_preloaded_object()
881 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
882 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
883 |> Repo.all()
884 end
885 end