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