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