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 (options[:depth] || 1) <= Federator.max_replies_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 (options[:depth] || 1) <= Federator.max_replies_depth() do
353 Object.normalize(reply_id, true)
358 if reply && (reply.data["type"] == "Question" and object["name"]) do
359 Map.put(object, "type", "Answer")
365 def fix_type(object, _), do: object
367 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
368 with true <- id =~ "follows",
369 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
370 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
377 defp mastodon_follow_hack(_, _), do: {:error, nil}
379 defp get_follow_activity(follow_object, followed) do
380 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
381 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
384 # Can't find the activity. This might a Mastodon 2.3 "Accept"
386 mastodon_follow_hack(follow_object, followed)
393 def handle_incoming(data, options \\ [])
395 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
397 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
398 with context <- data["context"] || Utils.generate_context_id(),
399 content <- data["content"] || "",
400 %User{} = actor <- User.get_cached_by_ap_id(actor),
402 # Reduce the object list to find the reported user.
404 Enum.reduce_while(objects, nil, fn ap_id, _ ->
405 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
412 # Remove the reported user from the object list.
413 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
421 "cc" => [account.ap_id]
425 ActivityPub.flag(params)
429 # disallow objects with bogus IDs
430 def handle_incoming(%{"id" => nil}, _options), do: :error
431 def handle_incoming(%{"id" => ""}, _options), do: :error
432 # length of https:// = 8, should validate better, but good enough for now.
433 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
436 # TODO: validate those with a Ecto scheme
440 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
443 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
444 actor = Containment.get_actor(data)
447 Map.put(data, "actor", actor)
450 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
451 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
452 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
453 object = fix_object(data["object"], options)
459 context: object["conversation"],
461 published: data["published"],
470 ActivityPub.create(params)
472 %Activity{} = activity -> {:ok, activity}
478 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
481 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
482 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
483 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
484 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
486 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
487 {_, false} <- {:user_locked, User.locked?(followed)},
488 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
490 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
491 ActivityPub.accept(%{
492 to: [follower.ap_id],
498 {:user_blocked, true} ->
499 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
501 ActivityPub.reject(%{
502 to: [follower.ap_id],
508 {:follow, {:error, _}} ->
509 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
511 ActivityPub.reject(%{
512 to: [follower.ap_id],
518 {:user_locked, true} ->
530 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
533 with actor <- Containment.get_actor(data),
534 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
535 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
536 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
537 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
538 {:ok, _follower} = User.follow(follower, followed) do
539 ActivityPub.accept(%{
540 to: follow_activity.data["to"],
543 object: follow_activity.data["id"],
552 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
555 with actor <- Containment.get_actor(data),
556 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
557 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
558 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
559 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
561 ActivityPub.reject(%{
562 to: follow_activity.data["to"],
565 object: follow_activity.data["id"],
568 User.unfollow(follower, followed)
577 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
580 with actor <- Containment.get_actor(data),
581 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
582 {:ok, object} <- get_obj_helper(object_id),
583 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
591 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
594 with actor <- Containment.get_actor(data),
595 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
596 {:ok, object} <- get_obj_helper(object_id),
597 public <- Visibility.is_public?(data),
598 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
606 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
610 when object_type in ["Person", "Application", "Service", "Organization"] do
611 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
612 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
614 banner = new_user_data[:info]["banner"]
615 locked = new_user_data[:info]["locked"] || false
619 |> Map.take([:name, :bio, :avatar])
620 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
623 |> User.upgrade_changeset(update_data)
624 |> User.update_and_set_cache()
626 ActivityPub.update(%{
628 to: data["to"] || [],
629 cc: data["cc"] || [],
640 # TODO: We presently assume that any actor on the same origin domain as the object being
641 # deleted has the rights to delete that object. A better way to validate whether or not
642 # the object should be deleted is to refetch the object URI, which should return either
643 # an error or a tombstone. This would allow us to verify that a deletion actually took
646 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data,
649 object_id = Utils.get_ap_id(object_id)
651 with actor <- Containment.get_actor(data),
652 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
653 {:ok, object} <- get_obj_helper(object_id),
654 :ok <- Containment.contain_origin(actor.ap_id, object.data),
655 {:ok, activity} <- ActivityPub.delete(object, false) do
665 "object" => %{"type" => "Announce", "object" => object_id},
671 with actor <- Containment.get_actor(data),
672 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
673 {:ok, object} <- get_obj_helper(object_id),
674 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
684 "object" => %{"type" => "Follow", "object" => followed},
690 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
691 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
692 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
693 User.unfollow(follower, followed)
703 "object" => %{"type" => "Block", "object" => blocked},
709 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
710 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
711 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
712 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
713 User.unblock(blocker, blocked)
721 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
724 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
725 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
726 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
727 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
728 User.unfollow(blocker, blocked)
729 User.block(blocker, blocked)
739 "object" => %{"type" => "Like", "object" => object_id},
745 with actor <- Containment.get_actor(data),
746 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
747 {:ok, object} <- get_obj_helper(object_id),
748 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
755 def handle_incoming(_, _), do: :error
757 def get_obj_helper(id, options \\ []) do
758 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
761 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
762 with false <- String.starts_with?(in_reply_to, "http"),
763 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
764 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
770 def set_reply_to_uri(obj), do: obj
772 # Prepares the object of an outgoing create activity.
773 def prepare_object(object) do
781 |> prepare_attachments
784 |> strip_internal_fields
785 |> strip_internal_tags
791 # internal -> Mastodon
794 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
796 Object.normalize(object_id).data
801 |> Map.put("object", object)
802 |> Map.merge(Utils.make_json_ld_header())
807 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
808 # because of course it does.
809 def prepare_outgoing(%{"type" => "Accept"} = data) do
810 with follow_activity <- Activity.normalize(data["object"]) do
812 "actor" => follow_activity.actor,
813 "object" => follow_activity.data["object"],
814 "id" => follow_activity.data["id"],
820 |> Map.put("object", object)
821 |> Map.merge(Utils.make_json_ld_header())
827 def prepare_outgoing(%{"type" => "Reject"} = data) do
828 with follow_activity <- Activity.normalize(data["object"]) do
830 "actor" => follow_activity.actor,
831 "object" => follow_activity.data["object"],
832 "id" => follow_activity.data["id"],
838 |> Map.put("object", object)
839 |> Map.merge(Utils.make_json_ld_header())
845 def prepare_outgoing(%{"type" => _type} = data) do
848 |> strip_internal_fields
849 |> maybe_fix_object_url
850 |> Map.merge(Utils.make_json_ld_header())
855 def maybe_fix_object_url(data) do
856 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
857 case get_obj_helper(data["object"]) do
858 {:ok, relative_object} ->
859 if relative_object.data["external_url"] do
862 |> Map.put("object", relative_object.data["external_url"])
868 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
876 def add_hashtags(object) do
878 (object["tag"] || [])
880 # Expand internal representation tags into AS2 tags.
881 tag when is_binary(tag) ->
883 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
888 # Do not process tags which are already AS2 tag objects.
889 tag when is_map(tag) ->
894 |> Map.put("tag", tags)
897 def add_mention_tags(object) do
900 |> Utils.get_notified_from_object()
901 |> Enum.map(fn user ->
902 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
905 tags = object["tag"] || []
908 |> Map.put("tag", tags ++ mentions)
911 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
912 user_info = add_emoji_tags(user_info)
915 |> Map.put(:info, user_info)
918 # TODO: we should probably send mtime instead of unix epoch time for updated
919 def add_emoji_tags(%{"emoji" => emoji} = object) do
920 tags = object["tag"] || []
924 |> Enum.map(fn {name, url} ->
926 "icon" => %{"url" => url, "type" => "Image"},
927 "name" => ":" <> name <> ":",
929 "updated" => "1970-01-01T00:00:00Z",
935 |> Map.put("tag", tags ++ out)
938 def add_emoji_tags(object) do
942 def set_conversation(object) do
943 Map.put(object, "conversation", object["context"])
946 def set_sensitive(object) do
947 tags = object["tag"] || []
948 Map.put(object, "sensitive", "nsfw" in tags)
951 def set_type(%{"type" => "Answer"} = object) do
952 Map.put(object, "type", "Note")
955 def set_type(object), do: object
957 def add_attributed_to(object) do
958 attributed_to = object["attributedTo"] || object["actor"]
961 |> Map.put("attributedTo", attributed_to)
964 def add_likes(%{"id" => id, "like_count" => likes} = object) do
966 "id" => "#{id}/likes",
967 "first" => "#{id}/likes?page=1",
968 "type" => "OrderedCollection",
969 "totalItems" => likes
973 |> Map.put("likes", likes)
976 def add_likes(object) do
980 def prepare_attachments(object) do
982 (object["attachment"] || [])
983 |> Enum.map(fn data ->
984 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
985 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
989 |> Map.put("attachment", attachments)
992 defp strip_internal_fields(object) do
997 "announcement_count",
1000 "deleted_activity_id"
1004 defp strip_internal_tags(%{"tag" => tags} = object) do
1007 |> Enum.filter(fn x -> is_map(x) end)
1010 |> Map.put("tag", tags)
1013 defp strip_internal_tags(object), do: object
1015 def perform(:user_upgrade, user) do
1016 # we pass a fake user so that the followers collection is stripped away
1017 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1022 where: ^old_follower_address in u.following,
1027 "array_replace(?,?,?)",
1029 ^old_follower_address,
1030 ^user.follower_address
1036 Repo.update_all(q, [])
1038 maybe_retire_websub(user.ap_id)
1043 where: ^old_follower_address in a.recipients,
1048 "array_replace(?,?,?)",
1050 ^old_follower_address,
1051 ^user.follower_address
1057 Repo.update_all(q, [])
1060 def upgrade_user_from_ap_id(ap_id) do
1061 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1062 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1063 already_ap <- User.ap_enabled?(user),
1064 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1065 unless already_ap do
1066 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1071 %User{} = user -> {:ok, user}
1076 def maybe_retire_websub(ap_id) do
1077 # some sanity checks
1078 if is_binary(ap_id) && String.length(ap_id) > 8 do
1081 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1082 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1089 def maybe_fix_user_url(data) do
1090 if is_map(data["url"]) do
1091 Map.put(data, "url", data["url"]["href"])
1097 def maybe_fix_user_object(data) do
1099 |> maybe_fix_user_url