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