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
26 Modifies an incoming AP object (mastodon format) to our internal format.
28 def fix_object(object, options \\ []) do
30 |> strip_internal_fields
35 |> fix_in_reply_to(options)
44 def fix_summary(%{"summary" => nil} = object) do
46 |> Map.put("summary", "")
49 def fix_summary(%{"summary" => _} = object) do
50 # summary is present, nothing to do
54 def fix_summary(object) do
56 |> Map.put("summary", "")
59 def fix_addressing_list(map, field) do
61 is_binary(map[field]) ->
62 Map.put(map, field, [map[field]])
65 Map.put(map, field, [])
72 def fix_explicit_addressing(
73 %{"to" => to, "cc" => cc} = object,
79 |> Enum.filter(fn x -> x in explicit_mentions end)
83 |> Enum.filter(fn x -> x not in explicit_mentions end)
87 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
91 |> Map.put("to", explicit_to)
92 |> Map.put("cc", final_cc)
95 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
97 # if directMessage flag is set to true, leave the addressing alone
98 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
100 def fix_explicit_addressing(object) do
103 |> Utils.determine_explicit_mentions()
105 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
107 explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
109 fix_explicit_addressing(object, explicit_mentions, follower_collection)
112 # if as:Public is addressed, then make sure the followers collection is also addressed
113 # so that the activities will be delivered to local users.
114 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
115 recipients = to ++ cc
117 if followers_collection not in recipients do
119 Pleroma.Constants.as_public() in cc ->
120 to = to ++ [followers_collection]
121 Map.put(object, "to", to)
123 Pleroma.Constants.as_public() in to ->
124 cc = cc ++ [followers_collection]
125 Map.put(object, "cc", cc)
135 def fix_implicit_addressing(object, _), do: object
137 def fix_addressing(object) do
138 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
139 followers_collection = User.ap_followers(user)
142 |> fix_addressing_list("to")
143 |> fix_addressing_list("cc")
144 |> fix_addressing_list("bto")
145 |> fix_addressing_list("bcc")
146 |> fix_explicit_addressing()
147 |> fix_implicit_addressing(followers_collection)
150 def fix_actor(%{"attributedTo" => actor} = object) do
152 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
155 def fix_in_reply_to(object, options \\ [])
157 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
158 when not is_nil(in_reply_to) do
161 is_bitstring(in_reply_to) ->
164 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
167 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
168 Enum.at(in_reply_to, 0)
170 # Maybe I should output an error too?
175 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
177 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
178 case get_obj_helper(in_reply_to_id, options) do
179 {:ok, replied_object} ->
180 with %Activity{} = _activity <-
181 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
183 |> Map.put("inReplyTo", replied_object.data["id"])
184 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
185 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
186 |> Map.put("context", replied_object.data["context"] || object["conversation"])
189 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
194 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
202 def fix_in_reply_to(object, _options), do: object
204 def fix_context(object) do
205 context = object["context"] || object["conversation"] || Utils.generate_context_id()
208 |> Map.put("context", context)
209 |> Map.put("conversation", context)
212 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
215 |> Enum.map(fn data ->
216 media_type = data["mediaType"] || data["mimeType"]
217 href = data["url"] || data["href"]
219 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
222 |> Map.put("mediaType", media_type)
223 |> Map.put("url", url)
227 |> Map.put("attachment", attachments)
230 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
231 Map.put(object, "attachment", [attachment])
235 def fix_attachments(object), do: object
237 def fix_url(%{"url" => url} = object) when is_map(url) do
239 |> Map.put("url", url["href"])
242 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
243 first_element = Enum.at(url, 0)
247 |> Enum.filter(fn x -> is_map(x) end)
248 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
252 |> Map.put("attachment", [first_element])
253 |> Map.put("url", link_element["href"])
256 def fix_url(%{"type" => object_type, "url" => url} = object)
257 when object_type != "Video" and is_list(url) do
258 first_element = Enum.at(url, 0)
262 is_bitstring(first_element) -> first_element
263 is_map(first_element) -> first_element["href"] || ""
268 |> Map.put("url", url_string)
271 def fix_url(object), do: object
273 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
274 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
278 |> Enum.reduce(%{}, fn data, mapping ->
279 name = String.trim(data["name"], ":")
281 mapping |> Map.put(name, data["icon"]["url"])
284 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
285 emoji = Map.merge(object["emoji"] || %{}, emoji)
288 |> Map.put("emoji", emoji)
291 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
292 name = String.trim(tag["name"], ":")
293 emoji = %{name => tag["icon"]["url"]}
296 |> Map.put("emoji", emoji)
299 def fix_emoji(object), do: object
301 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
304 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
305 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
307 combined = tag ++ tags
310 |> Map.put("tag", combined)
313 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
314 combined = [tag, String.slice(hashtag, 1..-1)]
317 |> Map.put("tag", combined)
320 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
322 def fix_tag(object), do: object
324 # content map usually only has one language so this will do for now.
325 def fix_content_map(%{"contentMap" => content_map} = object) do
326 content_groups = Map.to_list(content_map)
327 {_, content} = Enum.at(content_groups, 0)
330 |> Map.put("content", content)
333 def fix_content_map(object), do: object
335 def fix_type(object, options \\ [])
337 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
338 when is_binary(reply_id) do
340 with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
341 {:ok, object} <- get_obj_helper(reply_id, options) do
345 if reply && reply.data["type"] == "Question" do
346 Map.put(object, "type", "Answer")
352 def fix_type(object, _), do: object
354 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
355 with true <- id =~ "follows",
356 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
357 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
364 defp mastodon_follow_hack(_, _), do: {:error, nil}
366 defp get_follow_activity(follow_object, followed) do
367 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
368 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
371 # Can't find the activity. This might a Mastodon 2.3 "Accept"
373 mastodon_follow_hack(follow_object, followed)
380 def handle_incoming(data, options \\ [])
382 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
384 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
385 with context <- data["context"] || Utils.generate_context_id(),
386 content <- data["content"] || "",
387 %User{} = actor <- User.get_cached_by_ap_id(actor),
389 # Reduce the object list to find the reported user.
391 Enum.reduce_while(objects, nil, fn ap_id, _ ->
392 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
399 # Remove the reported user from the object list.
400 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
408 "cc" => [account.ap_id]
412 ActivityPub.flag(params)
416 # disallow objects with bogus IDs
417 def handle_incoming(%{"id" => nil}, _options), do: :error
418 def handle_incoming(%{"id" => ""}, _options), do: :error
419 # length of https:// = 8, should validate better, but good enough for now.
420 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
423 # TODO: validate those with a Ecto scheme
427 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
430 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
431 actor = Containment.get_actor(data)
434 Map.put(data, "actor", actor)
437 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
438 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
439 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
440 object = fix_object(data["object"], options)
446 context: object["conversation"],
448 published: data["published"],
457 ActivityPub.create(params)
459 %Activity{} = activity -> {:ok, activity}
465 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
468 with %User{local: true} = followed <-
469 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
470 {:ok, %User{} = follower} <-
471 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
472 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
473 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
474 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
475 {_, false} <- {:user_locked, User.locked?(followed)},
476 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
478 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
479 ActivityPub.accept(%{
480 to: [follower.ap_id],
486 {:user_blocked, true} ->
487 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
489 ActivityPub.reject(%{
490 to: [follower.ap_id],
496 {:follow, {:error, _}} ->
497 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
499 ActivityPub.reject(%{
500 to: [follower.ap_id],
506 {:user_locked, true} ->
518 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
521 with actor <- Containment.get_actor(data),
522 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
523 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
524 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
525 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
526 {:ok, _follower} = User.follow(follower, followed) do
527 ActivityPub.accept(%{
528 to: follow_activity.data["to"],
531 object: follow_activity.data["id"],
540 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
543 with actor <- Containment.get_actor(data),
544 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
545 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
546 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
547 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
549 ActivityPub.reject(%{
550 to: follow_activity.data["to"],
553 object: follow_activity.data["id"],
556 User.unfollow(follower, followed)
565 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
568 with actor <- Containment.get_actor(data),
569 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
570 {:ok, object} <- get_obj_helper(object_id),
571 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
579 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
582 with actor <- Containment.get_actor(data),
583 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
584 {:ok, object} <- get_obj_helper(object_id),
585 public <- Visibility.is_public?(data),
586 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
594 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
598 when object_type in ["Person", "Application", "Service", "Organization"] do
599 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
600 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
602 banner = new_user_data[:info][:banner]
603 locked = new_user_data[:info][:locked] || false
604 attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
608 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
609 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
613 |> Map.take([:name, :bio, :avatar])
614 |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
617 |> User.upgrade_changeset(update_data, true)
618 |> User.update_and_set_cache()
620 ActivityPub.update(%{
622 to: data["to"] || [],
623 cc: data["cc"] || [],
634 # TODO: We presently assume that any actor on the same origin domain as the object being
635 # deleted has the rights to delete that object. A better way to validate whether or not
636 # the object should be deleted is to refetch the object URI, which should return either
637 # an error or a tombstone. This would allow us to verify that a deletion actually took
640 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
643 object_id = Utils.get_ap_id(object_id)
645 with actor <- Containment.get_actor(data),
646 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
647 {:ok, object} <- get_obj_helper(object_id),
648 :ok <- Containment.contain_origin(actor.ap_id, object.data),
649 {:ok, activity} <- ActivityPub.delete(object, false) do
653 case User.get_cached_by_ap_id(object_id) do
654 %User{ap_id: ^actor} = user ->
669 "object" => %{"type" => "Announce", "object" => object_id},
675 with actor <- Containment.get_actor(data),
676 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
677 {:ok, object} <- get_obj_helper(object_id),
678 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
688 "object" => %{"type" => "Follow", "object" => followed},
694 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
695 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
696 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
697 User.unfollow(follower, followed)
707 "object" => %{"type" => "Block", "object" => blocked},
713 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
714 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
715 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
716 User.unblock(blocker, blocked)
724 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
727 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
728 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
729 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
730 User.unfollow(blocker, blocked)
731 User.block(blocker, blocked)
741 "object" => %{"type" => "Like", "object" => object_id},
747 with actor <- Containment.get_actor(data),
748 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
749 {:ok, object} <- get_obj_helper(object_id),
750 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
757 def handle_incoming(_, _), do: :error
759 def get_obj_helper(id, options \\ []) do
760 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
763 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
764 with false <- String.starts_with?(in_reply_to, "http"),
765 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
766 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
772 def set_reply_to_uri(obj), do: obj
774 # Prepares the object of an outgoing create activity.
775 def prepare_object(object) do
782 |> prepare_attachments
785 |> strip_internal_fields
786 |> strip_internal_tags
792 # internal -> Mastodon
795 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
798 |> Object.normalize()
804 |> Map.put("object", object)
805 |> Map.merge(Utils.make_json_ld_header())
811 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
812 # because of course it does.
813 def prepare_outgoing(%{"type" => "Accept"} = data) do
814 with follow_activity <- Activity.normalize(data["object"]) do
816 "actor" => follow_activity.actor,
817 "object" => follow_activity.data["object"],
818 "id" => follow_activity.data["id"],
824 |> Map.put("object", object)
825 |> Map.merge(Utils.make_json_ld_header())
831 def prepare_outgoing(%{"type" => "Reject"} = data) do
832 with follow_activity <- Activity.normalize(data["object"]) do
834 "actor" => follow_activity.actor,
835 "object" => follow_activity.data["object"],
836 "id" => follow_activity.data["id"],
842 |> Map.put("object", object)
843 |> Map.merge(Utils.make_json_ld_header())
849 def prepare_outgoing(%{"type" => _type} = data) do
852 |> strip_internal_fields
853 |> maybe_fix_object_url
854 |> Map.merge(Utils.make_json_ld_header())
859 def maybe_fix_object_url(data) do
860 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
861 case get_obj_helper(data["object"]) do
862 {:ok, relative_object} ->
863 if relative_object.data["external_url"] do
866 |> Map.put("object", relative_object.data["external_url"])
872 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
880 def add_hashtags(object) do
882 (object["tag"] || [])
884 # Expand internal representation tags into AS2 tags.
885 tag when is_binary(tag) ->
887 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
892 # Do not process tags which are already AS2 tag objects.
893 tag when is_map(tag) ->
898 |> Map.put("tag", tags)
901 def add_mention_tags(object) do
904 |> Utils.get_notified_from_object()
905 |> Enum.map(fn user ->
906 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
909 tags = object["tag"] || []
912 |> Map.put("tag", tags ++ mentions)
915 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
916 user_info = add_emoji_tags(user_info)
919 |> Map.put(:info, user_info)
922 # TODO: we should probably send mtime instead of unix epoch time for updated
923 def add_emoji_tags(%{"emoji" => emoji} = object) do
924 tags = object["tag"] || []
928 |> Enum.map(fn {name, url} ->
930 "icon" => %{"url" => url, "type" => "Image"},
931 "name" => ":" <> name <> ":",
933 "updated" => "1970-01-01T00:00:00Z",
939 |> Map.put("tag", tags ++ out)
942 def add_emoji_tags(object) do
946 def set_conversation(object) do
947 Map.put(object, "conversation", object["context"])
950 def set_sensitive(object) do
951 tags = object["tag"] || []
952 Map.put(object, "sensitive", "nsfw" in tags)
955 def set_type(%{"type" => "Answer"} = object) do
956 Map.put(object, "type", "Note")
959 def set_type(object), do: object
961 def add_attributed_to(object) do
962 attributed_to = object["attributedTo"] || object["actor"]
965 |> Map.put("attributedTo", attributed_to)
968 def prepare_attachments(object) do
970 (object["attachment"] || [])
971 |> Enum.map(fn data ->
972 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
973 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
977 |> Map.put("attachment", attachments)
980 defp strip_internal_fields(object) do
986 "announcement_count",
989 "deleted_activity_id"
993 defp strip_internal_tags(%{"tag" => tags} = object) do
996 |> Enum.filter(fn x -> is_map(x) end)
999 |> Map.put("tag", tags)
1002 defp strip_internal_tags(object), do: object
1004 def perform(:user_upgrade, user) do
1005 # we pass a fake user so that the followers collection is stripped away
1006 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1011 where: ^old_follower_address in u.following,
1016 "array_replace(?,?,?)",
1018 ^old_follower_address,
1019 ^user.follower_address
1025 Repo.update_all(q, [])
1027 maybe_retire_websub(user.ap_id)
1032 where: ^old_follower_address in a.recipients,
1037 "array_replace(?,?,?)",
1039 ^old_follower_address,
1040 ^user.follower_address
1046 Repo.update_all(q, [])
1049 def upgrade_user_from_ap_id(ap_id) do
1050 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1051 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1052 already_ap <- User.ap_enabled?(user),
1053 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1054 unless already_ap do
1055 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1060 %User{} = user -> {:ok, user}
1065 def maybe_retire_websub(ap_id) do
1066 # some sanity checks
1067 if is_binary(ap_id) && String.length(ap_id) > 8 do
1070 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1071 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1078 def maybe_fix_user_url(data) do
1079 if is_map(data["url"]) do
1080 Map.put(data, "url", data["url"]["href"])
1086 def maybe_fix_user_object(data) do
1088 |> maybe_fix_user_url