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