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