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", "Question", "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" => "ChatMessage"}} = data,
620 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
621 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
626 def handle_incoming(%{"type" => type} = data, _options)
627 when type in ~w{Like EmojiReact Announce} do
628 with :ok <- ObjectValidator.fetch_actor_and_object(data),
629 {:ok, activity, _meta} <-
630 Pipeline.common_pipeline(data, local: false) do
638 %{"type" => type} = data,
641 when type in ~w{Update Block Follow} do
642 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
643 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
649 %{"type" => "Delete"} = data,
652 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
655 {:error, {:validate_object, _}} = e ->
656 # Check if we have a create activity for this
657 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
658 %Activity{data: %{"actor" => actor}} <-
659 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
660 # We have one, insert a tombstone and retry
661 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
662 {:ok, _tombstone} <- Object.create(tombstone_data) do
663 handle_incoming(data)
673 "object" => %{"type" => "Follow", "object" => followed},
679 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
680 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
681 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
682 User.unfollow(follower, followed)
692 "object" => %{"type" => type}
696 when type in ["Like", "EmojiReact", "Announce", "Block"] do
697 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
702 # For Undos that don't have the complete object attached, try to find it in our database.
710 when is_binary(object) do
711 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
713 |> Map.put("object", data)
714 |> handle_incoming(options)
723 "actor" => origin_actor,
724 "object" => origin_actor,
725 "target" => target_actor
729 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
730 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
731 true <- origin_actor in target_user.also_known_as do
732 ActivityPub.move(origin_user, target_user, false)
738 def handle_incoming(_, _), do: :error
740 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
741 def get_obj_helper(id, options \\ []) do
742 case Object.normalize(id, true, options) do
743 %Object{} = object -> {:ok, object}
748 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
749 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
752 when attributed_to == ap_id do
753 with {:ok, activity} <-
758 "actor" => attributed_to,
761 {:ok, Object.normalize(activity)}
763 _ -> get_obj_helper(object_id)
767 def get_embedded_obj_helper(object_id, _) do
768 get_obj_helper(object_id)
771 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
772 with false <- String.starts_with?(in_reply_to, "http"),
773 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
774 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
780 def set_reply_to_uri(obj), do: obj
783 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
784 Based on Mastodon's ActivityPub::NoteSerializer#replies.
786 def set_replies(obj_data) do
788 with limit when limit > 0 <-
789 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
790 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
792 |> Object.self_replies()
793 |> select([o], fragment("?->>'id'", o.data))
800 set_replies(obj_data, replies_uris)
803 defp set_replies(obj, []) do
807 defp set_replies(obj, replies_uris) do
808 replies_collection = %{
809 "type" => "Collection",
810 "items" => replies_uris
813 Map.merge(obj, %{"replies" => replies_collection})
816 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
820 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
824 def replies(_), do: []
826 # Prepares the object of an outgoing create activity.
827 def prepare_object(object) do
834 |> prepare_attachments
838 |> strip_internal_fields
839 |> strip_internal_tags
845 # internal -> Mastodon
848 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
849 when activity_type in ["Create", "Listen"] do
852 |> Object.normalize()
858 |> Map.put("object", object)
859 |> Map.merge(Utils.make_json_ld_header())
865 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
868 |> Object.normalize()
871 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
872 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
874 data |> maybe_fix_object_url
879 |> strip_internal_fields
880 |> Map.merge(Utils.make_json_ld_header())
886 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
887 # because of course it does.
888 def prepare_outgoing(%{"type" => "Accept"} = data) do
889 with follow_activity <- Activity.normalize(data["object"]) do
891 "actor" => follow_activity.actor,
892 "object" => follow_activity.data["object"],
893 "id" => follow_activity.data["id"],
899 |> Map.put("object", object)
900 |> Map.merge(Utils.make_json_ld_header())
906 def prepare_outgoing(%{"type" => "Reject"} = data) do
907 with follow_activity <- Activity.normalize(data["object"]) do
909 "actor" => follow_activity.actor,
910 "object" => follow_activity.data["object"],
911 "id" => follow_activity.data["id"],
917 |> Map.put("object", object)
918 |> Map.merge(Utils.make_json_ld_header())
924 def prepare_outgoing(%{"type" => _type} = data) do
927 |> strip_internal_fields
928 |> maybe_fix_object_url
929 |> Map.merge(Utils.make_json_ld_header())
934 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
935 with false <- String.starts_with?(object, "http"),
936 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
937 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
939 Map.put(data, "object", external_url)
942 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
950 def maybe_fix_object_url(data), do: data
952 def add_hashtags(object) do
954 (object["tag"] || [])
956 # Expand internal representation tags into AS2 tags.
957 tag when is_binary(tag) ->
959 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
964 # Do not process tags which are already AS2 tag objects.
965 tag when is_map(tag) ->
969 Map.put(object, "tag", tags)
972 # TODO These should be added on our side on insertion, it doesn't make much
973 # sense to regenerate these all the time
974 def add_mention_tags(object) do
975 to = object["to"] || []
976 cc = object["cc"] || []
977 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
979 mentions = Enum.map(mentioned, &build_mention_tag/1)
981 tags = object["tag"] || []
982 Map.put(object, "tag", tags ++ mentions)
985 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
986 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
989 def take_emoji_tags(%User{emoji: emoji}) do
992 |> Enum.map(&build_emoji_tag/1)
995 # TODO: we should probably send mtime instead of unix epoch time for updated
996 def add_emoji_tags(%{"emoji" => emoji} = object) do
997 tags = object["tag"] || []
999 out = Enum.map(emoji, &build_emoji_tag/1)
1001 Map.put(object, "tag", tags ++ out)
1004 def add_emoji_tags(object), do: object
1006 defp build_emoji_tag({name, url}) do
1008 "icon" => %{"url" => url, "type" => "Image"},
1009 "name" => ":" <> name <> ":",
1011 "updated" => "1970-01-01T00:00:00Z",
1016 def set_conversation(object) do
1017 Map.put(object, "conversation", object["context"])
1020 def set_sensitive(%{"sensitive" => true} = object) do
1024 def set_sensitive(object) do
1025 tags = object["tag"] || []
1026 Map.put(object, "sensitive", "nsfw" in tags)
1029 def set_type(%{"type" => "Answer"} = object) do
1030 Map.put(object, "type", "Note")
1033 def set_type(object), do: object
1035 def add_attributed_to(object) do
1036 attributed_to = object["attributedTo"] || object["actor"]
1037 Map.put(object, "attributedTo", attributed_to)
1040 # TODO: Revisit this
1041 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1043 def prepare_attachments(object) do
1046 |> Map.get("attachment", [])
1047 |> Enum.map(fn data ->
1048 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1052 "mediaType" => media_type,
1053 "name" => data["name"],
1054 "type" => "Document"
1058 Map.put(object, "attachment", attachments)
1061 def strip_internal_fields(object) do
1062 Map.drop(object, Pleroma.Constants.object_internal_fields())
1065 defp strip_internal_tags(%{"tag" => tags} = object) do
1066 tags = Enum.filter(tags, fn x -> is_map(x) end)
1068 Map.put(object, "tag", tags)
1071 defp strip_internal_tags(object), do: object
1073 def perform(:user_upgrade, user) do
1074 # we pass a fake user so that the followers collection is stripped away
1075 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1079 where: ^old_follower_address in a.recipients,
1084 "array_replace(?,?,?)",
1086 ^old_follower_address,
1087 ^user.follower_address
1092 |> Repo.update_all([])
1095 def upgrade_user_from_ap_id(ap_id) do
1096 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1097 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1098 {:ok, user} <- update_user(user, data) do
1099 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1102 %User{} = user -> {:ok, user}
1107 defp update_user(user, data) do
1109 |> User.remote_user_changeset(data)
1110 |> User.update_and_set_cache()
1113 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1114 Map.put(data, "url", url["href"])
1117 def maybe_fix_user_url(data), do: data
1119 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)