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