Merge remote-tracking branch 'pleroma/develop' into gitignore-runtime-exs
[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
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 <- Visibility.is_local_public?(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(%Activity{data: %{"id" => id}, object: %{data: data}}) do
714 activity_actor = User.get_by_ap_id(data["actor"])
715
716 %{
717 "type" => "Note",
718 "id" => id,
719 "content" => data["content"],
720 "published" => data["published"],
721 "actor" =>
722 AccountView.render(
723 "show.json",
724 %{user: activity_actor, skip_visibility_check: true}
725 )
726 }
727 end
728
729 defp build_flag_object(act) when is_map(act) or is_binary(act) do
730 id =
731 case act do
732 %Activity{} = act -> act.data["id"]
733 act when is_map(act) -> act["id"]
734 act when is_binary(act) -> act
735 end
736
737 case Activity.get_by_ap_id_with_object(id) do
738 %Activity{} = activity ->
739 build_flag_object(activity)
740
741 nil ->
742 if activity = Activity.get_by_object_ap_id_with_object(id) do
743 build_flag_object(activity)
744 else
745 %{"id" => id, "deleted" => true}
746 end
747 end
748 end
749
750 defp build_flag_object(_), do: []
751
752 #### Report-related helpers
753 def get_reports(params, page, page_size) do
754 params =
755 params
756 |> Map.put(:type, "Flag")
757 |> Map.put(:skip_preload, true)
758 |> Map.put(:preload_report_notes, true)
759 |> Map.put(:total, true)
760 |> Map.put(:limit, page_size)
761 |> Map.put(:offset, (page - 1) * page_size)
762
763 ActivityPub.fetch_activities([], params, :offset)
764 end
765
766 def update_report_state(%Activity{} = activity, state)
767 when state in @strip_status_report_states do
768 {:ok, stripped_activity} = strip_report_status_data(activity)
769
770 new_data =
771 activity.data
772 |> Map.put("state", state)
773 |> Map.put("object", stripped_activity.data["object"])
774
775 activity
776 |> Changeset.change(data: new_data)
777 |> Repo.update()
778 end
779
780 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
781 new_data = Map.put(activity.data, "state", state)
782
783 activity
784 |> Changeset.change(data: new_data)
785 |> Repo.update()
786 end
787
788 def update_report_state(activity_ids, state) when state in @supported_report_states do
789 activities_num = length(activity_ids)
790
791 from(a in Activity, where: a.id in ^activity_ids)
792 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
793 |> Repo.update_all([])
794 |> case do
795 {^activities_num, _} -> :ok
796 _ -> {:error, activity_ids}
797 end
798 end
799
800 def update_report_state(_, _), do: {:error, "Unsupported state"}
801
802 def strip_report_status_data(activity) do
803 [actor | reported_activities] = activity.data["object"]
804
805 stripped_activities =
806 Enum.map(reported_activities, fn
807 act when is_map(act) -> act["id"]
808 act when is_binary(act) -> act
809 end)
810
811 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
812
813 {:ok, %{activity | data: new_data}}
814 end
815
816 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
817 [to, cc, recipients] =
818 activity
819 |> get_updated_targets(visibility)
820 |> Enum.map(&Enum.uniq/1)
821
822 object_data =
823 activity.object.data
824 |> Map.put("to", to)
825 |> Map.put("cc", cc)
826
827 {:ok, object} =
828 activity.object
829 |> Object.change(%{data: object_data})
830 |> Object.update_and_set_cache()
831
832 activity_data =
833 activity.data
834 |> Map.put("to", to)
835 |> Map.put("cc", cc)
836
837 activity
838 |> Map.put(:object, object)
839 |> Activity.change(%{data: activity_data, recipients: recipients})
840 |> Repo.update()
841 end
842
843 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
844
845 defp get_updated_targets(
846 %Activity{data: %{"to" => to} = data, recipients: recipients},
847 visibility
848 ) do
849 cc = Map.get(data, "cc", [])
850 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
851 public = Pleroma.Constants.as_public()
852
853 case visibility do
854 "public" ->
855 to = [public | List.delete(to, follower_address)]
856 cc = [follower_address | List.delete(cc, public)]
857 recipients = [public | recipients]
858 [to, cc, recipients]
859
860 "private" ->
861 to = [follower_address | List.delete(to, public)]
862 cc = List.delete(cc, public)
863 recipients = List.delete(recipients, public)
864 [to, cc, recipients]
865
866 "unlisted" ->
867 to = [follower_address | List.delete(to, public)]
868 cc = [public | List.delete(cc, follower_address)]
869 recipients = recipients ++ [follower_address, public]
870 [to, cc, recipients]
871
872 _ ->
873 [to, cc, recipients]
874 end
875 end
876
877 def get_existing_votes(actor, %{data: %{"id" => id}}) do
878 actor
879 |> Activity.Queries.by_actor()
880 |> Activity.Queries.by_type("Create")
881 |> Activity.with_preloaded_object()
882 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
883 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
884 |> Repo.all()
885 end
886 end