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