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", "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" => objtype} = object} = data,
620 when objtype in ["Question", "Answer"] do
623 |> Map.put("object", fix_object(object))
626 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
627 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
633 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
636 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
637 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
642 def handle_incoming(%{"type" => type} = data, _options)
643 when type in ~w{Like EmojiReact Announce} do
644 with :ok <- ObjectValidator.fetch_actor_and_object(data),
645 {:ok, activity, _meta} <-
646 Pipeline.common_pipeline(data, local: false) do
654 %{"type" => type} = data,
657 when type in ~w{Update Block Follow} do
658 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
659 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
665 %{"type" => "Delete"} = data,
668 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
671 {:error, {:validate_object, _}} = e ->
672 # Check if we have a create activity for this
673 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
674 %Activity{data: %{"actor" => actor}} <-
675 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
676 # We have one, insert a tombstone and retry
677 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
678 {:ok, _tombstone} <- Object.create(tombstone_data) do
679 handle_incoming(data)
689 "object" => %{"type" => "Follow", "object" => followed},
695 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
696 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
697 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
698 User.unfollow(follower, followed)
708 "object" => %{"type" => type}
712 when type in ["Like", "EmojiReact", "Announce", "Block"] do
713 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
718 # For Undos that don't have the complete object attached, try to find it in our database.
726 when is_binary(object) do
727 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
729 |> Map.put("object", data)
730 |> handle_incoming(options)
739 "actor" => origin_actor,
740 "object" => origin_actor,
741 "target" => target_actor
745 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
746 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
747 true <- origin_actor in target_user.also_known_as do
748 ActivityPub.move(origin_user, target_user, false)
754 def handle_incoming(_, _), do: :error
756 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
757 def get_obj_helper(id, options \\ []) do
758 case Object.normalize(id, true, options) do
759 %Object{} = object -> {:ok, object}
764 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
765 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
768 when attributed_to == ap_id do
769 with {:ok, activity} <-
774 "actor" => attributed_to,
777 {:ok, Object.normalize(activity)}
779 _ -> get_obj_helper(object_id)
783 def get_embedded_obj_helper(object_id, _) do
784 get_obj_helper(object_id)
787 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
788 with false <- String.starts_with?(in_reply_to, "http"),
789 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
790 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
796 def set_reply_to_uri(obj), do: obj
799 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
800 Based on Mastodon's ActivityPub::NoteSerializer#replies.
802 def set_replies(obj_data) do
804 with limit when limit > 0 <-
805 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
806 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
808 |> Object.self_replies()
809 |> select([o], fragment("?->>'id'", o.data))
816 set_replies(obj_data, replies_uris)
819 defp set_replies(obj, []) do
823 defp set_replies(obj, replies_uris) do
824 replies_collection = %{
825 "type" => "Collection",
826 "items" => replies_uris
829 Map.merge(obj, %{"replies" => replies_collection})
832 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
836 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
840 def replies(_), do: []
842 # Prepares the object of an outgoing create activity.
843 def prepare_object(object) do
850 |> prepare_attachments
854 |> strip_internal_fields
855 |> strip_internal_tags
861 # internal -> Mastodon
864 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
865 when activity_type in ["Create", "Listen"] do
868 |> Object.normalize()
874 |> Map.put("object", object)
875 |> Map.merge(Utils.make_json_ld_header())
881 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
884 |> Object.normalize()
887 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
888 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
890 data |> maybe_fix_object_url
895 |> strip_internal_fields
896 |> Map.merge(Utils.make_json_ld_header())
902 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
903 # because of course it does.
904 def prepare_outgoing(%{"type" => "Accept"} = 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" => "Reject"} = data) do
923 with follow_activity <- Activity.normalize(data["object"]) do
925 "actor" => follow_activity.actor,
926 "object" => follow_activity.data["object"],
927 "id" => follow_activity.data["id"],
933 |> Map.put("object", object)
934 |> Map.merge(Utils.make_json_ld_header())
940 def prepare_outgoing(%{"type" => _type} = data) do
943 |> strip_internal_fields
944 |> maybe_fix_object_url
945 |> Map.merge(Utils.make_json_ld_header())
950 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
951 with false <- String.starts_with?(object, "http"),
952 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
953 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
955 Map.put(data, "object", external_url)
958 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
966 def maybe_fix_object_url(data), do: data
968 def add_hashtags(object) do
970 (object["tag"] || [])
972 # Expand internal representation tags into AS2 tags.
973 tag when is_binary(tag) ->
975 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
980 # Do not process tags which are already AS2 tag objects.
981 tag when is_map(tag) ->
985 Map.put(object, "tag", tags)
988 # TODO These should be added on our side on insertion, it doesn't make much
989 # sense to regenerate these all the time
990 def add_mention_tags(object) do
991 to = object["to"] || []
992 cc = object["cc"] || []
993 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
995 mentions = Enum.map(mentioned, &build_mention_tag/1)
997 tags = object["tag"] || []
998 Map.put(object, "tag", tags ++ mentions)
1001 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1002 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1005 def take_emoji_tags(%User{emoji: emoji}) do
1008 |> Enum.map(&build_emoji_tag/1)
1011 # TODO: we should probably send mtime instead of unix epoch time for updated
1012 def add_emoji_tags(%{"emoji" => emoji} = object) do
1013 tags = object["tag"] || []
1015 out = Enum.map(emoji, &build_emoji_tag/1)
1017 Map.put(object, "tag", tags ++ out)
1020 def add_emoji_tags(object), do: object
1022 defp build_emoji_tag({name, url}) do
1024 "icon" => %{"url" => url, "type" => "Image"},
1025 "name" => ":" <> name <> ":",
1027 "updated" => "1970-01-01T00:00:00Z",
1032 def set_conversation(object) do
1033 Map.put(object, "conversation", object["context"])
1036 def set_sensitive(%{"sensitive" => true} = object) do
1040 def set_sensitive(object) do
1041 tags = object["tag"] || []
1042 Map.put(object, "sensitive", "nsfw" in tags)
1045 def set_type(%{"type" => "Answer"} = object) do
1046 Map.put(object, "type", "Note")
1049 def set_type(object), do: object
1051 def add_attributed_to(object) do
1052 attributed_to = object["attributedTo"] || object["actor"]
1053 Map.put(object, "attributedTo", attributed_to)
1056 # TODO: Revisit this
1057 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1059 def prepare_attachments(object) do
1062 |> Map.get("attachment", [])
1063 |> Enum.map(fn data ->
1064 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1068 "mediaType" => media_type,
1069 "name" => data["name"],
1070 "type" => "Document"
1074 Map.put(object, "attachment", attachments)
1077 def strip_internal_fields(object) do
1078 Map.drop(object, Pleroma.Constants.object_internal_fields())
1081 defp strip_internal_tags(%{"tag" => tags} = object) do
1082 tags = Enum.filter(tags, fn x -> is_map(x) end)
1084 Map.put(object, "tag", tags)
1087 defp strip_internal_tags(object), do: object
1089 def perform(:user_upgrade, user) do
1090 # we pass a fake user so that the followers collection is stripped away
1091 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1095 where: ^old_follower_address in a.recipients,
1100 "array_replace(?,?,?)",
1102 ^old_follower_address,
1103 ^user.follower_address
1108 |> Repo.update_all([])
1111 def upgrade_user_from_ap_id(ap_id) do
1112 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1113 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1114 {:ok, user} <- update_user(user, data) do
1115 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1118 %User{} = user -> {:ok, user}
1123 defp update_user(user, data) do
1125 |> User.remote_user_changeset(data)
1126 |> User.update_and_set_cache()
1129 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1130 Map.put(data, "url", url["href"])
1133 def maybe_fix_user_url(data), do: data
1135 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)