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