1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
7 A module to handle coding from internal to wire ActivityPub and back.
10 alias Pleroma.EarmarkRenderer
11 alias Pleroma.EctoType.ActivityPub.ObjectValidators
12 alias Pleroma.FollowingRelationship
14 alias Pleroma.Notification
16 alias Pleroma.Object.Containment
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
31 require Pleroma.Constants
34 Modifies an incoming AP object (mastodon format) to our internal format.
36 def fix_object(object, options \\ []) do
38 |> strip_internal_fields
43 |> fix_in_reply_to(options)
53 def fix_summary(%{"summary" => nil} = object) do
54 Map.put(object, "summary", "")
57 def fix_summary(%{"summary" => _} = object) do
58 # summary is present, nothing to do
62 def fix_summary(object), do: Map.put(object, "summary", "")
64 def fix_addressing_list(map, field) do
66 is_binary(map[field]) ->
67 Map.put(map, field, [map[field]])
70 Map.put(map, field, [])
77 def fix_explicit_addressing(
78 %{"to" => to, "cc" => cc} = object,
82 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)
88 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
92 |> Map.put("to", explicit_to)
93 |> Map.put("cc", final_cc)
96 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
98 # if directMessage flag is set to true, leave the addressing alone
99 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
101 def fix_explicit_addressing(object) do
102 explicit_mentions = Utils.determine_explicit_mentions(object)
104 %User{follower_address: follower_collection} =
106 |> Containment.get_actor()
107 |> User.get_cached_by_ap_id()
112 Pleroma.Constants.as_public(),
116 fix_explicit_addressing(object, explicit_mentions, follower_collection)
119 # if as:Public is addressed, then make sure the followers collection is also addressed
120 # so that the activities will be delivered to local users.
121 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
122 recipients = to ++ cc
124 if followers_collection not in recipients do
126 Pleroma.Constants.as_public() in cc ->
127 to = to ++ [followers_collection]
128 Map.put(object, "to", to)
130 Pleroma.Constants.as_public() in to ->
131 cc = cc ++ [followers_collection]
132 Map.put(object, "cc", cc)
142 def fix_implicit_addressing(object, _), do: object
144 def fix_addressing(object) do
145 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
146 followers_collection = User.ap_followers(user)
149 |> fix_addressing_list("to")
150 |> fix_addressing_list("cc")
151 |> fix_addressing_list("bto")
152 |> fix_addressing_list("bcc")
153 |> fix_explicit_addressing()
154 |> fix_implicit_addressing(followers_collection)
157 def fix_actor(%{"attributedTo" => actor} = object) do
158 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
161 def fix_in_reply_to(object, options \\ [])
163 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
164 when not is_nil(in_reply_to) do
165 in_reply_to_id = prepare_in_reply_to(in_reply_to)
166 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
167 depth = (options[:depth] || 0) + 1
169 if Federator.allowed_thread_distance?(depth) do
170 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
171 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
173 |> Map.put("inReplyTo", replied_object.data["id"])
174 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
175 |> Map.put("context", replied_object.data["context"] || object["conversation"])
176 |> Map.drop(["conversation"])
179 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
187 def fix_in_reply_to(object, _options), do: object
189 defp prepare_in_reply_to(in_reply_to) do
191 is_bitstring(in_reply_to) ->
194 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
197 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
198 Enum.at(in_reply_to, 0)
205 def fix_context(object) do
206 context = object["context"] || object["conversation"] || Utils.generate_context_id()
209 |> Map.put("context", context)
210 |> Map.drop(["conversation"])
213 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
215 Enum.map(attachment, fn data ->
218 is_list(data["url"]) -> List.first(data["url"])
219 is_map(data["url"]) -> data["url"]
225 is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
226 MIME.valid?(data["mediaType"]) -> data["mediaType"]
227 MIME.valid?(data["mimeType"]) -> data["mimeType"]
233 is_map(url) && is_binary(url["href"]) -> url["href"]
234 is_binary(data["url"]) -> data["url"]
235 is_binary(data["href"]) -> data["href"]
242 |> Maps.put_if_present("mediaType", media_type)
243 |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
245 %{"url" => [attachment_url]}
246 |> Maps.put_if_present("mediaType", media_type)
247 |> Maps.put_if_present("type", data["type"])
248 |> Maps.put_if_present("name", data["name"])
255 Map.put(object, "attachment", attachments)
258 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
260 |> Map.put("attachment", [attachment])
264 def fix_attachments(object), do: object
266 def fix_url(%{"url" => url} = object) when is_map(url) do
267 Map.put(object, "url", url["href"])
270 def fix_url(%{"type" => object_type, "url" => url} = object)
271 when object_type in ["Video", "Audio"] and is_list(url) do
273 Enum.find(url, fn x ->
274 media_type = x["mediaType"] || x["mimeType"] || ""
276 is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
280 Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
283 |> Map.put("attachment", [attachment])
284 |> Map.put("url", link_element["href"])
287 def fix_url(%{"type" => object_type, "url" => url} = object)
288 when object_type != "Video" and is_list(url) do
289 first_element = Enum.at(url, 0)
293 is_bitstring(first_element) -> first_element
294 is_map(first_element) -> first_element["href"] || ""
298 Map.put(object, "url", url_string)
301 def fix_url(object), do: object
303 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
306 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
307 |> Enum.reduce(%{}, fn data, mapping ->
308 name = String.trim(data["name"], ":")
310 Map.put(mapping, name, data["icon"]["url"])
313 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
314 emoji = Map.merge(object["emoji"] || %{}, emoji)
316 Map.put(object, "emoji", emoji)
319 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
320 name = String.trim(tag["name"], ":")
321 emoji = %{name => tag["icon"]["url"]}
323 Map.put(object, "emoji", emoji)
326 def fix_emoji(object), do: object
328 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
331 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
332 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
334 Map.put(object, "tag", tag ++ tags)
337 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
338 combined = [tag, String.slice(hashtag, 1..-1)]
340 Map.put(object, "tag", combined)
343 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
345 def fix_tag(object), do: object
347 # content map usually only has one language so this will do for now.
348 def fix_content_map(%{"contentMap" => content_map} = object) do
349 content_groups = Map.to_list(content_map)
350 {_, content} = Enum.at(content_groups, 0)
352 Map.put(object, "content", content)
355 def fix_content_map(object), do: object
357 def fix_type(object, options \\ [])
359 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
360 when is_binary(reply_id) do
361 with true <- Federator.allowed_thread_distance?(options[:depth]),
362 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
363 Map.put(object, "type", "Answer")
369 def fix_type(object, _), do: object
371 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
372 when is_binary(content) do
375 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
376 |> Pleroma.HTML.filter_tags()
378 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
381 defp fix_content(object), do: object
383 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
384 with true <- id =~ "follows",
385 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
386 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
393 defp mastodon_follow_hack(_, _), do: {:error, nil}
395 defp get_follow_activity(follow_object, followed) do
396 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
397 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
400 # Can't find the activity. This might a Mastodon 2.3 "Accept"
402 mastodon_follow_hack(follow_object, followed)
409 # Reduce the object list to find the reported user.
410 defp get_reported(objects) do
411 Enum.reduce_while(objects, nil, fn ap_id, _ ->
412 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
420 def handle_incoming(data, options \\ [])
422 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
424 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
425 with context <- data["context"] || Utils.generate_context_id(),
426 content <- data["content"] || "",
427 %User{} = actor <- User.get_cached_by_ap_id(actor),
428 # Reduce the object list to find the reported user.
429 %User{} = account <- get_reported(objects),
430 # Remove the reported user from the object list.
431 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
438 additional: %{"cc" => [account.ap_id]}
440 |> ActivityPub.flag()
444 # disallow objects with bogus IDs
445 def handle_incoming(%{"id" => nil}, _options), do: :error
446 def handle_incoming(%{"id" => ""}, _options), do: :error
447 # length of https:// = 8, should validate better, but good enough for now.
448 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
451 # TODO: validate those with a Ecto scheme
455 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
458 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
459 actor = Containment.get_actor(data)
461 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
462 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
463 data <- Map.put(data, "actor", actor) |> fix_addressing() do
464 object = fix_object(object, options)
470 context: object["context"],
472 published: data["published"],
481 with {:ok, created_activity} <- ActivityPub.create(params) do
482 reply_depth = (options[:depth] || 0) + 1
484 if Federator.allowed_thread_distance?(reply_depth) do
485 for reply_id <- replies(object) do
486 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
488 "depth" => reply_depth
493 {:ok, created_activity}
496 %Activity{} = activity -> {:ok, activity}
502 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
505 actor = Containment.get_actor(data)
508 Map.put(data, "actor", actor)
511 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
512 reply_depth = (options[:depth] || 0) + 1
513 options = Keyword.put(options, :depth, reply_depth)
514 object = fix_object(object, options)
522 published: data["published"],
523 additional: Map.take(data, ["cc", "id"])
526 ActivityPub.listen(params)
533 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
536 with actor <- Containment.get_actor(data),
537 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
538 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
539 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
540 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
541 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
542 User.update_follower_count(followed)
543 User.update_following_count(follower)
545 Notification.update_notification_type(followed, follow_activity)
547 ActivityPub.accept(%{
548 to: follow_activity.data["to"],
551 object: follow_activity.data["id"],
562 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
565 with actor <- Containment.get_actor(data),
566 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
567 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
568 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
569 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
570 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
572 ActivityPub.reject(%{
573 to: follow_activity.data["to"],
576 object: follow_activity.data["id"],
586 @misskey_reactions %{
600 @doc "Rewrite misskey likes into EmojiReacts"
604 "_misskey_reaction" => reaction
609 |> Map.put("type", "EmojiReact")
610 |> Map.put("content", @misskey_reactions[reaction] || reaction)
611 |> handle_incoming(options)
615 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
618 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
619 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
624 def handle_incoming(%{"type" => type} = data, _options)
625 when type in ~w{Like EmojiReact Announce} do
626 with :ok <- ObjectValidator.fetch_actor_and_object(data),
627 {:ok, activity, _meta} <-
628 Pipeline.common_pipeline(data, local: false) do
636 %{"type" => type} = data,
639 when type in ~w{Update Block Follow} do
640 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
641 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
647 %{"type" => "Delete"} = data,
650 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
653 {:error, {:validate_object, _}} = e ->
654 # Check if we have a create activity for this
655 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
656 %Activity{data: %{"actor" => actor}} <-
657 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
658 # We have one, insert a tombstone and retry
659 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
660 {:ok, _tombstone} <- Object.create(tombstone_data) do
661 handle_incoming(data)
671 "object" => %{"type" => "Follow", "object" => followed},
677 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
678 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
679 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
680 User.unfollow(follower, followed)
690 "object" => %{"type" => type}
694 when type in ["Like", "EmojiReact", "Announce", "Block"] do
695 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
700 # For Undos that don't have the complete object attached, try to find it in our database.
708 when is_binary(object) do
709 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
711 |> Map.put("object", data)
712 |> handle_incoming(options)
721 "actor" => origin_actor,
722 "object" => origin_actor,
723 "target" => target_actor
727 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
728 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
729 true <- origin_actor in target_user.also_known_as do
730 ActivityPub.move(origin_user, target_user, false)
736 def handle_incoming(_, _), do: :error
738 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
739 def get_obj_helper(id, options \\ []) do
740 case Object.normalize(id, true, options) do
741 %Object{} = object -> {:ok, object}
746 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
747 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
750 when attributed_to == ap_id do
751 with {:ok, activity} <-
756 "actor" => attributed_to,
759 {:ok, Object.normalize(activity)}
761 _ -> get_obj_helper(object_id)
765 def get_embedded_obj_helper(object_id, _) do
766 get_obj_helper(object_id)
769 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
770 with false <- String.starts_with?(in_reply_to, "http"),
771 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
772 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
778 def set_reply_to_uri(obj), do: obj
781 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
782 Based on Mastodon's ActivityPub::NoteSerializer#replies.
784 def set_replies(obj_data) do
786 with limit when limit > 0 <-
787 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
788 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
790 |> Object.self_replies()
791 |> select([o], fragment("?->>'id'", o.data))
798 set_replies(obj_data, replies_uris)
801 defp set_replies(obj, []) do
805 defp set_replies(obj, replies_uris) do
806 replies_collection = %{
807 "type" => "Collection",
808 "items" => replies_uris
811 Map.merge(obj, %{"replies" => replies_collection})
814 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
818 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
822 def replies(_), do: []
824 # Prepares the object of an outgoing create activity.
825 def prepare_object(object) do
832 |> prepare_attachments
836 |> strip_internal_fields
837 |> strip_internal_tags
843 # internal -> Mastodon
846 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
847 when activity_type in ["Create", "Listen"] do
850 |> Object.normalize()
856 |> Map.put("object", object)
857 |> Map.merge(Utils.make_json_ld_header())
863 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
866 |> Object.normalize()
869 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
870 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
872 data |> maybe_fix_object_url
877 |> strip_internal_fields
878 |> Map.merge(Utils.make_json_ld_header())
884 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
885 # because of course it does.
886 def prepare_outgoing(%{"type" => "Accept"} = data) do
887 with follow_activity <- Activity.normalize(data["object"]) do
889 "actor" => follow_activity.actor,
890 "object" => follow_activity.data["object"],
891 "id" => follow_activity.data["id"],
897 |> Map.put("object", object)
898 |> Map.merge(Utils.make_json_ld_header())
904 def prepare_outgoing(%{"type" => "Reject"} = data) do
905 with follow_activity <- Activity.normalize(data["object"]) do
907 "actor" => follow_activity.actor,
908 "object" => follow_activity.data["object"],
909 "id" => follow_activity.data["id"],
915 |> Map.put("object", object)
916 |> Map.merge(Utils.make_json_ld_header())
922 def prepare_outgoing(%{"type" => _type} = data) do
925 |> strip_internal_fields
926 |> maybe_fix_object_url
927 |> Map.merge(Utils.make_json_ld_header())
932 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
933 with false <- String.starts_with?(object, "http"),
934 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
935 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
937 Map.put(data, "object", external_url)
940 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
948 def maybe_fix_object_url(data), do: data
950 def add_hashtags(object) do
952 (object["tag"] || [])
954 # Expand internal representation tags into AS2 tags.
955 tag when is_binary(tag) ->
957 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
962 # Do not process tags which are already AS2 tag objects.
963 tag when is_map(tag) ->
967 Map.put(object, "tag", tags)
970 # TODO These should be added on our side on insertion, it doesn't make much
971 # sense to regenerate these all the time
972 def add_mention_tags(object) do
973 to = object["to"] || []
974 cc = object["cc"] || []
975 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
977 mentions = Enum.map(mentioned, &build_mention_tag/1)
979 tags = object["tag"] || []
980 Map.put(object, "tag", tags ++ mentions)
983 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
984 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
987 def take_emoji_tags(%User{emoji: emoji}) do
990 |> Enum.map(&build_emoji_tag/1)
993 # TODO: we should probably send mtime instead of unix epoch time for updated
994 def add_emoji_tags(%{"emoji" => emoji} = object) do
995 tags = object["tag"] || []
997 out = Enum.map(emoji, &build_emoji_tag/1)
999 Map.put(object, "tag", tags ++ out)
1002 def add_emoji_tags(object), do: object
1004 defp build_emoji_tag({name, url}) do
1006 "icon" => %{"url" => url, "type" => "Image"},
1007 "name" => ":" <> name <> ":",
1009 "updated" => "1970-01-01T00:00:00Z",
1014 def set_conversation(object) do
1015 Map.put(object, "conversation", object["context"])
1018 def set_sensitive(%{"sensitive" => true} = object) do
1022 def set_sensitive(object) do
1023 tags = object["tag"] || []
1024 Map.put(object, "sensitive", "nsfw" in tags)
1027 def set_type(%{"type" => "Answer"} = object) do
1028 Map.put(object, "type", "Note")
1031 def set_type(object), do: object
1033 def add_attributed_to(object) do
1034 attributed_to = object["attributedTo"] || object["actor"]
1035 Map.put(object, "attributedTo", attributed_to)
1038 # TODO: Revisit this
1039 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1041 def prepare_attachments(object) do
1044 |> Map.get("attachment", [])
1045 |> Enum.map(fn data ->
1046 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1050 "mediaType" => media_type,
1051 "name" => data["name"],
1052 "type" => "Document"
1056 Map.put(object, "attachment", attachments)
1059 def strip_internal_fields(object) do
1060 Map.drop(object, Pleroma.Constants.object_internal_fields())
1063 defp strip_internal_tags(%{"tag" => tags} = object) do
1064 tags = Enum.filter(tags, fn x -> is_map(x) end)
1066 Map.put(object, "tag", tags)
1069 defp strip_internal_tags(object), do: object
1071 def perform(:user_upgrade, user) do
1072 # we pass a fake user so that the followers collection is stripped away
1073 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1077 where: ^old_follower_address in a.recipients,
1082 "array_replace(?,?,?)",
1084 ^old_follower_address,
1085 ^user.follower_address
1090 |> Repo.update_all([])
1093 def upgrade_user_from_ap_id(ap_id) do
1094 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1095 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1096 {:ok, user} <- update_user(user, data) do
1097 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1100 %User{} = user -> {:ok, user}
1105 defp update_user(user, data) do
1107 |> User.remote_user_changeset(data)
1108 |> User.update_and_set_cache()
1111 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1112 Map.put(data, "url", url["href"])
1115 def maybe_fix_user_url(data), do: data
1117 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)