40a505b913c0c59c379d7e929ac35b29c0a4bbc0
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
6 @moduledoc """
7 A module to handle coding from internal to wire ActivityPub and back.
8 """
9 alias Pleroma.Activity
10 alias Pleroma.EarmarkRenderer
11 alias Pleroma.FollowingRelationship
12 alias Pleroma.Object
13 alias Pleroma.Object.Containment
14 alias Pleroma.Repo
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.ObjectValidator
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.Federator
22 alias Pleroma.Workers.TransmogrifierWorker
23
24 import Ecto.Query
25
26 require Logger
27 require Pleroma.Constants
28
29 @doc """
30 Modifies an incoming AP object (mastodon format) to our internal format.
31 """
32 def fix_object(object, options \\ []) do
33 object
34 |> strip_internal_fields
35 |> fix_actor
36 |> fix_url
37 |> fix_attachments
38 |> fix_context
39 |> fix_in_reply_to(options)
40 |> fix_emoji
41 |> fix_tag
42 |> fix_content_map
43 |> fix_addressing
44 |> fix_summary
45 |> fix_type(options)
46 |> fix_content
47 end
48
49 def fix_summary(%{"summary" => nil} = object) do
50 Map.put(object, "summary", "")
51 end
52
53 def fix_summary(%{"summary" => _} = object) do
54 # summary is present, nothing to do
55 object
56 end
57
58 def fix_summary(object), do: Map.put(object, "summary", "")
59
60 def fix_addressing_list(map, field) do
61 cond do
62 is_binary(map[field]) ->
63 Map.put(map, field, [map[field]])
64
65 is_nil(map[field]) ->
66 Map.put(map, field, [])
67
68 true ->
69 map
70 end
71 end
72
73 def fix_explicit_addressing(
74 %{"to" => to, "cc" => cc} = object,
75 explicit_mentions,
76 follower_collection
77 ) do
78 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
79
80 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
81
82 final_cc =
83 (cc ++ explicit_cc)
84 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
85 |> Enum.uniq()
86
87 object
88 |> Map.put("to", explicit_to)
89 |> Map.put("cc", final_cc)
90 end
91
92 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
93
94 # if directMessage flag is set to true, leave the addressing alone
95 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
96
97 def fix_explicit_addressing(object) do
98 explicit_mentions = Utils.determine_explicit_mentions(object)
99
100 %User{follower_address: follower_collection} =
101 object
102 |> Containment.get_actor()
103 |> User.get_cached_by_ap_id()
104
105 explicit_mentions =
106 explicit_mentions ++
107 [
108 Pleroma.Constants.as_public(),
109 follower_collection
110 ]
111
112 fix_explicit_addressing(object, explicit_mentions, follower_collection)
113 end
114
115 # if as:Public is addressed, then make sure the followers collection is also addressed
116 # so that the activities will be delivered to local users.
117 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
118 recipients = to ++ cc
119
120 if followers_collection not in recipients do
121 cond do
122 Pleroma.Constants.as_public() in cc ->
123 to = to ++ [followers_collection]
124 Map.put(object, "to", to)
125
126 Pleroma.Constants.as_public() in to ->
127 cc = cc ++ [followers_collection]
128 Map.put(object, "cc", cc)
129
130 true ->
131 object
132 end
133 else
134 object
135 end
136 end
137
138 def fix_implicit_addressing(object, _), do: object
139
140 def fix_addressing(object) do
141 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
142 followers_collection = User.ap_followers(user)
143
144 object
145 |> fix_addressing_list("to")
146 |> fix_addressing_list("cc")
147 |> fix_addressing_list("bto")
148 |> fix_addressing_list("bcc")
149 |> fix_explicit_addressing()
150 |> fix_implicit_addressing(followers_collection)
151 end
152
153 def fix_actor(%{"attributedTo" => actor} = object) do
154 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
155 end
156
157 def fix_in_reply_to(object, options \\ [])
158
159 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
160 when not is_nil(in_reply_to) do
161 in_reply_to_id = prepare_in_reply_to(in_reply_to)
162 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
163 depth = (options[:depth] || 0) + 1
164
165 if Federator.allowed_thread_distance?(depth) do
166 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
167 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
168 object
169 |> Map.put("inReplyTo", replied_object.data["id"])
170 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
171 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
172 |> Map.put("context", replied_object.data["context"] || object["conversation"])
173 else
174 e ->
175 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
176 object
177 end
178 else
179 object
180 end
181 end
182
183 def fix_in_reply_to(object, _options), do: object
184
185 defp prepare_in_reply_to(in_reply_to) do
186 cond do
187 is_bitstring(in_reply_to) ->
188 in_reply_to
189
190 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
191 in_reply_to["id"]
192
193 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
194 Enum.at(in_reply_to, 0)
195
196 true ->
197 ""
198 end
199 end
200
201 def fix_context(object) do
202 context = object["context"] || object["conversation"] || Utils.generate_context_id()
203
204 object
205 |> Map.put("context", context)
206 |> Map.put("conversation", context)
207 end
208
209 defp add_if_present(map, _key, nil), do: map
210
211 defp add_if_present(map, key, value) do
212 Map.put(map, key, value)
213 end
214
215 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
216 attachments =
217 Enum.map(attachment, fn data ->
218 url =
219 cond do
220 is_list(data["url"]) -> List.first(data["url"])
221 is_map(data["url"]) -> data["url"]
222 true -> nil
223 end
224
225 media_type =
226 cond do
227 is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
228 is_binary(data["mediaType"]) -> data["mediaType"]
229 is_binary(data["mimeType"]) -> data["mimeType"]
230 true -> nil
231 end
232
233 href =
234 cond do
235 is_map(url) && is_binary(url["href"]) -> url["href"]
236 is_binary(data["url"]) -> data["url"]
237 is_binary(data["href"]) -> data["href"]
238 end
239
240 attachment_url =
241 %{"href" => href}
242 |> add_if_present("mediaType", media_type)
243 |> add_if_present("type", Map.get(url || %{}, "type"))
244
245 %{"url" => [attachment_url]}
246 |> add_if_present("mediaType", media_type)
247 |> add_if_present("type", data["type"])
248 |> add_if_present("name", data["name"])
249 end)
250
251 Map.put(object, "attachment", attachments)
252 end
253
254 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
255 object
256 |> Map.put("attachment", [attachment])
257 |> fix_attachments()
258 end
259
260 def fix_attachments(object), do: object
261
262 def fix_url(%{"url" => url} = object) when is_map(url) do
263 Map.put(object, "url", url["href"])
264 end
265
266 def fix_url(%{"type" => object_type, "url" => url} = object)
267 when object_type in ["Video", "Audio"] and is_list(url) do
268 first_element = Enum.at(url, 0)
269
270 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
271
272 object
273 |> Map.put("attachment", [first_element])
274 |> Map.put("url", link_element["href"])
275 end
276
277 def fix_url(%{"type" => object_type, "url" => url} = object)
278 when object_type != "Video" and is_list(url) do
279 first_element = Enum.at(url, 0)
280
281 url_string =
282 cond do
283 is_bitstring(first_element) -> first_element
284 is_map(first_element) -> first_element["href"] || ""
285 true -> ""
286 end
287
288 Map.put(object, "url", url_string)
289 end
290
291 def fix_url(object), do: object
292
293 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
294 emoji =
295 tags
296 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
297 |> Enum.reduce(%{}, fn data, mapping ->
298 name = String.trim(data["name"], ":")
299
300 Map.put(mapping, name, data["icon"]["url"])
301 end)
302
303 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
304 emoji = Map.merge(object["emoji"] || %{}, emoji)
305
306 Map.put(object, "emoji", emoji)
307 end
308
309 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
310 name = String.trim(tag["name"], ":")
311 emoji = %{name => tag["icon"]["url"]}
312
313 Map.put(object, "emoji", emoji)
314 end
315
316 def fix_emoji(object), do: object
317
318 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
319 tags =
320 tag
321 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
322 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
323
324 Map.put(object, "tag", tag ++ tags)
325 end
326
327 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
328 combined = [tag, String.slice(hashtag, 1..-1)]
329
330 Map.put(object, "tag", combined)
331 end
332
333 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
334
335 def fix_tag(object), do: object
336
337 # content map usually only has one language so this will do for now.
338 def fix_content_map(%{"contentMap" => content_map} = object) do
339 content_groups = Map.to_list(content_map)
340 {_, content} = Enum.at(content_groups, 0)
341
342 Map.put(object, "content", content)
343 end
344
345 def fix_content_map(object), do: object
346
347 def fix_type(object, options \\ [])
348
349 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
350 when is_binary(reply_id) do
351 with true <- Federator.allowed_thread_distance?(options[:depth]),
352 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
353 Map.put(object, "type", "Answer")
354 else
355 _ -> object
356 end
357 end
358
359 def fix_type(object, _), do: object
360
361 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
362 when is_binary(content) do
363 html_content =
364 content
365 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
366 |> Pleroma.HTML.filter_tags()
367
368 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
369 end
370
371 defp fix_content(object), do: object
372
373 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
374 with true <- id =~ "follows",
375 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
376 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
377 {:ok, activity}
378 else
379 _ -> {:error, nil}
380 end
381 end
382
383 defp mastodon_follow_hack(_, _), do: {:error, nil}
384
385 defp get_follow_activity(follow_object, followed) do
386 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
387 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
388 {:ok, activity}
389 else
390 # Can't find the activity. This might a Mastodon 2.3 "Accept"
391 {:activity, nil} ->
392 mastodon_follow_hack(follow_object, followed)
393
394 _ ->
395 {:error, nil}
396 end
397 end
398
399 # Reduce the object list to find the reported user.
400 defp get_reported(objects) do
401 Enum.reduce_while(objects, nil, fn ap_id, _ ->
402 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
403 {:halt, user}
404 else
405 _ -> {:cont, nil}
406 end
407 end)
408 end
409
410 def handle_incoming(data, options \\ [])
411
412 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
413 # with nil ID.
414 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
415 with context <- data["context"] || Utils.generate_context_id(),
416 content <- data["content"] || "",
417 %User{} = actor <- User.get_cached_by_ap_id(actor),
418 # Reduce the object list to find the reported user.
419 %User{} = account <- get_reported(objects),
420 # Remove the reported user from the object list.
421 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
422 %{
423 actor: actor,
424 context: context,
425 account: account,
426 statuses: statuses,
427 content: content,
428 additional: %{"cc" => [account.ap_id]}
429 }
430 |> ActivityPub.flag()
431 end
432 end
433
434 # disallow objects with bogus IDs
435 def handle_incoming(%{"id" => nil}, _options), do: :error
436 def handle_incoming(%{"id" => ""}, _options), do: :error
437 # length of https:// = 8, should validate better, but good enough for now.
438 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
439 do: :error
440
441 # TODO: validate those with a Ecto scheme
442 # - tags
443 # - emoji
444 def handle_incoming(
445 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
446 options
447 )
448 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
449 actor = Containment.get_actor(data)
450
451 data =
452 Map.put(data, "actor", actor)
453 |> fix_addressing
454
455 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
456 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
457 object = fix_object(object, options)
458
459 params = %{
460 to: data["to"],
461 object: object,
462 actor: user,
463 context: object["conversation"],
464 local: false,
465 published: data["published"],
466 additional:
467 Map.take(data, [
468 "cc",
469 "directMessage",
470 "id"
471 ])
472 }
473
474 with {:ok, created_activity} <- ActivityPub.create(params) do
475 reply_depth = (options[:depth] || 0) + 1
476
477 if Federator.allowed_thread_distance?(reply_depth) do
478 for reply_id <- replies(object) do
479 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
480 "id" => reply_id,
481 "depth" => reply_depth
482 })
483 end
484 end
485
486 {:ok, created_activity}
487 end
488 else
489 %Activity{} = activity -> {:ok, activity}
490 _e -> :error
491 end
492 end
493
494 def handle_incoming(
495 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
496 options
497 ) do
498 actor = Containment.get_actor(data)
499
500 data =
501 Map.put(data, "actor", actor)
502 |> fix_addressing
503
504 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
505 reply_depth = (options[:depth] || 0) + 1
506 options = Keyword.put(options, :depth, reply_depth)
507 object = fix_object(object, options)
508
509 params = %{
510 to: data["to"],
511 object: object,
512 actor: user,
513 context: nil,
514 local: false,
515 published: data["published"],
516 additional: Map.take(data, ["cc", "id"])
517 }
518
519 ActivityPub.listen(params)
520 else
521 _e -> :error
522 end
523 end
524
525 def handle_incoming(
526 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
527 _options
528 ) do
529 with %User{local: true} = followed <-
530 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
531 {:ok, %User{} = follower} <-
532 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
533 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
534 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
535 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
536 {_, false} <- {:user_locked, User.locked?(followed)},
537 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
538 {_, {:ok, _}} <-
539 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
540 {:ok, _relationship} <-
541 FollowingRelationship.update(follower, followed, :follow_accept) do
542 ActivityPub.accept(%{
543 to: [follower.ap_id],
544 actor: followed,
545 object: data,
546 local: true
547 })
548 else
549 {:user_blocked, true} ->
550 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
551 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
552
553 ActivityPub.reject(%{
554 to: [follower.ap_id],
555 actor: followed,
556 object: data,
557 local: true
558 })
559
560 {:follow, {:error, _}} ->
561 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
562 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
563
564 ActivityPub.reject(%{
565 to: [follower.ap_id],
566 actor: followed,
567 object: data,
568 local: true
569 })
570
571 {:user_locked, true} ->
572 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
573 :noop
574 end
575
576 {:ok, activity}
577 else
578 _e ->
579 :error
580 end
581 end
582
583 def handle_incoming(
584 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
585 _options
586 ) do
587 with actor <- Containment.get_actor(data),
588 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
589 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
590 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
591 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
592 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
593 ActivityPub.accept(%{
594 to: follow_activity.data["to"],
595 type: "Accept",
596 actor: followed,
597 object: follow_activity.data["id"],
598 local: false,
599 activity_id: id
600 })
601 else
602 _e -> :error
603 end
604 end
605
606 def handle_incoming(
607 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
608 _options
609 ) do
610 with actor <- Containment.get_actor(data),
611 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
612 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
613 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
614 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
615 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
616 {:ok, activity} <-
617 ActivityPub.reject(%{
618 to: follow_activity.data["to"],
619 type: "Reject",
620 actor: followed,
621 object: follow_activity.data["id"],
622 local: false,
623 activity_id: id
624 }) do
625 {:ok, activity}
626 else
627 _e -> :error
628 end
629 end
630
631 @misskey_reactions %{
632 "like" => "👍",
633 "love" => "❤️",
634 "laugh" => "😆",
635 "hmm" => "🤔",
636 "surprise" => "😮",
637 "congrats" => "🎉",
638 "angry" => "💢",
639 "confused" => "😥",
640 "rip" => "😇",
641 "pudding" => "🍮",
642 "star" => "⭐"
643 }
644
645 @doc "Rewrite misskey likes into EmojiReacts"
646 def handle_incoming(
647 %{
648 "type" => "Like",
649 "_misskey_reaction" => reaction
650 } = data,
651 options
652 ) do
653 data
654 |> Map.put("type", "EmojiReact")
655 |> Map.put("content", @misskey_reactions[reaction] || reaction)
656 |> handle_incoming(options)
657 end
658
659 def handle_incoming(
660 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
661 _options
662 ) do
663 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
664 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
665 {:ok, activity}
666 end
667 end
668
669 def handle_incoming(%{"type" => "Like"} = data, _options) do
670 with :ok <- ObjectValidator.fetch_actor_and_object(data),
671 {:ok, activity, _meta} <-
672 Pipeline.common_pipeline(data, local: false) do
673 {:ok, activity}
674 else
675 e -> {:error, e}
676 end
677 end
678
679 def handle_incoming(
680 %{
681 "type" => "EmojiReact",
682 "object" => object_id,
683 "actor" => _actor,
684 "id" => id,
685 "content" => emoji
686 } = data,
687 _options
688 ) do
689 with actor <- Containment.get_actor(data),
690 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
691 {:ok, object} <- get_obj_helper(object_id),
692 {:ok, activity, _object} <-
693 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
694 {:ok, activity}
695 else
696 _e -> :error
697 end
698 end
699
700 def handle_incoming(
701 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
702 _options
703 ) do
704 with actor <- Containment.get_actor(data),
705 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
706 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
707 public <- Visibility.is_public?(data),
708 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
709 {:ok, activity}
710 else
711 _e -> :error
712 end
713 end
714
715 def handle_incoming(
716 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
717 data,
718 _options
719 )
720 when object_type in [
721 "Person",
722 "Application",
723 "Service",
724 "Organization"
725 ] do
726 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
727 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
728
729 actor
730 |> User.remote_user_changeset(new_user_data)
731 |> User.update_and_set_cache()
732
733 ActivityPub.update(%{
734 local: false,
735 to: data["to"] || [],
736 cc: data["cc"] || [],
737 object: object,
738 actor: actor_id,
739 activity_id: data["id"]
740 })
741 else
742 e ->
743 Logger.error(e)
744 :error
745 end
746 end
747
748 # TODO: We presently assume that any actor on the same origin domain as the object being
749 # deleted has the rights to delete that object. A better way to validate whether or not
750 # the object should be deleted is to refetch the object URI, which should return either
751 # an error or a tombstone. This would allow us to verify that a deletion actually took
752 # place.
753 def handle_incoming(
754 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
755 _options
756 ) do
757 object_id = Utils.get_ap_id(object_id)
758
759 with actor <- Containment.get_actor(data),
760 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
761 {:ok, object} <- get_obj_helper(object_id),
762 :ok <- Containment.contain_origin(actor.ap_id, object.data),
763 {:ok, activity} <-
764 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
765 {:ok, activity}
766 else
767 nil ->
768 case User.get_cached_by_ap_id(object_id) do
769 %User{ap_id: ^actor} = user ->
770 User.delete(user)
771
772 nil ->
773 :error
774 end
775
776 _e ->
777 :error
778 end
779 end
780
781 def handle_incoming(
782 %{
783 "type" => "Undo",
784 "object" => %{"type" => "Announce", "object" => object_id},
785 "actor" => _actor,
786 "id" => id
787 } = data,
788 _options
789 ) do
790 with actor <- Containment.get_actor(data),
791 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
792 {:ok, object} <- get_obj_helper(object_id),
793 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
794 {:ok, activity}
795 else
796 _e -> :error
797 end
798 end
799
800 def handle_incoming(
801 %{
802 "type" => "Undo",
803 "object" => %{"type" => "Follow", "object" => followed},
804 "actor" => follower,
805 "id" => id
806 } = _data,
807 _options
808 ) do
809 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
810 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
811 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
812 User.unfollow(follower, followed)
813 {:ok, activity}
814 else
815 _e -> :error
816 end
817 end
818
819 def handle_incoming(
820 %{
821 "type" => "Undo",
822 "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
823 "actor" => _actor,
824 "id" => id
825 } = data,
826 _options
827 ) do
828 with actor <- Containment.get_actor(data),
829 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
830 {:ok, activity, _} <-
831 ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
832 activity_id: id,
833 local: false
834 ) do
835 {:ok, activity}
836 else
837 _e -> :error
838 end
839 end
840
841 def handle_incoming(
842 %{
843 "type" => "Undo",
844 "object" => %{"type" => "Block", "object" => blocked},
845 "actor" => blocker,
846 "id" => id
847 } = _data,
848 _options
849 ) do
850 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
851 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
852 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
853 User.unblock(blocker, blocked)
854 {:ok, activity}
855 else
856 _e -> :error
857 end
858 end
859
860 def handle_incoming(
861 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
862 _options
863 ) do
864 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
865 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
866 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
867 User.unfollow(blocker, blocked)
868 User.block(blocker, blocked)
869 {:ok, activity}
870 else
871 _e -> :error
872 end
873 end
874
875 def handle_incoming(
876 %{
877 "type" => "Undo",
878 "object" => %{"type" => "Like", "object" => object_id},
879 "actor" => _actor,
880 "id" => id
881 } = data,
882 _options
883 ) do
884 with actor <- Containment.get_actor(data),
885 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
886 {:ok, object} <- get_obj_helper(object_id),
887 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
888 {:ok, activity}
889 else
890 _e -> :error
891 end
892 end
893
894 # For Undos that don't have the complete object attached, try to find it in our database.
895 def handle_incoming(
896 %{
897 "type" => "Undo",
898 "object" => object
899 } = activity,
900 options
901 )
902 when is_binary(object) do
903 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
904 activity
905 |> Map.put("object", data)
906 |> handle_incoming(options)
907 else
908 _e -> :error
909 end
910 end
911
912 def handle_incoming(
913 %{
914 "type" => "Move",
915 "actor" => origin_actor,
916 "object" => origin_actor,
917 "target" => target_actor
918 },
919 _options
920 ) do
921 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
922 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
923 true <- origin_actor in target_user.also_known_as do
924 ActivityPub.move(origin_user, target_user, false)
925 else
926 _e -> :error
927 end
928 end
929
930 def handle_incoming(_, _), do: :error
931
932 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
933 def get_obj_helper(id, options \\ []) do
934 case Object.normalize(id, true, options) do
935 %Object{} = object -> {:ok, object}
936 _ -> nil
937 end
938 end
939
940 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
941 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
942 ap_id: ap_id
943 })
944 when attributed_to == ap_id do
945 with {:ok, activity} <-
946 handle_incoming(%{
947 "type" => "Create",
948 "to" => data["to"],
949 "cc" => data["cc"],
950 "actor" => attributed_to,
951 "object" => data
952 }) do
953 {:ok, Object.normalize(activity)}
954 else
955 _ -> get_obj_helper(object_id)
956 end
957 end
958
959 def get_embedded_obj_helper(object_id, _) do
960 get_obj_helper(object_id)
961 end
962
963 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
964 with false <- String.starts_with?(in_reply_to, "http"),
965 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
966 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
967 else
968 _e -> object
969 end
970 end
971
972 def set_reply_to_uri(obj), do: obj
973
974 @doc """
975 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
976 Based on Mastodon's ActivityPub::NoteSerializer#replies.
977 """
978 def set_replies(obj_data) do
979 replies_uris =
980 with limit when limit > 0 <-
981 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
982 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
983 object
984 |> Object.self_replies()
985 |> select([o], fragment("?->>'id'", o.data))
986 |> limit(^limit)
987 |> Repo.all()
988 else
989 _ -> []
990 end
991
992 set_replies(obj_data, replies_uris)
993 end
994
995 defp set_replies(obj, []) do
996 obj
997 end
998
999 defp set_replies(obj, replies_uris) do
1000 replies_collection = %{
1001 "type" => "Collection",
1002 "items" => replies_uris
1003 }
1004
1005 Map.merge(obj, %{"replies" => replies_collection})
1006 end
1007
1008 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
1009 items
1010 end
1011
1012 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
1013 items
1014 end
1015
1016 def replies(_), do: []
1017
1018 # Prepares the object of an outgoing create activity.
1019 def prepare_object(object) do
1020 object
1021 |> set_sensitive
1022 |> add_hashtags
1023 |> add_mention_tags
1024 |> add_emoji_tags
1025 |> add_attributed_to
1026 |> prepare_attachments
1027 |> set_conversation
1028 |> set_reply_to_uri
1029 |> set_replies
1030 |> strip_internal_fields
1031 |> strip_internal_tags
1032 |> set_type
1033 end
1034
1035 # @doc
1036 # """
1037 # internal -> Mastodon
1038 # """
1039
1040 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
1041 when activity_type in ["Create", "Listen"] do
1042 object =
1043 object_id
1044 |> Object.normalize()
1045 |> Map.get(:data)
1046 |> prepare_object
1047
1048 data =
1049 data
1050 |> Map.put("object", object)
1051 |> Map.merge(Utils.make_json_ld_header())
1052 |> Map.delete("bcc")
1053
1054 {:ok, data}
1055 end
1056
1057 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
1058 object =
1059 object_id
1060 |> Object.normalize()
1061
1062 data =
1063 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
1064 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
1065 else
1066 data |> maybe_fix_object_url
1067 end
1068
1069 data =
1070 data
1071 |> strip_internal_fields
1072 |> Map.merge(Utils.make_json_ld_header())
1073 |> Map.delete("bcc")
1074
1075 {:ok, data}
1076 end
1077
1078 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
1079 # because of course it does.
1080 def prepare_outgoing(%{"type" => "Accept"} = data) do
1081 with follow_activity <- Activity.normalize(data["object"]) do
1082 object = %{
1083 "actor" => follow_activity.actor,
1084 "object" => follow_activity.data["object"],
1085 "id" => follow_activity.data["id"],
1086 "type" => "Follow"
1087 }
1088
1089 data =
1090 data
1091 |> Map.put("object", object)
1092 |> Map.merge(Utils.make_json_ld_header())
1093
1094 {:ok, data}
1095 end
1096 end
1097
1098 def prepare_outgoing(%{"type" => "Reject"} = data) do
1099 with follow_activity <- Activity.normalize(data["object"]) do
1100 object = %{
1101 "actor" => follow_activity.actor,
1102 "object" => follow_activity.data["object"],
1103 "id" => follow_activity.data["id"],
1104 "type" => "Follow"
1105 }
1106
1107 data =
1108 data
1109 |> Map.put("object", object)
1110 |> Map.merge(Utils.make_json_ld_header())
1111
1112 {:ok, data}
1113 end
1114 end
1115
1116 def prepare_outgoing(%{"type" => _type} = data) do
1117 data =
1118 data
1119 |> strip_internal_fields
1120 |> maybe_fix_object_url
1121 |> Map.merge(Utils.make_json_ld_header())
1122
1123 {:ok, data}
1124 end
1125
1126 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1127 with false <- String.starts_with?(object, "http"),
1128 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1129 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1130 relative_object do
1131 Map.put(data, "object", external_url)
1132 else
1133 {:fetch, e} ->
1134 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1135 data
1136
1137 _ ->
1138 data
1139 end
1140 end
1141
1142 def maybe_fix_object_url(data), do: data
1143
1144 def add_hashtags(object) do
1145 tags =
1146 (object["tag"] || [])
1147 |> Enum.map(fn
1148 # Expand internal representation tags into AS2 tags.
1149 tag when is_binary(tag) ->
1150 %{
1151 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1152 "name" => "##{tag}",
1153 "type" => "Hashtag"
1154 }
1155
1156 # Do not process tags which are already AS2 tag objects.
1157 tag when is_map(tag) ->
1158 tag
1159 end)
1160
1161 Map.put(object, "tag", tags)
1162 end
1163
1164 def add_mention_tags(object) do
1165 {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
1166 potential_receivers = enabled_receivers ++ disabled_receivers
1167 mentions = Enum.map(potential_receivers, &build_mention_tag/1)
1168
1169 tags = object["tag"] || []
1170 Map.put(object, "tag", tags ++ mentions)
1171 end
1172
1173 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1174 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1175 end
1176
1177 def take_emoji_tags(%User{emoji: emoji}) do
1178 emoji
1179 |> Map.to_list()
1180 |> Enum.map(&build_emoji_tag/1)
1181 end
1182
1183 # TODO: we should probably send mtime instead of unix epoch time for updated
1184 def add_emoji_tags(%{"emoji" => emoji} = object) do
1185 tags = object["tag"] || []
1186
1187 out = Enum.map(emoji, &build_emoji_tag/1)
1188
1189 Map.put(object, "tag", tags ++ out)
1190 end
1191
1192 def add_emoji_tags(object), do: object
1193
1194 defp build_emoji_tag({name, url}) do
1195 %{
1196 "icon" => %{"url" => url, "type" => "Image"},
1197 "name" => ":" <> name <> ":",
1198 "type" => "Emoji",
1199 "updated" => "1970-01-01T00:00:00Z",
1200 "id" => url
1201 }
1202 end
1203
1204 def set_conversation(object) do
1205 Map.put(object, "conversation", object["context"])
1206 end
1207
1208 def set_sensitive(%{"sensitive" => true} = object) do
1209 object
1210 end
1211
1212 def set_sensitive(object) do
1213 tags = object["tag"] || []
1214 Map.put(object, "sensitive", "nsfw" in tags)
1215 end
1216
1217 def set_type(%{"type" => "Answer"} = object) do
1218 Map.put(object, "type", "Note")
1219 end
1220
1221 def set_type(object), do: object
1222
1223 def add_attributed_to(object) do
1224 attributed_to = object["attributedTo"] || object["actor"]
1225 Map.put(object, "attributedTo", attributed_to)
1226 end
1227
1228 def prepare_attachments(object) do
1229 attachments =
1230 object
1231 |> Map.get("attachment", [])
1232 |> Enum.map(fn data ->
1233 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1234
1235 %{
1236 "url" => href,
1237 "mediaType" => media_type,
1238 "name" => data["name"],
1239 "type" => "Document"
1240 }
1241 end)
1242
1243 Map.put(object, "attachment", attachments)
1244 end
1245
1246 def strip_internal_fields(object) do
1247 Map.drop(object, Pleroma.Constants.object_internal_fields())
1248 end
1249
1250 defp strip_internal_tags(%{"tag" => tags} = object) do
1251 tags = Enum.filter(tags, fn x -> is_map(x) end)
1252
1253 Map.put(object, "tag", tags)
1254 end
1255
1256 defp strip_internal_tags(object), do: object
1257
1258 def perform(:user_upgrade, user) do
1259 # we pass a fake user so that the followers collection is stripped away
1260 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1261
1262 from(
1263 a in Activity,
1264 where: ^old_follower_address in a.recipients,
1265 update: [
1266 set: [
1267 recipients:
1268 fragment(
1269 "array_replace(?,?,?)",
1270 a.recipients,
1271 ^old_follower_address,
1272 ^user.follower_address
1273 )
1274 ]
1275 ]
1276 )
1277 |> Repo.update_all([])
1278 end
1279
1280 def upgrade_user_from_ap_id(ap_id) do
1281 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1282 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1283 {:ok, user} <- update_user(user, data) do
1284 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1285 {:ok, user}
1286 else
1287 %User{} = user -> {:ok, user}
1288 e -> e
1289 end
1290 end
1291
1292 defp update_user(user, data) do
1293 user
1294 |> User.remote_user_changeset(data)
1295 |> User.update_and_set_cache()
1296 end
1297
1298 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1299 Map.put(data, "url", url["href"])
1300 end
1301
1302 def maybe_fix_user_url(data), do: data
1303
1304 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1305 end