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