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
69 Map.put(map, field, Enum.filter(addrs, &is_binary/1))
72 Map.put(map, field, [addrs])
75 Map.put(map, field, [])
79 def fix_explicit_addressing(
80 %{"to" => to, "cc" => cc} = object,
84 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
86 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
90 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
94 |> Map.put("to", explicit_to)
95 |> Map.put("cc", final_cc)
98 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
100 # if directMessage flag is set to true, leave the addressing alone
101 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
103 def fix_explicit_addressing(object) do
104 explicit_mentions = Utils.determine_explicit_mentions(object)
106 %User{follower_address: follower_collection} =
108 |> Containment.get_actor()
109 |> User.get_cached_by_ap_id()
114 Pleroma.Constants.as_public(),
118 fix_explicit_addressing(object, explicit_mentions, follower_collection)
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
126 if followers_collection not in recipients do
128 Pleroma.Constants.as_public() in cc ->
129 to = to ++ [followers_collection]
130 Map.put(object, "to", to)
132 Pleroma.Constants.as_public() in to ->
133 cc = cc ++ [followers_collection]
134 Map.put(object, "cc", cc)
144 def fix_implicit_addressing(object, _), do: object
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)
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)
159 def fix_actor(%{"attributedTo" => actor} = object) do
160 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
163 def fix_in_reply_to(object, options \\ [])
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
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
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"])
181 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
189 def fix_in_reply_to(object, _options), do: object
191 defp prepare_in_reply_to(in_reply_to) do
193 is_bitstring(in_reply_to) ->
196 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
199 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
200 Enum.at(in_reply_to, 0)
207 def fix_context(object) do
208 context = object["context"] || object["conversation"] || Utils.generate_context_id()
211 |> Map.put("context", context)
212 |> Map.drop(["conversation"])
215 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
217 Enum.map(attachment, fn data ->
220 is_list(data["url"]) -> List.first(data["url"])
221 is_map(data["url"]) -> data["url"]
227 is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
228 MIME.valid?(data["mediaType"]) -> data["mediaType"]
229 MIME.valid?(data["mimeType"]) -> data["mimeType"]
235 is_map(url) && is_binary(url["href"]) -> url["href"]
236 is_binary(data["url"]) -> data["url"]
237 is_binary(data["href"]) -> data["href"]
244 |> Maps.put_if_present("mediaType", media_type)
245 |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
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"])
257 Map.put(object, "attachment", attachments)
260 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
262 |> Map.put("attachment", [attachment])
266 def fix_attachments(object), do: object
268 def fix_url(%{"url" => url} = object) when is_map(url) do
269 Map.put(object, "url", url["href"])
272 def fix_url(%{"type" => object_type, "url" => url} = object)
273 when object_type in ["Video", "Audio"] and is_list(url) do
275 Enum.find(url, fn x ->
276 media_type = x["mediaType"] || x["mimeType"] || ""
278 is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
282 Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
285 |> Map.put("attachment", [attachment])
286 |> Map.put("url", link_element["href"])
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)
295 is_bitstring(first_element) -> first_element
296 is_map(first_element) -> first_element["href"] || ""
300 Map.put(object, "url", url_string)
303 def fix_url(object), do: object
305 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
308 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
309 |> Enum.reduce(%{}, fn data, mapping ->
310 name = String.trim(data["name"], ":")
312 Map.put(mapping, name, data["icon"]["url"])
315 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
316 emoji = Map.merge(object["emoji"] || %{}, emoji)
318 Map.put(object, "emoji", emoji)
321 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
322 name = String.trim(tag["name"], ":")
323 emoji = %{name => tag["icon"]["url"]}
325 Map.put(object, "emoji", emoji)
328 def fix_emoji(object), do: object
330 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
333 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
334 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
336 Map.put(object, "tag", tag ++ tags)
339 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
340 combined = [tag, String.slice(hashtag, 1..-1)]
342 Map.put(object, "tag", combined)
345 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
347 def fix_tag(object), do: object
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)
354 Map.put(object, "content", content)
357 def fix_content_map(object), do: object
359 def fix_type(object, options \\ [])
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")
371 def fix_type(object, _), do: object
373 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
374 when is_binary(content) do
377 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
378 |> Pleroma.HTML.filter_tags()
380 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
383 defp fix_content(object), do: object
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
395 defp mastodon_follow_hack(_, _), do: {:error, nil}
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
402 # Can't find the activity. This might a Mastodon 2.3 "Accept"
404 mastodon_follow_hack(follow_object, followed)
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
422 def handle_incoming(data, options \\ [])
424 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
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
440 additional: %{"cc" => [account.ap_id]}
442 |> ActivityPub.flag()
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,
453 # TODO: validate those with a Ecto scheme
457 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
460 when objtype in ["Article", "Event", "Note", "Video", "Page", "Answer", "Audio"] do
461 actor = Containment.get_actor(data)
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)
472 context: object["context"],
474 published: data["published"],
483 with {:ok, created_activity} <- ActivityPub.create(params) do
484 reply_depth = (options[:depth] || 0) + 1
486 if Federator.allowed_thread_distance?(reply_depth) do
487 for reply_id <- replies(object) do
488 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
490 "depth" => reply_depth
495 {:ok, created_activity}
498 %Activity{} = activity -> {:ok, activity}
504 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
507 actor = Containment.get_actor(data)
510 Map.put(data, "actor", actor)
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)
524 published: data["published"],
525 additional: Map.take(data, ["cc", "id"])
528 ActivityPub.listen(params)
535 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
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)
547 Notification.update_notification_type(followed, follow_activity)
549 ActivityPub.accept(%{
550 to: follow_activity.data["to"],
553 object: follow_activity.data["id"],
564 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
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),
574 ActivityPub.reject(%{
575 to: follow_activity.data["to"],
578 object: follow_activity.data["id"],
588 @misskey_reactions %{
602 @doc "Rewrite misskey likes into EmojiReacts"
606 "_misskey_reaction" => reaction
611 |> Map.put("type", "EmojiReact")
612 |> Map.put("content", @misskey_reactions[reaction] || reaction)
613 |> handle_incoming(options)
617 %{"type" => "Create", "object" => %{"type" => "Question"} = object} = data,
622 |> Map.put("object", fix_object(object))
625 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
626 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
632 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
635 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
636 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
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
653 %{"type" => type} = data,
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
664 %{"type" => "Delete"} = data,
667 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
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)
688 "object" => %{"type" => "Follow", "object" => followed},
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)
707 "object" => %{"type" => type}
711 when type in ["Like", "EmojiReact", "Announce", "Block"] do
712 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
717 # For Undos that don't have the complete object attached, try to find it in our database.
725 when is_binary(object) do
726 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
728 |> Map.put("object", data)
729 |> handle_incoming(options)
738 "actor" => origin_actor,
739 "object" => origin_actor,
740 "target" => target_actor
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)
753 def handle_incoming(_, _), do: :error
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}
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{
767 when attributed_to == ap_id do
768 with {:ok, activity} <-
773 "actor" => attributed_to,
776 {:ok, Object.normalize(activity)}
778 _ -> get_obj_helper(object_id)
782 def get_embedded_obj_helper(object_id, _) do
783 get_obj_helper(object_id)
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)
795 def set_reply_to_uri(obj), do: obj
798 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
799 Based on Mastodon's ActivityPub::NoteSerializer#replies.
801 def set_replies(obj_data) do
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
807 |> Object.self_replies()
808 |> select([o], fragment("?->>'id'", o.data))
815 set_replies(obj_data, replies_uris)
818 defp set_replies(obj, []) do
822 defp set_replies(obj, replies_uris) do
823 replies_collection = %{
824 "type" => "Collection",
825 "items" => replies_uris
828 Map.merge(obj, %{"replies" => replies_collection})
831 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
835 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
839 def replies(_), do: []
841 # Prepares the object of an outgoing create activity.
842 def prepare_object(object) do
849 |> prepare_attachments
853 |> strip_internal_fields
854 |> strip_internal_tags
860 # internal -> Mastodon
863 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
864 when activity_type in ["Create", "Listen"] do
867 |> Object.normalize()
873 |> Map.put("object", object)
874 |> Map.merge(Utils.make_json_ld_header())
880 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
883 |> Object.normalize()
886 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
887 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
889 data |> maybe_fix_object_url
894 |> strip_internal_fields
895 |> Map.merge(Utils.make_json_ld_header())
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
906 "actor" => follow_activity.actor,
907 "object" => follow_activity.data["object"],
908 "id" => follow_activity.data["id"],
914 |> Map.put("object", object)
915 |> Map.merge(Utils.make_json_ld_header())
921 def prepare_outgoing(%{"type" => "Reject"} = data) do
922 with follow_activity <- Activity.normalize(data["object"]) do
924 "actor" => follow_activity.actor,
925 "object" => follow_activity.data["object"],
926 "id" => follow_activity.data["id"],
932 |> Map.put("object", object)
933 |> Map.merge(Utils.make_json_ld_header())
939 def prepare_outgoing(%{"type" => _type} = data) do
942 |> strip_internal_fields
943 |> maybe_fix_object_url
944 |> Map.merge(Utils.make_json_ld_header())
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) <-
954 Map.put(data, "object", external_url)
957 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
965 def maybe_fix_object_url(data), do: data
967 def add_hashtags(object) do
969 (object["tag"] || [])
971 # Expand internal representation tags into AS2 tags.
972 tag when is_binary(tag) ->
974 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
979 # Do not process tags which are already AS2 tag objects.
980 tag when is_map(tag) ->
984 Map.put(object, "tag", tags)
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)
994 mentions = Enum.map(mentioned, &build_mention_tag/1)
996 tags = object["tag"] || []
997 Map.put(object, "tag", tags ++ mentions)
1000 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1001 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1004 def take_emoji_tags(%User{emoji: emoji}) do
1007 |> Enum.map(&build_emoji_tag/1)
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"] || []
1014 out = Enum.map(emoji, &build_emoji_tag/1)
1016 Map.put(object, "tag", tags ++ out)
1019 def add_emoji_tags(object), do: object
1021 defp build_emoji_tag({name, url}) do
1023 "icon" => %{"url" => url, "type" => "Image"},
1024 "name" => ":" <> name <> ":",
1026 "updated" => "1970-01-01T00:00:00Z",
1031 def set_conversation(object) do
1032 Map.put(object, "conversation", object["context"])
1035 def set_sensitive(%{"sensitive" => true} = object) do
1039 def set_sensitive(object) do
1040 tags = object["tag"] || []
1041 Map.put(object, "sensitive", "nsfw" in tags)
1044 def set_type(%{"type" => "Answer"} = object) do
1045 Map.put(object, "type", "Note")
1048 def set_type(object), do: object
1050 def add_attributed_to(object) do
1051 attributed_to = object["attributedTo"] || object["actor"]
1052 Map.put(object, "attributedTo", attributed_to)
1055 # TODO: Revisit this
1056 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1058 def prepare_attachments(object) do
1061 |> Map.get("attachment", [])
1062 |> Enum.map(fn data ->
1063 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1067 "mediaType" => media_type,
1068 "name" => data["name"],
1069 "type" => "Document"
1073 Map.put(object, "attachment", attachments)
1076 def strip_internal_fields(object) do
1077 Map.drop(object, Pleroma.Constants.object_internal_fields())
1080 defp strip_internal_tags(%{"tag" => tags} = object) do
1081 tags = Enum.filter(tags, fn x -> is_map(x) end)
1083 Map.put(object, "tag", tags)
1086 defp strip_internal_tags(object), do: object
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})
1094 where: ^old_follower_address in a.recipients,
1099 "array_replace(?,?,?)",
1101 ^old_follower_address,
1102 ^user.follower_address
1107 |> Repo.update_all([])
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})
1117 %User{} = user -> {:ok, user}
1122 defp update_user(user, data) do
1124 |> User.remote_user_changeset(data)
1125 |> User.update_and_set_cache()
1128 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1129 Map.put(data, "url", url["href"])
1132 def maybe_fix_user_url(data), do: data
1134 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)