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