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