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
24 Modifies an incoming AP object (mastodon format) to our internal format.
26 def fix_object(object, options \\ []) do
32 |> fix_in_reply_to(options)
42 def fix_summary(%{"summary" => nil} = object) do
44 |> Map.put("summary", "")
47 def fix_summary(%{"summary" => _} = object) do
48 # summary is present, nothing to do
52 def fix_summary(object) do
54 |> Map.put("summary", "")
57 def fix_addressing_list(map, field) do
59 is_binary(map[field]) ->
60 Map.put(map, field, [map[field]])
63 Map.put(map, field, [])
70 def fix_explicit_addressing(
71 %{"to" => to, "cc" => cc} = object,
77 |> Enum.filter(fn x -> x in explicit_mentions end)
81 |> Enum.filter(fn x -> x not in explicit_mentions end)
85 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
89 |> Map.put("to", explicit_to)
90 |> Map.put("cc", final_cc)
93 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
95 # if directMessage flag is set to true, leave the addressing alone
96 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
98 def fix_explicit_addressing(object) do
101 |> Utils.determine_explicit_mentions()
103 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
106 explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#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 "https://www.w3.org/ns/activitystreams#Public" in cc ->
119 to = to ++ [followers_collection]
120 Map.put(object, "to", to)
122 "https://www.w3.org/ns/activitystreams#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]),
484 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
485 {_, false} <- {:user_locked, User.locked?(followed)},
486 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
488 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
489 ActivityPub.accept(%{
490 to: [follower.ap_id],
496 {:user_blocked, true} ->
497 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
499 ActivityPub.reject(%{
500 to: [follower.ap_id],
506 {:follow, {:error, _}} ->
507 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
509 ActivityPub.reject(%{
510 to: [follower.ap_id],
516 {:user_locked, true} ->
528 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
531 with actor <- Containment.get_actor(data),
532 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
533 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
534 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
535 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
536 {:ok, _follower} = User.follow(follower, followed) do
537 ActivityPub.accept(%{
538 to: follow_activity.data["to"],
541 object: follow_activity.data["id"],
550 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
553 with actor <- Containment.get_actor(data),
554 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
555 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
556 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
557 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
559 ActivityPub.reject(%{
560 to: follow_activity.data["to"],
563 object: follow_activity.data["id"],
566 User.unfollow(follower, followed)
575 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
578 with actor <- Containment.get_actor(data),
579 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
580 {:ok, object} <- get_obj_helper(object_id),
581 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
589 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
592 with actor <- Containment.get_actor(data),
593 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
594 {:ok, object} <- get_obj_helper(object_id),
595 public <- Visibility.is_public?(data),
596 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
604 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
608 when object_type in ["Person", "Application", "Service", "Organization"] do
609 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
610 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
612 banner = new_user_data[:info]["banner"]
613 locked = new_user_data[:info]["locked"] || false
617 |> Map.take([:name, :bio, :avatar])
618 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
621 |> User.upgrade_changeset(update_data)
622 |> User.update_and_set_cache()
624 ActivityPub.update(%{
626 to: data["to"] || [],
627 cc: data["cc"] || [],
638 # TODO: We presently assume that any actor on the same origin domain as the object being
639 # deleted has the rights to delete that object. A better way to validate whether or not
640 # the object should be deleted is to refetch the object URI, which should return either
641 # an error or a tombstone. This would allow us to verify that a deletion actually took
644 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data,
647 object_id = Utils.get_ap_id(object_id)
649 with actor <- Containment.get_actor(data),
650 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
651 {:ok, object} <- get_obj_helper(object_id),
652 :ok <- Containment.contain_origin(actor.ap_id, object.data),
653 {:ok, activity} <- ActivityPub.delete(object, false) do
663 "object" => %{"type" => "Announce", "object" => object_id},
669 with actor <- Containment.get_actor(data),
670 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
671 {:ok, object} <- get_obj_helper(object_id),
672 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
682 "object" => %{"type" => "Follow", "object" => followed},
688 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
689 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
690 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
691 User.unfollow(follower, followed)
701 "object" => %{"type" => "Block", "object" => blocked},
707 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
708 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
709 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
710 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
711 User.unblock(blocker, blocked)
719 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
722 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
723 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
724 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
725 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
726 User.unfollow(blocker, blocked)
727 User.block(blocker, blocked)
737 "object" => %{"type" => "Like", "object" => object_id},
743 with actor <- Containment.get_actor(data),
744 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
745 {:ok, object} <- get_obj_helper(object_id),
746 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
753 def handle_incoming(_, _), do: :error
755 def get_obj_helper(id, options \\ []) do
756 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
759 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
760 with false <- String.starts_with?(in_reply_to, "http"),
761 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
762 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
768 def set_reply_to_uri(obj), do: obj
770 # Prepares the object of an outgoing create activity.
771 def prepare_object(object) do
779 |> prepare_attachments
782 |> strip_internal_fields
783 |> strip_internal_tags
789 # internal -> Mastodon
792 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
794 Object.normalize(object_id).data
799 |> Map.put("object", object)
800 |> Map.merge(Utils.make_json_ld_header())
805 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
806 # because of course it does.
807 def prepare_outgoing(%{"type" => "Accept"} = data) do
808 with follow_activity <- Activity.normalize(data["object"]) do
810 "actor" => follow_activity.actor,
811 "object" => follow_activity.data["object"],
812 "id" => follow_activity.data["id"],
818 |> Map.put("object", object)
819 |> Map.merge(Utils.make_json_ld_header())
825 def prepare_outgoing(%{"type" => "Reject"} = data) do
826 with follow_activity <- Activity.normalize(data["object"]) do
828 "actor" => follow_activity.actor,
829 "object" => follow_activity.data["object"],
830 "id" => follow_activity.data["id"],
836 |> Map.put("object", object)
837 |> Map.merge(Utils.make_json_ld_header())
843 def prepare_outgoing(%{"type" => _type} = data) do
846 |> strip_internal_fields
847 |> maybe_fix_object_url
848 |> Map.merge(Utils.make_json_ld_header())
853 def maybe_fix_object_url(data) do
854 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
855 case get_obj_helper(data["object"]) do
856 {:ok, relative_object} ->
857 if relative_object.data["external_url"] do
860 |> Map.put("object", relative_object.data["external_url"])
866 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
874 def add_hashtags(object) do
876 (object["tag"] || [])
878 # Expand internal representation tags into AS2 tags.
879 tag when is_binary(tag) ->
881 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
886 # Do not process tags which are already AS2 tag objects.
887 tag when is_map(tag) ->
892 |> Map.put("tag", tags)
895 def add_mention_tags(object) do
898 |> Utils.get_notified_from_object()
899 |> Enum.map(fn user ->
900 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
903 tags = object["tag"] || []
906 |> Map.put("tag", tags ++ mentions)
909 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
910 user_info = add_emoji_tags(user_info)
913 |> Map.put(:info, user_info)
916 # TODO: we should probably send mtime instead of unix epoch time for updated
917 def add_emoji_tags(%{"emoji" => emoji} = object) do
918 tags = object["tag"] || []
922 |> Enum.map(fn {name, url} ->
924 "icon" => %{"url" => url, "type" => "Image"},
925 "name" => ":" <> name <> ":",
927 "updated" => "1970-01-01T00:00:00Z",
933 |> Map.put("tag", tags ++ out)
936 def add_emoji_tags(object) do
940 def set_conversation(object) do
941 Map.put(object, "conversation", object["context"])
944 def set_sensitive(object) do
945 tags = object["tag"] || []
946 Map.put(object, "sensitive", "nsfw" in tags)
949 def set_type(%{"type" => "Answer"} = object) do
950 Map.put(object, "type", "Note")
953 def set_type(object), do: object
955 def add_attributed_to(object) do
956 attributed_to = object["attributedTo"] || object["actor"]
959 |> Map.put("attributedTo", attributed_to)
962 def add_likes(%{"id" => id, "like_count" => likes} = object) do
964 "id" => "#{id}/likes",
965 "first" => "#{id}/likes?page=1",
966 "type" => "OrderedCollection",
967 "totalItems" => likes
971 |> Map.put("likes", likes)
974 def add_likes(object) do
978 def prepare_attachments(object) do
980 (object["attachment"] || [])
981 |> Enum.map(fn data ->
982 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
983 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
987 |> Map.put("attachment", attachments)
990 defp strip_internal_fields(object) do
995 "announcement_count",
998 "deleted_activity_id"
1002 defp strip_internal_tags(%{"tag" => tags} = object) do
1005 |> Enum.filter(fn x -> is_map(x) end)
1008 |> Map.put("tag", tags)
1011 defp strip_internal_tags(object), do: object
1013 def perform(:user_upgrade, user) do
1014 # we pass a fake user so that the followers collection is stripped away
1015 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1020 where: ^old_follower_address in u.following,
1025 "array_replace(?,?,?)",
1027 ^old_follower_address,
1028 ^user.follower_address
1034 Repo.update_all(q, [])
1036 maybe_retire_websub(user.ap_id)
1041 where: ^old_follower_address in a.recipients,
1046 "array_replace(?,?,?)",
1048 ^old_follower_address,
1049 ^user.follower_address
1055 Repo.update_all(q, [])
1058 def upgrade_user_from_ap_id(ap_id) do
1059 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1060 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1061 already_ap <- User.ap_enabled?(user),
1062 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1063 unless already_ap do
1064 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1069 %User{} = user -> {:ok, user}
1074 def maybe_retire_websub(ap_id) do
1075 # some sanity checks
1076 if is_binary(ap_id) && String.length(ap_id) > 8 do
1079 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1080 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1087 def maybe_fix_user_url(data) do
1088 if is_map(data["url"]) do
1089 Map.put(data, "url", data["url"]["href"])
1095 def maybe_fix_user_object(data) do
1097 |> maybe_fix_user_url