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 ->
659 {:ok, followers} = User.get_followers(user)
661 Enum.each(followers, fn follower ->
662 User.unfollow(follower, user)
665 {:ok, friends} = User.get_friends(user)
667 Enum.each(friends, fn followed ->
668 User.unfollow(user, followed)
671 User.invalidate_cache(user)
686 "object" => %{"type" => "Announce", "object" => object_id},
692 with actor <- Containment.get_actor(data),
693 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
694 {:ok, object} <- get_obj_helper(object_id),
695 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
705 "object" => %{"type" => "Follow", "object" => followed},
711 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
712 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
713 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
714 User.unfollow(follower, followed)
724 "object" => %{"type" => "Block", "object" => blocked},
730 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
731 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
732 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
733 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
734 User.unblock(blocker, blocked)
742 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
745 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
746 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
747 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
748 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
749 User.unfollow(blocker, blocked)
750 User.block(blocker, blocked)
760 "object" => %{"type" => "Like", "object" => object_id},
766 with actor <- Containment.get_actor(data),
767 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
768 {:ok, object} <- get_obj_helper(object_id),
769 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
776 def handle_incoming(_, _), do: :error
778 def get_obj_helper(id, options \\ []) do
779 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
782 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
783 with false <- String.starts_with?(in_reply_to, "http"),
784 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
785 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
791 def set_reply_to_uri(obj), do: obj
793 # Prepares the object of an outgoing create activity.
794 def prepare_object(object) do
802 |> prepare_attachments
805 |> strip_internal_fields
806 |> strip_internal_tags
812 # internal -> Mastodon
815 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
817 Object.normalize(object_id).data
822 |> Map.put("object", object)
823 |> Map.merge(Utils.make_json_ld_header())
828 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
829 # because of course it does.
830 def prepare_outgoing(%{"type" => "Accept"} = data) do
831 with follow_activity <- Activity.normalize(data["object"]) do
833 "actor" => follow_activity.actor,
834 "object" => follow_activity.data["object"],
835 "id" => follow_activity.data["id"],
841 |> Map.put("object", object)
842 |> Map.merge(Utils.make_json_ld_header())
848 def prepare_outgoing(%{"type" => "Reject"} = data) do
849 with follow_activity <- Activity.normalize(data["object"]) do
851 "actor" => follow_activity.actor,
852 "object" => follow_activity.data["object"],
853 "id" => follow_activity.data["id"],
859 |> Map.put("object", object)
860 |> Map.merge(Utils.make_json_ld_header())
866 def prepare_outgoing(%{"type" => _type} = data) do
869 |> strip_internal_fields
870 |> maybe_fix_object_url
871 |> Map.merge(Utils.make_json_ld_header())
876 def maybe_fix_object_url(data) do
877 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
878 case get_obj_helper(data["object"]) do
879 {:ok, relative_object} ->
880 if relative_object.data["external_url"] do
883 |> Map.put("object", relative_object.data["external_url"])
889 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
897 def add_hashtags(object) do
899 (object["tag"] || [])
901 # Expand internal representation tags into AS2 tags.
902 tag when is_binary(tag) ->
904 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
909 # Do not process tags which are already AS2 tag objects.
910 tag when is_map(tag) ->
915 |> Map.put("tag", tags)
918 def add_mention_tags(object) do
921 |> Utils.get_notified_from_object()
922 |> Enum.map(fn user ->
923 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
926 tags = object["tag"] || []
929 |> Map.put("tag", tags ++ mentions)
932 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
933 user_info = add_emoji_tags(user_info)
936 |> Map.put(:info, user_info)
939 # TODO: we should probably send mtime instead of unix epoch time for updated
940 def add_emoji_tags(%{"emoji" => emoji} = object) do
941 tags = object["tag"] || []
945 |> Enum.map(fn {name, url} ->
947 "icon" => %{"url" => url, "type" => "Image"},
948 "name" => ":" <> name <> ":",
950 "updated" => "1970-01-01T00:00:00Z",
956 |> Map.put("tag", tags ++ out)
959 def add_emoji_tags(object) do
963 def set_conversation(object) do
964 Map.put(object, "conversation", object["context"])
967 def set_sensitive(object) do
968 tags = object["tag"] || []
969 Map.put(object, "sensitive", "nsfw" in tags)
972 def set_type(%{"type" => "Answer"} = object) do
973 Map.put(object, "type", "Note")
976 def set_type(object), do: object
978 def add_attributed_to(object) do
979 attributed_to = object["attributedTo"] || object["actor"]
982 |> Map.put("attributedTo", attributed_to)
985 def add_likes(%{"id" => id, "like_count" => likes} = object) do
987 "id" => "#{id}/likes",
988 "first" => "#{id}/likes?page=1",
989 "type" => "OrderedCollection",
990 "totalItems" => likes
994 |> Map.put("likes", likes)
997 def add_likes(object) do
1001 def prepare_attachments(object) do
1003 (object["attachment"] || [])
1004 |> Enum.map(fn data ->
1005 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1006 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
1010 |> Map.put("attachment", attachments)
1013 defp strip_internal_fields(object) do
1018 "announcement_count",
1021 "deleted_activity_id"
1025 defp strip_internal_tags(%{"tag" => tags} = object) do
1028 |> Enum.filter(fn x -> is_map(x) end)
1031 |> Map.put("tag", tags)
1034 defp strip_internal_tags(object), do: object
1036 def perform(:user_upgrade, user) do
1037 # we pass a fake user so that the followers collection is stripped away
1038 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1043 where: ^old_follower_address in u.following,
1048 "array_replace(?,?,?)",
1050 ^old_follower_address,
1051 ^user.follower_address
1057 Repo.update_all(q, [])
1059 maybe_retire_websub(user.ap_id)
1064 where: ^old_follower_address in a.recipients,
1069 "array_replace(?,?,?)",
1071 ^old_follower_address,
1072 ^user.follower_address
1078 Repo.update_all(q, [])
1081 def upgrade_user_from_ap_id(ap_id) do
1082 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1083 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1084 already_ap <- User.ap_enabled?(user),
1085 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1086 unless already_ap do
1087 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1090 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1091 update_following_followers_counters(user)
1096 %User{} = user -> {:ok, user}
1101 def maybe_retire_websub(ap_id) do
1102 # some sanity checks
1103 if is_binary(ap_id) && String.length(ap_id) > 8 do
1106 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1107 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1114 def maybe_fix_user_url(data) do
1115 if is_map(data["url"]) do
1116 Map.put(data, "url", data["url"]["href"])
1122 def maybe_fix_user_object(data) do
1124 |> maybe_fix_user_url
1127 def update_following_followers_counters(user) do
1130 following = fetch_counter(user.following_address)
1131 info = if following, do: Map.put(info, :following_count, following), else: info
1133 followers = fetch_counter(user.follower_address)
1134 info = if followers, do: Map.put(info, :follower_count, followers), else: info
1136 User.set_info_cache(user, info)
1139 defp fetch_counter(url) do
1140 with {:ok, %{body: body, status: code}} when code in 200..299 <-
1143 [{:Accept, "application/activity+json"}]
1145 {:ok, data} <- Jason.decode(body) do