Merge branch 'develop' into 'cleanup/masto_fe-default_settings'
[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.Maps
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.Repo
14 alias Pleroma.User
15 alias Pleroma.Web
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.AdminAPI.AccountView
19 alias Pleroma.Web.Endpoint
20 alias Pleroma.Web.Router.Helpers
21
22 import Ecto.Query
23
24 require Logger
25 require Pleroma.Constants
26
27 @supported_object_types [
28 "Article",
29 "Note",
30 "Event",
31 "Video",
32 "Page",
33 "Question",
34 "Answer",
35 "Audio"
36 ]
37 @strip_status_report_states ~w(closed resolved)
38 @supported_report_states ~w(open closed resolved)
39 @valid_visibilities ~w(public unlisted private direct)
40
41 # Some implementations send the actor URI as the actor field, others send the entire actor object,
42 # so figure out what the actor's URI is based on what we have.
43 def get_ap_id(%{"id" => id} = _), do: id
44 def get_ap_id(id), do: id
45
46 def normalize_params(params) do
47 Map.put(params, "actor", get_ap_id(params["actor"]))
48 end
49
50 @spec determine_explicit_mentions(map()) :: [any]
51 def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
52 Enum.flat_map(tag, fn
53 %{"type" => "Mention", "href" => href} -> [href]
54 _ -> []
55 end)
56 end
57
58 def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
59 object
60 |> Map.put("tag", [tag])
61 |> determine_explicit_mentions()
62 end
63
64 def determine_explicit_mentions(_), do: []
65
66 @spec label_in_collection?(any(), any()) :: boolean()
67 defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
68 defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
69 defp label_in_collection?(_, _), do: false
70
71 @spec label_in_message?(String.t(), map()) :: boolean()
72 def label_in_message?(label, params),
73 do:
74 [params["to"], params["cc"], params["bto"], params["bcc"]]
75 |> Enum.any?(&label_in_collection?(label, &1))
76
77 @spec unaddressed_message?(map()) :: boolean()
78 def unaddressed_message?(params),
79 do:
80 [params["to"], params["cc"], params["bto"], params["bcc"]]
81 |> Enum.all?(&is_nil(&1))
82
83 @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
84 def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
85 do:
86 label_in_message?(ap_id, params) || unaddressed_message?(params) ||
87 User.following?(recipient, actor)
88
89 defp extract_list(target) when is_binary(target), do: [target]
90 defp extract_list(lst) when is_list(lst), do: lst
91 defp extract_list(_), do: []
92
93 def maybe_splice_recipient(ap_id, params) do
94 need_splice? =
95 !label_in_collection?(ap_id, params["to"]) &&
96 !label_in_collection?(ap_id, params["cc"])
97
98 if need_splice? do
99 cc_list = extract_list(params["cc"])
100 Map.put(params, "cc", [ap_id | cc_list])
101 else
102 params
103 end
104 end
105
106 def make_json_ld_header do
107 %{
108 "@context" => [
109 "https://www.w3.org/ns/activitystreams",
110 "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
111 %{
112 "@language" => "und"
113 }
114 ]
115 }
116 end
117
118 def make_date do
119 DateTime.utc_now() |> DateTime.to_iso8601()
120 end
121
122 def generate_activity_id do
123 generate_id("activities")
124 end
125
126 def generate_context_id do
127 generate_id("contexts")
128 end
129
130 def generate_object_id do
131 Helpers.o_status_url(Endpoint, :object, UUID.generate())
132 end
133
134 def generate_id(type) do
135 "#{Web.base_url()}/#{type}/#{UUID.generate()}"
136 end
137
138 def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
139 fake_create_activity = %{
140 "to" => object["to"],
141 "cc" => object["cc"],
142 "type" => "Create",
143 "object" => object
144 }
145
146 get_notified_from_object(fake_create_activity)
147 end
148
149 def get_notified_from_object(object) do
150 Notification.get_notified_from_activity(%Activity{data: object}, false)
151 end
152
153 def create_context(context) do
154 context = context || generate_id("contexts")
155
156 # Ecto has problems accessing the constraint inside the jsonb,
157 # so we explicitly check for the existed object before insert
158 object = Object.get_cached_by_ap_id(context)
159
160 with true <- is_nil(object),
161 changeset <- Object.context_mapping(context),
162 {:ok, inserted_object} <- Repo.insert(changeset) do
163 inserted_object
164 else
165 _ ->
166 object
167 end
168 end
169
170 @doc """
171 Enqueues an activity for federation if it's local
172 """
173 @spec maybe_federate(any()) :: :ok
174 def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
175 outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
176
177 with true <- Config.get!([:instance, :federating]),
178 true <- type != "Block" || outgoing_blocks 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(act) when is_map(act) or is_binary(act) do
713 id =
714 case act do
715 %Activity{} = act -> act.data["id"]
716 act when is_map(act) -> act["id"]
717 act when is_binary(act) -> act
718 end
719
720 case Activity.get_by_ap_id_with_object(id) do
721 %Activity{} = activity ->
722 activity_actor = User.get_by_ap_id(activity.object.data["actor"])
723
724 %{
725 "type" => "Note",
726 "id" => activity.data["id"],
727 "content" => activity.object.data["content"],
728 "published" => activity.object.data["published"],
729 "actor" =>
730 AccountView.render(
731 "show.json",
732 %{user: activity_actor, skip_visibility_check: true}
733 )
734 }
735
736 _ ->
737 %{"id" => id, "deleted" => true}
738 end
739 end
740
741 defp build_flag_object(_), do: []
742
743 #### Report-related helpers
744 def get_reports(params, page, page_size) do
745 params =
746 params
747 |> Map.put(:type, "Flag")
748 |> Map.put(:skip_preload, true)
749 |> Map.put(:preload_report_notes, true)
750 |> Map.put(:total, true)
751 |> Map.put(:limit, page_size)
752 |> Map.put(:offset, (page - 1) * page_size)
753
754 ActivityPub.fetch_activities([], params, :offset)
755 end
756
757 def update_report_state(%Activity{} = activity, state)
758 when state in @strip_status_report_states do
759 {:ok, stripped_activity} = strip_report_status_data(activity)
760
761 new_data =
762 activity.data
763 |> Map.put("state", state)
764 |> Map.put("object", stripped_activity.data["object"])
765
766 activity
767 |> Changeset.change(data: new_data)
768 |> Repo.update()
769 end
770
771 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
772 new_data = Map.put(activity.data, "state", state)
773
774 activity
775 |> Changeset.change(data: new_data)
776 |> Repo.update()
777 end
778
779 def update_report_state(activity_ids, state) when state in @supported_report_states do
780 activities_num = length(activity_ids)
781
782 from(a in Activity, where: a.id in ^activity_ids)
783 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
784 |> Repo.update_all([])
785 |> case do
786 {^activities_num, _} -> :ok
787 _ -> {:error, activity_ids}
788 end
789 end
790
791 def update_report_state(_, _), do: {:error, "Unsupported state"}
792
793 def strip_report_status_data(activity) do
794 [actor | reported_activities] = activity.data["object"]
795
796 stripped_activities =
797 Enum.map(reported_activities, fn
798 act when is_map(act) -> act["id"]
799 act when is_binary(act) -> act
800 end)
801
802 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
803
804 {:ok, %{activity | data: new_data}}
805 end
806
807 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
808 [to, cc, recipients] =
809 activity
810 |> get_updated_targets(visibility)
811 |> Enum.map(&Enum.uniq/1)
812
813 object_data =
814 activity.object.data
815 |> Map.put("to", to)
816 |> Map.put("cc", cc)
817
818 {:ok, object} =
819 activity.object
820 |> Object.change(%{data: object_data})
821 |> Object.update_and_set_cache()
822
823 activity_data =
824 activity.data
825 |> Map.put("to", to)
826 |> Map.put("cc", cc)
827
828 activity
829 |> Map.put(:object, object)
830 |> Activity.change(%{data: activity_data, recipients: recipients})
831 |> Repo.update()
832 end
833
834 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
835
836 defp get_updated_targets(
837 %Activity{data: %{"to" => to} = data, recipients: recipients},
838 visibility
839 ) do
840 cc = Map.get(data, "cc", [])
841 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
842 public = Pleroma.Constants.as_public()
843
844 case visibility do
845 "public" ->
846 to = [public | List.delete(to, follower_address)]
847 cc = [follower_address | List.delete(cc, public)]
848 recipients = [public | recipients]
849 [to, cc, recipients]
850
851 "private" ->
852 to = [follower_address | List.delete(to, public)]
853 cc = List.delete(cc, public)
854 recipients = List.delete(recipients, public)
855 [to, cc, recipients]
856
857 "unlisted" ->
858 to = [follower_address | List.delete(to, public)]
859 cc = [public | List.delete(cc, follower_address)]
860 recipients = recipients ++ [follower_address, public]
861 [to, cc, recipients]
862
863 _ ->
864 [to, cc, recipients]
865 end
866 end
867
868 def get_existing_votes(actor, %{data: %{"id" => id}}) do
869 actor
870 |> Activity.Queries.by_actor()
871 |> Activity.Queries.by_type("Create")
872 |> Activity.with_preloaded_object()
873 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
874 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
875 |> Repo.all()
876 end
877 end