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