1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.
11 alias Pleroma.Object.Containment
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.Federator
22 require Pleroma.Constants
25 Modifies an incoming AP object (mastodon format) to our internal format.
27 def fix_object(object, options \\ []) do
33 |> fix_in_reply_to(options)
43 def fix_summary(%{"summary" => nil} = object) do
45 |> Map.put("summary", "")
48 def fix_summary(%{"summary" => _} = object) do
49 # summary is present, nothing to do
53 def fix_summary(object) do
55 |> Map.put("summary", "")
58 def fix_addressing_list(map, field) do
60 is_binary(map[field]) ->
61 Map.put(map, field, [map[field]])
64 Map.put(map, field, [])
71 def fix_explicit_addressing(
72 %{"to" => to, "cc" => cc} = object,
78 |> Enum.filter(fn x -> x in explicit_mentions end)
82 |> Enum.filter(fn x -> x not in explicit_mentions end)
86 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
90 |> Map.put("to", explicit_to)
91 |> Map.put("cc", final_cc)
94 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
96 # if directMessage flag is set to true, leave the addressing alone
97 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
99 def fix_explicit_addressing(object) do
102 |> Utils.determine_explicit_mentions()
104 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
106 explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
108 fix_explicit_addressing(object, explicit_mentions, follower_collection)
111 # if as:Public is addressed, then make sure the followers collection is also addressed
112 # so that the activities will be delivered to local users.
113 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
114 recipients = to ++ cc
116 if followers_collection not in recipients do
118 Pleroma.Constants.as_public() in cc ->
119 to = to ++ [followers_collection]
120 Map.put(object, "to", to)
122 Pleroma.Constants.as_public() in to ->
123 cc = cc ++ [followers_collection]
124 Map.put(object, "cc", cc)
134 def fix_implicit_addressing(object, _), do: object
136 def fix_addressing(object) do
137 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
138 followers_collection = User.ap_followers(user)
141 |> fix_addressing_list("to")
142 |> fix_addressing_list("cc")
143 |> fix_addressing_list("bto")
144 |> fix_addressing_list("bcc")
145 |> fix_explicit_addressing()
146 |> fix_implicit_addressing(followers_collection)
149 def fix_actor(%{"attributedTo" => actor} = object) do
151 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
154 # Check for standardisation
155 # This is what Peertube does
156 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
157 # Prismo returns only an integer (count) as "likes"
158 def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
160 |> Map.put("likes", [])
161 |> Map.put("like_count", 0)
164 def fix_likes(object) do
168 def fix_in_reply_to(object, options \\ [])
170 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
171 when not is_nil(in_reply_to) do
174 is_bitstring(in_reply_to) ->
177 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
180 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
181 Enum.at(in_reply_to, 0)
183 # Maybe I should output an error too?
188 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
190 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
191 case get_obj_helper(in_reply_to_id, options) do
192 {:ok, replied_object} ->
193 with %Activity{} = _activity <-
194 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
196 |> Map.put("inReplyTo", replied_object.data["id"])
197 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
198 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
199 |> Map.put("context", replied_object.data["context"] || object["conversation"])
202 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
207 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
215 def fix_in_reply_to(object, _options), do: object
217 def fix_context(object) do
218 context = object["context"] || object["conversation"] || Utils.generate_context_id()
221 |> Map.put("context", context)
222 |> Map.put("conversation", context)
225 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
228 |> Enum.map(fn data ->
229 media_type = data["mediaType"] || data["mimeType"]
230 href = data["url"] || data["href"]
232 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
235 |> Map.put("mediaType", media_type)
236 |> Map.put("url", url)
240 |> Map.put("attachment", attachments)
243 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
244 Map.put(object, "attachment", [attachment])
248 def fix_attachments(object), do: object
250 def fix_url(%{"url" => url} = object) when is_map(url) do
252 |> Map.put("url", url["href"])
255 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
256 first_element = Enum.at(url, 0)
260 |> Enum.filter(fn x -> is_map(x) end)
261 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
265 |> Map.put("attachment", [first_element])
266 |> Map.put("url", link_element["href"])
269 def fix_url(%{"type" => object_type, "url" => url} = object)
270 when object_type != "Video" and is_list(url) do
271 first_element = Enum.at(url, 0)
275 is_bitstring(first_element) -> first_element
276 is_map(first_element) -> first_element["href"] || ""
281 |> Map.put("url", url_string)
284 def fix_url(object), do: object
286 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
287 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
291 |> Enum.reduce(%{}, fn data, mapping ->
292 name = String.trim(data["name"], ":")
294 mapping |> Map.put(name, data["icon"]["url"])
297 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
298 emoji = Map.merge(object["emoji"] || %{}, emoji)
301 |> Map.put("emoji", emoji)
304 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
305 name = String.trim(tag["name"], ":")
306 emoji = %{name => tag["icon"]["url"]}
309 |> Map.put("emoji", emoji)
312 def fix_emoji(object), do: object
314 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
317 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
318 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
320 combined = tag ++ tags
323 |> Map.put("tag", combined)
326 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
327 combined = [tag, String.slice(hashtag, 1..-1)]
330 |> Map.put("tag", combined)
333 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
335 def fix_tag(object), do: object
337 # content map usually only has one language so this will do for now.
338 def fix_content_map(%{"contentMap" => content_map} = object) do
339 content_groups = Map.to_list(content_map)
340 {_, content} = Enum.at(content_groups, 0)
343 |> Map.put("content", content)
346 def fix_content_map(object), do: object
348 def fix_type(object, options \\ [])
350 def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do
352 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
353 Object.normalize(reply_id, true)
356 if reply && (reply.data["type"] == "Question" and object["name"]) do
357 Map.put(object, "type", "Answer")
363 def fix_type(object, _), do: object
365 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
366 with true <- id =~ "follows",
367 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
368 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
375 defp mastodon_follow_hack(_, _), do: {:error, nil}
377 defp get_follow_activity(follow_object, followed) do
378 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
379 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
382 # Can't find the activity. This might a Mastodon 2.3 "Accept"
384 mastodon_follow_hack(follow_object, followed)
391 def handle_incoming(data, options \\ [])
393 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
395 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
396 with context <- data["context"] || Utils.generate_context_id(),
397 content <- data["content"] || "",
398 %User{} = actor <- User.get_cached_by_ap_id(actor),
400 # Reduce the object list to find the reported user.
402 Enum.reduce_while(objects, nil, fn ap_id, _ ->
403 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
410 # Remove the reported user from the object list.
411 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
419 "cc" => [account.ap_id]
423 ActivityPub.flag(params)
427 # disallow objects with bogus IDs
428 def handle_incoming(%{"id" => nil}, _options), do: :error
429 def handle_incoming(%{"id" => ""}, _options), do: :error
430 # length of https:// = 8, should validate better, but good enough for now.
431 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
434 # TODO: validate those with a Ecto scheme
438 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
441 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
442 actor = Containment.get_actor(data)
445 Map.put(data, "actor", actor)
448 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
449 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
450 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
451 object = fix_object(data["object"], options)
457 context: object["conversation"],
459 published: data["published"],
468 ActivityPub.create(params)
470 %Activity{} = activity -> {:ok, activity}
476 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
479 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
480 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
481 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
482 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
483 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
484 {_, false} <- {:user_locked, User.locked?(followed)},
485 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
487 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
488 ActivityPub.accept(%{
489 to: [follower.ap_id],
495 {:user_blocked, true} ->
496 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
498 ActivityPub.reject(%{
499 to: [follower.ap_id],
505 {:follow, {:error, _}} ->
506 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
508 ActivityPub.reject(%{
509 to: [follower.ap_id],
515 {:user_locked, true} ->
527 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
530 with actor <- Containment.get_actor(data),
531 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
532 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
533 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
534 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
535 {:ok, _follower} = User.follow(follower, followed) do
536 ActivityPub.accept(%{
537 to: follow_activity.data["to"],
540 object: follow_activity.data["id"],
549 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
552 with actor <- Containment.get_actor(data),
553 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
554 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
555 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
556 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
558 ActivityPub.reject(%{
559 to: follow_activity.data["to"],
562 object: follow_activity.data["id"],
565 User.unfollow(follower, followed)
574 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
577 with actor <- Containment.get_actor(data),
578 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
579 {:ok, object} <- get_obj_helper(object_id),
580 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
588 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
591 with actor <- Containment.get_actor(data),
592 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
593 {:ok, object} <- get_obj_helper(object_id),
594 public <- Visibility.is_public?(data),
595 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
603 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
607 when object_type in ["Person", "Application", "Service", "Organization"] do
608 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
609 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
611 banner = new_user_data[:info][:banner]
612 locked = new_user_data[:info][:locked] || false
616 |> Map.take([:name, :bio, :avatar])
617 |> Map.put(:info, %{banner: banner, locked: locked})
620 |> User.upgrade_changeset(update_data)
621 |> User.update_and_set_cache()
623 ActivityPub.update(%{
625 to: data["to"] || [],
626 cc: data["cc"] || [],
637 # TODO: We presently assume that any actor on the same origin domain as the object being
638 # deleted has the rights to delete that object. A better way to validate whether or not
639 # the object should be deleted is to refetch the object URI, which should return either
640 # an error or a tombstone. This would allow us to verify that a deletion actually took
643 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
646 object_id = Utils.get_ap_id(object_id)
648 with actor <- Containment.get_actor(data),
649 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
650 {:ok, object} <- get_obj_helper(object_id),
651 :ok <- Containment.contain_origin(actor.ap_id, object.data),
652 {:ok, activity} <- ActivityPub.delete(object, false) do
656 case User.get_cached_by_ap_id(object_id) do
657 %User{ap_id: ^actor} = user ->
672 "object" => %{"type" => "Announce", "object" => object_id},
678 with actor <- Containment.get_actor(data),
679 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
680 {:ok, object} <- get_obj_helper(object_id),
681 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
691 "object" => %{"type" => "Follow", "object" => followed},
697 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
698 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
699 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
700 User.unfollow(follower, followed)
710 "object" => %{"type" => "Block", "object" => blocked},
716 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
717 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
718 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
719 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
720 User.unblock(blocker, blocked)
728 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
731 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
732 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
733 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
734 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
735 User.unfollow(blocker, blocked)
736 User.block(blocker, blocked)
746 "object" => %{"type" => "Like", "object" => object_id},
752 with actor <- Containment.get_actor(data),
753 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
754 {:ok, object} <- get_obj_helper(object_id),
755 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
762 def handle_incoming(_, _), do: :error
764 def get_obj_helper(id, options \\ []) do
765 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
768 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
769 with false <- String.starts_with?(in_reply_to, "http"),
770 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
771 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
777 def set_reply_to_uri(obj), do: obj
779 # Prepares the object of an outgoing create activity.
780 def prepare_object(object) do
788 |> prepare_attachments
791 |> strip_internal_fields
792 |> strip_internal_tags
798 # internal -> Mastodon
801 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
804 |> Object.normalize()
810 |> Map.put("object", object)
811 |> Map.merge(Utils.make_json_ld_header())
817 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
818 # because of course it does.
819 def prepare_outgoing(%{"type" => "Accept"} = data) do
820 with follow_activity <- Activity.normalize(data["object"]) do
822 "actor" => follow_activity.actor,
823 "object" => follow_activity.data["object"],
824 "id" => follow_activity.data["id"],
830 |> Map.put("object", object)
831 |> Map.merge(Utils.make_json_ld_header())
837 def prepare_outgoing(%{"type" => "Reject"} = data) do
838 with follow_activity <- Activity.normalize(data["object"]) do
840 "actor" => follow_activity.actor,
841 "object" => follow_activity.data["object"],
842 "id" => follow_activity.data["id"],
848 |> Map.put("object", object)
849 |> Map.merge(Utils.make_json_ld_header())
855 def prepare_outgoing(%{"type" => _type} = data) do
858 |> strip_internal_fields
859 |> maybe_fix_object_url
860 |> Map.merge(Utils.make_json_ld_header())
865 def maybe_fix_object_url(data) do
866 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
867 case get_obj_helper(data["object"]) do
868 {:ok, relative_object} ->
869 if relative_object.data["external_url"] do
872 |> Map.put("object", relative_object.data["external_url"])
878 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
886 def add_hashtags(object) do
888 (object["tag"] || [])
890 # Expand internal representation tags into AS2 tags.
891 tag when is_binary(tag) ->
893 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
898 # Do not process tags which are already AS2 tag objects.
899 tag when is_map(tag) ->
904 |> Map.put("tag", tags)
907 def add_mention_tags(object) do
910 |> Utils.get_notified_from_object()
911 |> Enum.map(fn user ->
912 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
915 tags = object["tag"] || []
918 |> Map.put("tag", tags ++ mentions)
921 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
922 user_info = add_emoji_tags(user_info)
925 |> Map.put(:info, user_info)
928 # TODO: we should probably send mtime instead of unix epoch time for updated
929 def add_emoji_tags(%{"emoji" => emoji} = object) do
930 tags = object["tag"] || []
934 |> Enum.map(fn {name, url} ->
936 "icon" => %{"url" => url, "type" => "Image"},
937 "name" => ":" <> name <> ":",
939 "updated" => "1970-01-01T00:00:00Z",
945 |> Map.put("tag", tags ++ out)
948 def add_emoji_tags(object) do
952 def set_conversation(object) do
953 Map.put(object, "conversation", object["context"])
956 def set_sensitive(object) do
957 tags = object["tag"] || []
958 Map.put(object, "sensitive", "nsfw" in tags)
961 def set_type(%{"type" => "Answer"} = object) do
962 Map.put(object, "type", "Note")
965 def set_type(object), do: object
967 def add_attributed_to(object) do
968 attributed_to = object["attributedTo"] || object["actor"]
971 |> Map.put("attributedTo", attributed_to)
974 def add_likes(%{"id" => id, "like_count" => likes} = object) do
976 "id" => "#{id}/likes",
977 "first" => "#{id}/likes?page=1",
978 "type" => "OrderedCollection",
979 "totalItems" => likes
983 |> Map.put("likes", likes)
986 def add_likes(object) do
990 def prepare_attachments(object) do
992 (object["attachment"] || [])
993 |> Enum.map(fn data ->
994 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
995 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
999 |> Map.put("attachment", attachments)
1002 defp strip_internal_fields(object) do
1007 "announcement_count",
1010 "deleted_activity_id"
1014 defp strip_internal_tags(%{"tag" => tags} = object) do
1017 |> Enum.filter(fn x -> is_map(x) end)
1020 |> Map.put("tag", tags)
1023 defp strip_internal_tags(object), do: object
1025 def perform(:user_upgrade, user) do
1026 # we pass a fake user so that the followers collection is stripped away
1027 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1032 where: ^old_follower_address in u.following,
1037 "array_replace(?,?,?)",
1039 ^old_follower_address,
1040 ^user.follower_address
1046 Repo.update_all(q, [])
1048 maybe_retire_websub(user.ap_id)
1053 where: ^old_follower_address in a.recipients,
1058 "array_replace(?,?,?)",
1060 ^old_follower_address,
1061 ^user.follower_address
1067 Repo.update_all(q, [])
1070 def upgrade_user_from_ap_id(ap_id) do
1071 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1072 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1073 already_ap <- User.ap_enabled?(user),
1074 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1075 unless already_ap do
1076 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1081 %User{} = user -> {:ok, user}
1086 def maybe_retire_websub(ap_id) do
1087 # some sanity checks
1088 if is_binary(ap_id) && String.length(ap_id) > 8 do
1091 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1092 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1099 def maybe_fix_user_url(data) do
1100 if is_map(data["url"]) do
1101 Map.put(data, "url", data["url"]["href"])
1107 def maybe_fix_user_object(data) do
1109 |> maybe_fix_user_url