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