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
657 case User.get_cached_by_ap_id(object_id) do
658 %User{ap_id: ^actor} = user ->
673 "object" => %{"type" => "Announce", "object" => object_id},
679 with actor <- Containment.get_actor(data),
680 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
681 {:ok, object} <- get_obj_helper(object_id),
682 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
692 "object" => %{"type" => "Follow", "object" => followed},
698 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
699 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
700 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
701 User.unfollow(follower, followed)
711 "object" => %{"type" => "Block", "object" => blocked},
717 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
718 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
719 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
720 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
721 User.unblock(blocker, blocked)
729 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
732 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
733 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
734 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
735 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
736 User.unfollow(blocker, blocked)
737 User.block(blocker, blocked)
747 "object" => %{"type" => "Like", "object" => object_id},
753 with actor <- Containment.get_actor(data),
754 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
755 {:ok, object} <- get_obj_helper(object_id),
756 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
763 def handle_incoming(_, _), do: :error
765 def get_obj_helper(id, options \\ []) do
766 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
769 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
770 with false <- String.starts_with?(in_reply_to, "http"),
771 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
772 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
778 def set_reply_to_uri(obj), do: obj
780 # Prepares the object of an outgoing create activity.
781 def prepare_object(object) do
789 |> prepare_attachments
792 |> strip_internal_fields
793 |> strip_internal_tags
799 # internal -> Mastodon
802 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
805 |> Object.normalize()
811 |> Map.put("object", object)
812 |> Map.merge(Utils.make_json_ld_header())
818 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
819 # because of course it does.
820 def prepare_outgoing(%{"type" => "Accept"} = data) do
821 with follow_activity <- Activity.normalize(data["object"]) do
823 "actor" => follow_activity.actor,
824 "object" => follow_activity.data["object"],
825 "id" => follow_activity.data["id"],
831 |> Map.put("object", object)
832 |> Map.merge(Utils.make_json_ld_header())
838 def prepare_outgoing(%{"type" => "Reject"} = data) do
839 with follow_activity <- Activity.normalize(data["object"]) do
841 "actor" => follow_activity.actor,
842 "object" => follow_activity.data["object"],
843 "id" => follow_activity.data["id"],
849 |> Map.put("object", object)
850 |> Map.merge(Utils.make_json_ld_header())
856 def prepare_outgoing(%{"type" => _type} = data) do
859 |> strip_internal_fields
860 |> maybe_fix_object_url
861 |> Map.merge(Utils.make_json_ld_header())
866 def maybe_fix_object_url(data) do
867 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
868 case get_obj_helper(data["object"]) do
869 {:ok, relative_object} ->
870 if relative_object.data["external_url"] do
873 |> Map.put("object", relative_object.data["external_url"])
879 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
887 def add_hashtags(object) do
889 (object["tag"] || [])
891 # Expand internal representation tags into AS2 tags.
892 tag when is_binary(tag) ->
894 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
899 # Do not process tags which are already AS2 tag objects.
900 tag when is_map(tag) ->
905 |> Map.put("tag", tags)
908 def add_mention_tags(object) do
911 |> Utils.get_notified_from_object()
912 |> Enum.map(fn user ->
913 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
916 tags = object["tag"] || []
919 |> Map.put("tag", tags ++ mentions)
922 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
923 user_info = add_emoji_tags(user_info)
926 |> Map.put(:info, user_info)
929 # TODO: we should probably send mtime instead of unix epoch time for updated
930 def add_emoji_tags(%{"emoji" => emoji} = object) do
931 tags = object["tag"] || []
935 |> Enum.map(fn {name, url} ->
937 "icon" => %{"url" => url, "type" => "Image"},
938 "name" => ":" <> name <> ":",
940 "updated" => "1970-01-01T00:00:00Z",
946 |> Map.put("tag", tags ++ out)
949 def add_emoji_tags(object) do
953 def set_conversation(object) do
954 Map.put(object, "conversation", object["context"])
957 def set_sensitive(object) do
958 tags = object["tag"] || []
959 Map.put(object, "sensitive", "nsfw" in tags)
962 def set_type(%{"type" => "Answer"} = object) do
963 Map.put(object, "type", "Note")
966 def set_type(object), do: object
968 def add_attributed_to(object) do
969 attributed_to = object["attributedTo"] || object["actor"]
972 |> Map.put("attributedTo", attributed_to)
975 def add_likes(%{"id" => id, "like_count" => likes} = object) do
977 "id" => "#{id}/likes",
978 "first" => "#{id}/likes?page=1",
979 "type" => "OrderedCollection",
980 "totalItems" => likes
984 |> Map.put("likes", likes)
987 def add_likes(object) do
991 def prepare_attachments(object) do
993 (object["attachment"] || [])
994 |> Enum.map(fn data ->
995 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
996 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
1000 |> Map.put("attachment", attachments)
1003 defp strip_internal_fields(object) do
1008 "announcement_count",
1011 "deleted_activity_id"
1015 defp strip_internal_tags(%{"tag" => tags} = object) do
1018 |> Enum.filter(fn x -> is_map(x) end)
1021 |> Map.put("tag", tags)
1024 defp strip_internal_tags(object), do: object
1026 def perform(:user_upgrade, user) do
1027 # we pass a fake user so that the followers collection is stripped away
1028 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1033 where: ^old_follower_address in u.following,
1038 "array_replace(?,?,?)",
1040 ^old_follower_address,
1041 ^user.follower_address
1047 Repo.update_all(q, [])
1049 maybe_retire_websub(user.ap_id)
1054 where: ^old_follower_address in a.recipients,
1059 "array_replace(?,?,?)",
1061 ^old_follower_address,
1062 ^user.follower_address
1068 Repo.update_all(q, [])
1071 def upgrade_user_from_ap_id(ap_id) do
1072 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1073 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1074 already_ap <- User.ap_enabled?(user),
1075 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1076 unless already_ap do
1077 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1080 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1081 update_following_followers_counters(user)
1086 %User{} = user -> {:ok, user}
1091 def maybe_retire_websub(ap_id) do
1092 # some sanity checks
1093 if is_binary(ap_id) && String.length(ap_id) > 8 do
1096 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1097 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1104 def maybe_fix_user_url(data) do
1105 if is_map(data["url"]) do
1106 Map.put(data, "url", data["url"]["href"])
1112 def maybe_fix_user_object(data) do
1114 |> maybe_fix_user_url
1117 def update_following_followers_counters(user) do
1120 following = fetch_counter(user.following_address)
1121 info = if following, do: Map.put(info, :following_count, following), else: info
1123 followers = fetch_counter(user.follower_address)
1124 info = if followers, do: Map.put(info, :follower_count, followers), else: info
1126 User.set_info_cache(user, info)
1129 defp fetch_counter(url) do
1130 with {:ok, %{body: body, status: code}} when code in 200..299 <-
1133 [{:Accept, "application/activity+json"}]
1135 {:ok, data} <- Jason.decode(body) do