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
18 alias Pleroma.Workers.TransmogrifierWorker
23 require Pleroma.Constants
25 import Pleroma.Workers.WorkerHelper, only: [worker_args: 1]
28 Modifies an incoming AP object (mastodon format) to our internal format.
30 def fix_object(object, options \\ []) do
32 |> strip_internal_fields
37 |> fix_in_reply_to(options)
46 def fix_summary(%{"summary" => nil} = object) do
48 |> Map.put("summary", "")
51 def fix_summary(%{"summary" => _} = object) do
52 # summary is present, nothing to do
56 def fix_summary(object) do
58 |> Map.put("summary", "")
61 def fix_addressing_list(map, field) do
63 is_binary(map[field]) ->
64 Map.put(map, field, [map[field]])
67 Map.put(map, field, [])
74 def fix_explicit_addressing(
75 %{"to" => to, "cc" => cc} = object,
81 |> Enum.filter(fn x -> x in explicit_mentions end)
85 |> Enum.filter(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
105 |> Utils.determine_explicit_mentions()
107 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
109 explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
111 fix_explicit_addressing(object, explicit_mentions, follower_collection)
114 # if as:Public is addressed, then make sure the followers collection is also addressed
115 # so that the activities will be delivered to local users.
116 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
117 recipients = to ++ cc
119 if followers_collection not in recipients do
121 Pleroma.Constants.as_public() in cc ->
122 to = to ++ [followers_collection]
123 Map.put(object, "to", to)
125 Pleroma.Constants.as_public() in to ->
126 cc = cc ++ [followers_collection]
127 Map.put(object, "cc", cc)
137 def fix_implicit_addressing(object, _), do: object
139 def fix_addressing(object) do
140 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
141 followers_collection = User.ap_followers(user)
144 |> fix_addressing_list("to")
145 |> fix_addressing_list("cc")
146 |> fix_addressing_list("bto")
147 |> fix_addressing_list("bcc")
148 |> fix_explicit_addressing()
149 |> fix_implicit_addressing(followers_collection)
152 def fix_actor(%{"attributedTo" => actor} = object) do
154 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
157 def fix_in_reply_to(object, options \\ [])
159 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
160 when not is_nil(in_reply_to) do
163 is_bitstring(in_reply_to) ->
166 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
169 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
170 Enum.at(in_reply_to, 0)
172 # Maybe I should output an error too?
177 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
179 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
180 case get_obj_helper(in_reply_to_id, options) do
181 {:ok, replied_object} ->
182 with %Activity{} = _activity <-
183 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
185 |> Map.put("inReplyTo", replied_object.data["id"])
186 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
187 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
188 |> Map.put("context", replied_object.data["context"] || object["conversation"])
191 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
196 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
204 def fix_in_reply_to(object, _options), do: object
206 def fix_context(object) do
207 context = object["context"] || object["conversation"] || Utils.generate_context_id()
210 |> Map.put("context", context)
211 |> Map.put("conversation", context)
214 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
217 |> Enum.map(fn data ->
218 media_type = data["mediaType"] || data["mimeType"]
219 href = data["url"] || data["href"]
221 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
224 |> Map.put("mediaType", media_type)
225 |> Map.put("url", url)
229 |> Map.put("attachment", attachments)
232 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
233 Map.put(object, "attachment", [attachment])
237 def fix_attachments(object), do: object
239 def fix_url(%{"url" => url} = object) when is_map(url) do
241 |> Map.put("url", url["href"])
244 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
245 first_element = Enum.at(url, 0)
249 |> Enum.filter(fn x -> is_map(x) end)
250 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
254 |> Map.put("attachment", [first_element])
255 |> Map.put("url", link_element["href"])
258 def fix_url(%{"type" => object_type, "url" => url} = object)
259 when object_type != "Video" and is_list(url) do
260 first_element = Enum.at(url, 0)
264 is_bitstring(first_element) -> first_element
265 is_map(first_element) -> first_element["href"] || ""
270 |> Map.put("url", url_string)
273 def fix_url(object), do: object
275 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
276 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
280 |> Enum.reduce(%{}, fn data, mapping ->
281 name = String.trim(data["name"], ":")
283 mapping |> Map.put(name, data["icon"]["url"])
286 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
287 emoji = Map.merge(object["emoji"] || %{}, emoji)
290 |> Map.put("emoji", emoji)
293 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
294 name = String.trim(tag["name"], ":")
295 emoji = %{name => tag["icon"]["url"]}
298 |> Map.put("emoji", emoji)
301 def fix_emoji(object), do: object
303 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
306 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
307 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
309 combined = tag ++ tags
312 |> Map.put("tag", combined)
315 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
316 combined = [tag, String.slice(hashtag, 1..-1)]
319 |> Map.put("tag", combined)
322 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
324 def fix_tag(object), do: object
326 # content map usually only has one language so this will do for now.
327 def fix_content_map(%{"contentMap" => content_map} = object) do
328 content_groups = Map.to_list(content_map)
329 {_, content} = Enum.at(content_groups, 0)
332 |> Map.put("content", content)
335 def fix_content_map(object), do: object
337 def fix_type(object, options \\ [])
339 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
340 when is_binary(reply_id) do
342 with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
343 {:ok, object} <- get_obj_helper(reply_id, options) do
347 if reply && reply.data["type"] == "Question" do
348 Map.put(object, "type", "Answer")
354 def fix_type(object, _), do: object
356 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
357 with true <- id =~ "follows",
358 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
359 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
366 defp mastodon_follow_hack(_, _), do: {:error, nil}
368 defp get_follow_activity(follow_object, followed) do
369 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
370 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
373 # Can't find the activity. This might a Mastodon 2.3 "Accept"
375 mastodon_follow_hack(follow_object, followed)
382 def handle_incoming(data, options \\ [])
384 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
386 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
387 with context <- data["context"] || Utils.generate_context_id(),
388 content <- data["content"] || "",
389 %User{} = actor <- User.get_cached_by_ap_id(actor),
391 # Reduce the object list to find the reported user.
393 Enum.reduce_while(objects, nil, fn ap_id, _ ->
394 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
401 # Remove the reported user from the object list.
402 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
410 "cc" => [account.ap_id]
414 ActivityPub.flag(params)
418 # disallow objects with bogus IDs
419 def handle_incoming(%{"id" => nil}, _options), do: :error
420 def handle_incoming(%{"id" => ""}, _options), do: :error
421 # length of https:// = 8, should validate better, but good enough for now.
422 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
425 # TODO: validate those with a Ecto scheme
429 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
432 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
433 actor = Containment.get_actor(data)
436 Map.put(data, "actor", actor)
439 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
440 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
441 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
442 object = fix_object(data["object"], options)
448 context: object["conversation"],
450 published: data["published"],
459 ActivityPub.create(params)
461 %Activity{} = activity -> {:ok, activity}
467 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
470 with %User{local: true} = followed <-
471 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
472 {:ok, %User{} = follower} <-
473 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
474 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
475 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
476 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
477 {_, false} <- {:user_locked, User.locked?(followed)},
478 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
480 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
481 ActivityPub.accept(%{
482 to: [follower.ap_id],
488 {:user_blocked, true} ->
489 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
491 ActivityPub.reject(%{
492 to: [follower.ap_id],
498 {:follow, {:error, _}} ->
499 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
501 ActivityPub.reject(%{
502 to: [follower.ap_id],
508 {:user_locked, true} ->
520 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
523 with actor <- Containment.get_actor(data),
524 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
525 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
526 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
527 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
528 {:ok, _follower} = User.follow(follower, followed) do
529 ActivityPub.accept(%{
530 to: follow_activity.data["to"],
533 object: follow_activity.data["id"],
542 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
545 with actor <- Containment.get_actor(data),
546 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
547 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
548 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
549 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
551 ActivityPub.reject(%{
552 to: follow_activity.data["to"],
555 object: follow_activity.data["id"],
558 User.unfollow(follower, followed)
567 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
570 with actor <- Containment.get_actor(data),
571 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
572 {:ok, object} <- get_obj_helper(object_id),
573 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
581 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
584 with actor <- Containment.get_actor(data),
585 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
586 {:ok, object} <- get_obj_helper(object_id),
587 public <- Visibility.is_public?(data),
588 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
596 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
600 when object_type in ["Person", "Application", "Service", "Organization"] do
601 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
602 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
604 banner = new_user_data[:info][:banner]
605 locked = new_user_data[:info][:locked] || false
606 attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
610 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
611 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
615 |> Map.take([:name, :bio, :avatar])
616 |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
619 |> User.upgrade_changeset(update_data, true)
620 |> User.update_and_set_cache()
622 ActivityPub.update(%{
624 to: data["to"] || [],
625 cc: data["cc"] || [],
636 # TODO: We presently assume that any actor on the same origin domain as the object being
637 # deleted has the rights to delete that object. A better way to validate whether or not
638 # the object should be deleted is to refetch the object URI, which should return either
639 # an error or a tombstone. This would allow us to verify that a deletion actually took
642 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
645 object_id = Utils.get_ap_id(object_id)
647 with actor <- Containment.get_actor(data),
648 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
649 {:ok, object} <- get_obj_helper(object_id),
650 :ok <- Containment.contain_origin(actor.ap_id, object.data),
651 {:ok, activity} <- ActivityPub.delete(object, false) do
655 case User.get_cached_by_ap_id(object_id) do
656 %User{ap_id: ^actor} = user ->
671 "object" => %{"type" => "Announce", "object" => object_id},
677 with actor <- Containment.get_actor(data),
678 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
679 {:ok, object} <- get_obj_helper(object_id),
680 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
690 "object" => %{"type" => "Follow", "object" => followed},
696 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
697 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
698 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
699 User.unfollow(follower, followed)
709 "object" => %{"type" => "Block", "object" => blocked},
715 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
716 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
717 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
718 User.unblock(blocker, blocked)
726 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
729 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
730 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
731 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
732 User.unfollow(blocker, blocked)
733 User.block(blocker, blocked)
743 "object" => %{"type" => "Like", "object" => object_id},
749 with actor <- Containment.get_actor(data),
750 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
751 {:ok, object} <- get_obj_helper(object_id),
752 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
759 def handle_incoming(_, _), do: :error
761 def get_obj_helper(id, options \\ []) do
762 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
765 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
766 with false <- String.starts_with?(in_reply_to, "http"),
767 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
768 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
774 def set_reply_to_uri(obj), do: obj
776 # Prepares the object of an outgoing create activity.
777 def prepare_object(object) do
784 |> prepare_attachments
787 |> strip_internal_fields
788 |> strip_internal_tags
794 # internal -> Mastodon
797 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
800 |> Object.normalize()
806 |> Map.put("object", object)
807 |> Map.merge(Utils.make_json_ld_header())
813 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
814 # because of course it does.
815 def prepare_outgoing(%{"type" => "Accept"} = data) do
816 with follow_activity <- Activity.normalize(data["object"]) do
818 "actor" => follow_activity.actor,
819 "object" => follow_activity.data["object"],
820 "id" => follow_activity.data["id"],
826 |> Map.put("object", object)
827 |> Map.merge(Utils.make_json_ld_header())
833 def prepare_outgoing(%{"type" => "Reject"} = data) do
834 with follow_activity <- Activity.normalize(data["object"]) do
836 "actor" => follow_activity.actor,
837 "object" => follow_activity.data["object"],
838 "id" => follow_activity.data["id"],
844 |> Map.put("object", object)
845 |> Map.merge(Utils.make_json_ld_header())
851 def prepare_outgoing(%{"type" => _type} = data) do
854 |> strip_internal_fields
855 |> maybe_fix_object_url
856 |> Map.merge(Utils.make_json_ld_header())
861 def maybe_fix_object_url(data) do
862 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
863 case get_obj_helper(data["object"]) do
864 {:ok, relative_object} ->
865 if relative_object.data["external_url"] do
868 |> Map.put("object", relative_object.data["external_url"])
874 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
882 def add_hashtags(object) do
884 (object["tag"] || [])
886 # Expand internal representation tags into AS2 tags.
887 tag when is_binary(tag) ->
889 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
894 # Do not process tags which are already AS2 tag objects.
895 tag when is_map(tag) ->
900 |> Map.put("tag", tags)
903 def add_mention_tags(object) do
906 |> Utils.get_notified_from_object()
907 |> Enum.map(fn user ->
908 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
911 tags = object["tag"] || []
914 |> Map.put("tag", tags ++ mentions)
917 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
918 user_info = add_emoji_tags(user_info)
921 |> Map.put(:info, user_info)
924 # TODO: we should probably send mtime instead of unix epoch time for updated
925 def add_emoji_tags(%{"emoji" => emoji} = object) do
926 tags = object["tag"] || []
930 |> Enum.map(fn {name, url} ->
932 "icon" => %{"url" => url, "type" => "Image"},
933 "name" => ":" <> name <> ":",
935 "updated" => "1970-01-01T00:00:00Z",
941 |> Map.put("tag", tags ++ out)
944 def add_emoji_tags(object) do
948 def set_conversation(object) do
949 Map.put(object, "conversation", object["context"])
952 def set_sensitive(object) do
953 tags = object["tag"] || []
954 Map.put(object, "sensitive", "nsfw" in tags)
957 def set_type(%{"type" => "Answer"} = object) do
958 Map.put(object, "type", "Note")
961 def set_type(object), do: object
963 def add_attributed_to(object) do
964 attributed_to = object["attributedTo"] || object["actor"]
967 |> Map.put("attributedTo", attributed_to)
970 def prepare_attachments(object) do
972 (object["attachment"] || [])
973 |> Enum.map(fn data ->
974 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
975 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
979 |> Map.put("attachment", attachments)
982 defp strip_internal_fields(object) do
988 "announcement_count",
991 "deleted_activity_id"
995 defp strip_internal_tags(%{"tag" => tags} = object) do
998 |> Enum.filter(fn x -> is_map(x) end)
1001 |> Map.put("tag", tags)
1004 defp strip_internal_tags(object), do: object
1006 def perform(:user_upgrade, user) do
1007 # we pass a fake user so that the followers collection is stripped away
1008 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1013 where: ^old_follower_address in u.following,
1018 "array_replace(?,?,?)",
1020 ^old_follower_address,
1021 ^user.follower_address
1027 Repo.update_all(q, [])
1029 maybe_retire_websub(user.ap_id)
1034 where: ^old_follower_address in a.recipients,
1039 "array_replace(?,?,?)",
1041 ^old_follower_address,
1042 ^user.follower_address
1048 Repo.update_all(q, [])
1051 def upgrade_user_from_ap_id(ap_id) do
1052 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1053 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1054 already_ap <- User.ap_enabled?(user),
1055 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1056 unless already_ap do
1057 %{"op" => "user_upgrade", "user_id" => user.id}
1058 |> TransmogrifierWorker.new(worker_args(:transmogrifier))
1064 %User{} = user -> {:ok, user}
1069 def maybe_retire_websub(ap_id) do
1070 # some sanity checks
1071 if is_binary(ap_id) && String.length(ap_id) > 8 do
1074 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1075 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1082 def maybe_fix_user_url(data) do
1083 if is_map(data["url"]) do
1084 Map.put(data, "url", data["url"]["href"])
1090 def maybe_fix_user_object(data) do
1092 |> maybe_fix_user_url