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
15 alias Pleroma.Object.Containment
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Builder
20 alias Pleroma.Web.ActivityPub.ObjectValidator
21 alias Pleroma.Web.ActivityPub.Pipeline
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.ActivityPub.Visibility
24 alias Pleroma.Web.Federator
25 alias Pleroma.Workers.TransmogrifierWorker
30 require Pleroma.Constants
33 Modifies an incoming AP object (mastodon format) to our internal format.
35 def fix_object(object, options \\ []) do
37 |> strip_internal_fields
42 |> fix_in_reply_to(options)
52 def fix_summary(%{"summary" => nil} = object) do
53 Map.put(object, "summary", "")
56 def fix_summary(%{"summary" => _} = object) do
57 # summary is present, nothing to do
61 def fix_summary(object), do: Map.put(object, "summary", "")
63 def fix_addressing_list(map, field) do
68 Map.put(map, field, Enum.filter(addrs, &is_binary/1))
71 Map.put(map, field, [addrs])
74 Map.put(map, field, [])
78 def fix_explicit_addressing(
79 %{"to" => to, "cc" => cc} = object,
83 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
85 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
89 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
93 |> Map.put("to", explicit_to)
94 |> Map.put("cc", final_cc)
97 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
99 # if directMessage flag is set to true, leave the addressing alone
100 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
102 def fix_explicit_addressing(object) do
103 explicit_mentions = Utils.determine_explicit_mentions(object)
105 %User{follower_address: follower_collection} =
107 |> Containment.get_actor()
108 |> User.get_cached_by_ap_id()
113 Pleroma.Constants.as_public(),
117 fix_explicit_addressing(object, explicit_mentions, follower_collection)
120 # if as:Public is addressed, then make sure the followers collection is also addressed
121 # so that the activities will be delivered to local users.
122 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
123 recipients = to ++ cc
125 if followers_collection not in recipients do
127 Pleroma.Constants.as_public() in cc ->
128 to = to ++ [followers_collection]
129 Map.put(object, "to", to)
131 Pleroma.Constants.as_public() in to ->
132 cc = cc ++ [followers_collection]
133 Map.put(object, "cc", cc)
143 def fix_implicit_addressing(object, _), do: object
145 def fix_addressing(object) do
146 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
147 followers_collection = User.ap_followers(user)
150 |> fix_addressing_list("to")
151 |> fix_addressing_list("cc")
152 |> fix_addressing_list("bto")
153 |> fix_addressing_list("bcc")
154 |> fix_explicit_addressing()
155 |> fix_implicit_addressing(followers_collection)
158 def fix_actor(%{"attributedTo" => actor} = object) do
159 actor = Containment.get_actor(%{"actor" => actor})
161 # TODO: Remove actor field for Objects
163 |> Map.put("actor", actor)
164 |> Map.put("attributedTo", actor)
167 def fix_in_reply_to(object, options \\ [])
169 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
170 when not is_nil(in_reply_to) do
171 in_reply_to_id = prepare_in_reply_to(in_reply_to)
172 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
173 depth = (options[:depth] || 0) + 1
175 if Federator.allowed_thread_distance?(depth) do
176 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
177 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
179 |> Map.put("inReplyTo", replied_object.data["id"])
180 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
181 |> Map.put("context", replied_object.data["context"] || object["conversation"])
182 |> Map.drop(["conversation"])
185 Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
193 def fix_in_reply_to(object, _options), do: object
195 defp prepare_in_reply_to(in_reply_to) do
197 is_bitstring(in_reply_to) ->
200 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
203 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
204 Enum.at(in_reply_to, 0)
211 def fix_context(object) do
212 context = object["context"] || object["conversation"] || Utils.generate_context_id()
215 |> Map.put("context", context)
216 |> Map.drop(["conversation"])
219 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
221 Enum.map(attachment, fn data ->
224 is_list(data["url"]) -> List.first(data["url"])
225 is_map(data["url"]) -> data["url"]
231 is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
232 MIME.valid?(data["mediaType"]) -> data["mediaType"]
233 MIME.valid?(data["mimeType"]) -> data["mimeType"]
239 is_map(url) && is_binary(url["href"]) -> url["href"]
240 is_binary(data["url"]) -> data["url"]
241 is_binary(data["href"]) -> data["href"]
249 "type" => Map.get(url || %{}, "type", "Link")
251 |> Maps.put_if_present("mediaType", media_type)
254 "url" => [attachment_url],
255 "type" => data["type"] || "Document"
257 |> Maps.put_if_present("mediaType", media_type)
258 |> Maps.put_if_present("name", data["name"])
265 Map.put(object, "attachment", attachments)
268 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
270 |> Map.put("attachment", [attachment])
274 def fix_attachments(object), do: object
276 def fix_url(%{"url" => url} = object) when is_map(url) do
277 Map.put(object, "url", url["href"])
280 def fix_url(%{"type" => object_type, "url" => url} = object)
281 when object_type in ["Video", "Audio"] and is_list(url) do
283 Enum.find(url, fn x ->
284 media_type = x["mediaType"] || x["mimeType"] || ""
286 is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
290 Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
293 |> Map.put("attachment", [attachment])
294 |> Map.put("url", link_element["href"])
297 def fix_url(%{"type" => object_type, "url" => url} = object)
298 when object_type != "Video" and is_list(url) do
299 first_element = Enum.at(url, 0)
303 is_bitstring(first_element) -> first_element
304 is_map(first_element) -> first_element["href"] || ""
308 Map.put(object, "url", url_string)
311 def fix_url(object), do: object
313 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
316 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
317 |> Enum.reduce(%{}, fn data, mapping ->
318 name = String.trim(data["name"], ":")
320 Map.put(mapping, name, data["icon"]["url"])
323 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
324 emoji = Map.merge(object["emoji"] || %{}, emoji)
326 Map.put(object, "emoji", emoji)
329 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
330 name = String.trim(tag["name"], ":")
331 emoji = %{name => tag["icon"]["url"]}
333 Map.put(object, "emoji", emoji)
336 def fix_emoji(object), do: object
338 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
341 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
342 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
344 Map.put(object, "tag", tag ++ tags)
347 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
348 combined = [tag, String.slice(hashtag, 1..-1)]
350 Map.put(object, "tag", combined)
353 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
355 def fix_tag(object), do: object
357 # content map usually only has one language so this will do for now.
358 def fix_content_map(%{"contentMap" => content_map} = object) do
359 content_groups = Map.to_list(content_map)
360 {_, content} = Enum.at(content_groups, 0)
362 Map.put(object, "content", content)
365 def fix_content_map(object), do: object
367 def fix_type(object, options \\ [])
369 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
370 when is_binary(reply_id) do
371 with true <- Federator.allowed_thread_distance?(options[:depth]),
372 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
373 Map.put(object, "type", "Answer")
379 def fix_type(object, _), do: object
381 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
382 when is_binary(content) do
385 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
386 |> Pleroma.HTML.filter_tags()
388 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
391 defp fix_content(object), do: object
393 defp get_follow_activity(follow_object, _followed) do
394 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
395 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
403 # Reduce the object list to find the reported user.
404 defp get_reported(objects) do
405 Enum.reduce_while(objects, nil, fn ap_id, _ ->
406 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
414 # Compatibility wrapper for Mastodon votes
415 defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do
416 handle_incoming(data)
419 defp handle_create(%{"object" => object} = data, user) do
424 context: object["context"],
426 published: data["published"],
434 |> ActivityPub.create()
437 def handle_incoming(data, options \\ [])
439 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
441 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
442 with context <- data["context"] || Utils.generate_context_id(),
443 content <- data["content"] || "",
444 %User{} = actor <- User.get_cached_by_ap_id(actor),
445 # Reduce the object list to find the reported user.
446 %User{} = account <- get_reported(objects),
447 # Remove the reported user from the object list.
448 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
455 additional: %{"cc" => [account.ap_id]}
457 |> ActivityPub.flag()
461 # disallow objects with bogus IDs
462 def handle_incoming(%{"id" => nil}, _options), do: :error
463 def handle_incoming(%{"id" => ""}, _options), do: :error
464 # length of https:// = 8, should validate better, but good enough for now.
465 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
468 # TODO: validate those with a Ecto scheme
472 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
475 when objtype in ["Article", "Event", "Note", "Video", "Page", "Audio"] do
476 actor = Containment.get_actor(data)
478 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
479 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
482 |> Map.put("object", fix_object(object, options))
483 |> Map.put("actor", actor)
486 with {:ok, created_activity} <- handle_create(data, user) do
487 reply_depth = (options[:depth] || 0) + 1
489 if Federator.allowed_thread_distance?(reply_depth) do
490 for reply_id <- replies(object) do
491 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
493 "depth" => reply_depth
498 {:ok, created_activity}
501 %Activity{} = activity -> {:ok, activity}
507 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
510 actor = Containment.get_actor(data)
513 Map.put(data, "actor", actor)
516 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
517 reply_depth = (options[:depth] || 0) + 1
518 options = Keyword.put(options, :depth, reply_depth)
519 object = fix_object(object, options)
527 published: data["published"],
528 additional: Map.take(data, ["cc", "id"])
531 ActivityPub.listen(params)
538 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
541 with actor <- Containment.get_actor(data),
542 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
543 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
544 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
545 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
546 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
548 ActivityPub.reject(%{
549 to: follow_activity.data["to"],
552 object: follow_activity.data["id"],
562 @misskey_reactions %{
576 @doc "Rewrite misskey likes into EmojiReacts"
580 "_misskey_reaction" => reaction
585 |> Map.put("type", "EmojiReact")
586 |> Map.put("content", @misskey_reactions[reaction] || reaction)
587 |> handle_incoming(options)
591 %{"type" => "Create", "object" => %{"type" => objtype}} = data,
594 when objtype in ["Question", "Answer", "ChatMessage"] do
595 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
596 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
601 def handle_incoming(%{"type" => type} = data, _options)
602 when type in ~w{Like EmojiReact Announce} do
603 with :ok <- ObjectValidator.fetch_actor_and_object(data),
604 {:ok, activity, _meta} <-
605 Pipeline.common_pipeline(data, local: false) do
613 %{"type" => type} = data,
616 when type in ~w{Update Block Follow Accept} do
617 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
618 {:ok, activity, _} <-
619 Pipeline.common_pipeline(data, local: false) do
625 %{"type" => "Delete"} = data,
628 with {:ok, activity, _} <-
629 Pipeline.common_pipeline(data, local: false) do
632 {:error, {:validate_object, _}} = e ->
633 # Check if we have a create activity for this
634 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
635 %Activity{data: %{"actor" => actor}} <-
636 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
637 # We have one, insert a tombstone and retry
638 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
639 {:ok, _tombstone} <- Object.create(tombstone_data) do
640 handle_incoming(data)
650 "object" => %{"type" => "Follow", "object" => followed},
656 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
657 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
658 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
659 User.unfollow(follower, followed)
669 "object" => %{"type" => type}
673 when type in ["Like", "EmojiReact", "Announce", "Block"] do
674 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
679 # For Undos that don't have the complete object attached, try to find it in our database.
687 when is_binary(object) do
688 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
690 |> Map.put("object", data)
691 |> handle_incoming(options)
700 "actor" => origin_actor,
701 "object" => origin_actor,
702 "target" => target_actor
706 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
707 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
708 true <- origin_actor in target_user.also_known_as do
709 ActivityPub.move(origin_user, target_user, false)
715 def handle_incoming(_, _), do: :error
717 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
718 def get_obj_helper(id, options \\ []) do
719 case Object.normalize(id, true, options) do
720 %Object{} = object -> {:ok, object}
725 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
726 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
729 when attributed_to == ap_id do
730 with {:ok, activity} <-
735 "actor" => attributed_to,
738 {:ok, Object.normalize(activity)}
740 _ -> get_obj_helper(object_id)
744 def get_embedded_obj_helper(object_id, _) do
745 get_obj_helper(object_id)
748 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
749 with false <- String.starts_with?(in_reply_to, "http"),
750 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
751 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
757 def set_reply_to_uri(obj), do: obj
760 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
761 Based on Mastodon's ActivityPub::NoteSerializer#replies.
763 def set_replies(obj_data) do
765 with limit when limit > 0 <-
766 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
767 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
769 |> Object.self_replies()
770 |> select([o], fragment("?->>'id'", o.data))
777 set_replies(obj_data, replies_uris)
780 defp set_replies(obj, []) do
784 defp set_replies(obj, replies_uris) do
785 replies_collection = %{
786 "type" => "Collection",
787 "items" => replies_uris
790 Map.merge(obj, %{"replies" => replies_collection})
793 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
797 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
801 def replies(_), do: []
803 # Prepares the object of an outgoing create activity.
804 def prepare_object(object) do
811 |> prepare_attachments
815 |> strip_internal_fields
816 |> strip_internal_tags
822 # internal -> Mastodon
825 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
826 when activity_type in ["Create", "Listen"] do
829 |> Object.normalize()
835 |> Map.put("object", object)
836 |> Map.merge(Utils.make_json_ld_header())
842 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
845 |> Object.normalize()
848 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
849 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
851 data |> maybe_fix_object_url
856 |> strip_internal_fields
857 |> Map.merge(Utils.make_json_ld_header())
863 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
864 # because of course it does.
865 def prepare_outgoing(%{"type" => "Accept"} = data) do
866 with follow_activity <- Activity.normalize(data["object"]) do
868 "actor" => follow_activity.actor,
869 "object" => follow_activity.data["object"],
870 "id" => follow_activity.data["id"],
876 |> Map.put("object", object)
877 |> Map.merge(Utils.make_json_ld_header())
883 def prepare_outgoing(%{"type" => "Reject"} = data) do
884 with follow_activity <- Activity.normalize(data["object"]) do
886 "actor" => follow_activity.actor,
887 "object" => follow_activity.data["object"],
888 "id" => follow_activity.data["id"],
894 |> Map.put("object", object)
895 |> Map.merge(Utils.make_json_ld_header())
901 def prepare_outgoing(%{"type" => _type} = data) do
904 |> strip_internal_fields
905 |> maybe_fix_object_url
906 |> Map.merge(Utils.make_json_ld_header())
911 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
912 with false <- String.starts_with?(object, "http"),
913 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
914 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
916 Map.put(data, "object", external_url)
919 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
927 def maybe_fix_object_url(data), do: data
929 def add_hashtags(object) do
931 (object["tag"] || [])
933 # Expand internal representation tags into AS2 tags.
934 tag when is_binary(tag) ->
936 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
941 # Do not process tags which are already AS2 tag objects.
942 tag when is_map(tag) ->
946 Map.put(object, "tag", tags)
949 # TODO These should be added on our side on insertion, it doesn't make much
950 # sense to regenerate these all the time
951 def add_mention_tags(object) do
952 to = object["to"] || []
953 cc = object["cc"] || []
954 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
956 mentions = Enum.map(mentioned, &build_mention_tag/1)
958 tags = object["tag"] || []
959 Map.put(object, "tag", tags ++ mentions)
962 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
963 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
966 def take_emoji_tags(%User{emoji: emoji}) do
969 |> Enum.map(&build_emoji_tag/1)
972 # TODO: we should probably send mtime instead of unix epoch time for updated
973 def add_emoji_tags(%{"emoji" => emoji} = object) do
974 tags = object["tag"] || []
976 out = Enum.map(emoji, &build_emoji_tag/1)
978 Map.put(object, "tag", tags ++ out)
981 def add_emoji_tags(object), do: object
983 defp build_emoji_tag({name, url}) do
985 "icon" => %{"url" => url, "type" => "Image"},
986 "name" => ":" <> name <> ":",
988 "updated" => "1970-01-01T00:00:00Z",
993 def set_conversation(object) do
994 Map.put(object, "conversation", object["context"])
997 def set_sensitive(%{"sensitive" => true} = object) do
1001 def set_sensitive(object) do
1002 tags = object["tag"] || []
1003 Map.put(object, "sensitive", "nsfw" in tags)
1006 def set_type(%{"type" => "Answer"} = object) do
1007 Map.put(object, "type", "Note")
1010 def set_type(object), do: object
1012 def add_attributed_to(object) do
1013 attributed_to = object["attributedTo"] || object["actor"]
1014 Map.put(object, "attributedTo", attributed_to)
1017 # TODO: Revisit this
1018 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1020 def prepare_attachments(object) do
1023 |> Map.get("attachment", [])
1024 |> Enum.map(fn data ->
1025 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1029 "mediaType" => media_type,
1030 "name" => data["name"],
1031 "type" => "Document"
1035 Map.put(object, "attachment", attachments)
1038 def strip_internal_fields(object) do
1039 Map.drop(object, Pleroma.Constants.object_internal_fields())
1042 defp strip_internal_tags(%{"tag" => tags} = object) do
1043 tags = Enum.filter(tags, fn x -> is_map(x) end)
1045 Map.put(object, "tag", tags)
1048 defp strip_internal_tags(object), do: object
1050 def perform(:user_upgrade, user) do
1051 # we pass a fake user so that the followers collection is stripped away
1052 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1056 where: ^old_follower_address in a.recipients,
1061 "array_replace(?,?,?)",
1063 ^old_follower_address,
1064 ^user.follower_address
1069 |> Repo.update_all([])
1072 def upgrade_user_from_ap_id(ap_id) do
1073 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1074 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1075 {:ok, user} <- update_user(user, data) do
1076 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1079 %User{} = user -> {:ok, user}
1084 defp update_user(user, data) do
1086 |> User.remote_user_changeset(data)
1087 |> User.update_and_set_cache()
1090 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1091 Map.put(data, "url", url["href"])
1094 def maybe_fix_user_url(data), do: data
1096 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)