1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.
10 alias Pleroma.FollowingRelationship
12 alias Pleroma.Object.Containment
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.Federator
19 alias Pleroma.Workers.TransmogrifierWorker
24 require Pleroma.Constants
27 Modifies an incoming AP object (mastodon format) to our internal format.
29 def fix_object(object, options \\ []) do
31 |> strip_internal_fields
36 |> fix_in_reply_to(options)
45 def fix_summary(%{"summary" => nil} = object) do
46 Map.put(object, "summary", "")
49 def fix_summary(%{"summary" => _} = object) do
50 # summary is present, nothing to do
54 def fix_summary(object), do: Map.put(object, "summary", "")
56 def fix_addressing_list(map, field) do
58 is_binary(map[field]) ->
59 Map.put(map, field, [map[field]])
62 Map.put(map, field, [])
69 def fix_explicit_addressing(
70 %{"to" => to, "cc" => cc} = object,
74 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
76 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
80 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
84 |> Map.put("to", explicit_to)
85 |> Map.put("cc", final_cc)
88 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
90 # if directMessage flag is set to true, leave the addressing alone
91 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
93 def fix_explicit_addressing(object) do
94 explicit_mentions = Utils.determine_explicit_mentions(object)
96 %User{follower_address: follower_collection} =
98 |> Containment.get_actor()
99 |> User.get_cached_by_ap_id()
104 Pleroma.Constants.as_public(),
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 Pleroma.Constants.as_public() in cc ->
119 to = to ++ [followers_collection]
120 Map.put(object, "to", to)
122 Pleroma.Constants.as_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
150 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
153 def fix_in_reply_to(object, options \\ [])
155 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
156 when not is_nil(in_reply_to) do
157 in_reply_to_id = prepare_in_reply_to(in_reply_to)
158 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
159 depth = (options[:depth] || 0) + 1
161 if Federator.allowed_thread_distance?(depth) do
162 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
163 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
165 |> Map.put("inReplyTo", replied_object.data["id"])
166 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
167 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
168 |> Map.put("context", replied_object.data["context"] || object["conversation"])
171 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
179 def fix_in_reply_to(object, _options), do: object
181 defp prepare_in_reply_to(in_reply_to) do
183 is_bitstring(in_reply_to) ->
186 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
189 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
190 Enum.at(in_reply_to, 0)
197 def fix_context(object) do
198 context = object["context"] || object["conversation"] || Utils.generate_context_id()
201 |> Map.put("context", context)
202 |> Map.put("conversation", context)
205 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
207 |> Map.put("attachment", [attachment])
211 def fix_attachments(object), do: object
213 def fix_url(%{"url" => url} = object) when is_map(url) do
214 Map.put(object, "url", url["href"])
217 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
218 first_element = Enum.at(url, 0)
220 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
223 |> Map.put("attachment", [first_element])
224 |> Map.put("url", link_element["href"])
227 def fix_url(%{"type" => object_type, "url" => url} = object)
228 when object_type != "Video" and is_list(url) do
229 first_element = Enum.at(url, 0)
233 is_bitstring(first_element) -> first_element
234 is_map(first_element) -> first_element["href"] || ""
238 Map.put(object, "url", url_string)
241 def fix_url(object), do: object
243 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
246 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
247 |> Enum.reduce(%{}, fn data, mapping ->
248 name = String.trim(data["name"], ":")
250 Map.put(mapping, name, data["icon"]["url"])
253 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
254 emoji = Map.merge(object["emoji"] || %{}, emoji)
256 Map.put(object, "emoji", emoji)
259 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
260 name = String.trim(tag["name"], ":")
261 emoji = %{name => tag["icon"]["url"]}
263 Map.put(object, "emoji", emoji)
266 def fix_emoji(object), do: object
268 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
271 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
272 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
274 Map.put(object, "tag", tag ++ tags)
277 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
278 combined = [tag, String.slice(hashtag, 1..-1)]
280 Map.put(object, "tag", combined)
283 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
285 def fix_tag(object), do: object
287 # content map usually only has one language so this will do for now.
288 def fix_content_map(%{"contentMap" => content_map} = object) do
289 content_groups = Map.to_list(content_map)
290 {_, content} = Enum.at(content_groups, 0)
292 Map.put(object, "content", content)
295 def fix_content_map(object), do: object
297 def fix_type(object, options \\ [])
299 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
300 when is_binary(reply_id) do
301 with true <- Federator.allowed_thread_distance?(options[:depth]),
302 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
303 Map.put(object, "type", "Answer")
309 def fix_type(object, _), do: object
311 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
312 with true <- id =~ "follows",
313 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
314 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
321 defp mastodon_follow_hack(_, _), do: {:error, nil}
323 defp get_follow_activity(follow_object, followed) do
324 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
325 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
328 # Can't find the activity. This might a Mastodon 2.3 "Accept"
330 mastodon_follow_hack(follow_object, followed)
337 # Reduce the object list to find the reported user.
338 defp get_reported(objects) do
339 Enum.reduce_while(objects, nil, fn ap_id, _ ->
340 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
348 def handle_incoming(data, options \\ [])
350 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
352 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
353 with context <- data["context"] || Utils.generate_context_id(),
354 content <- data["content"] || "",
355 %User{} = actor <- User.get_cached_by_ap_id(actor),
356 # Reduce the object list to find the reported user.
357 %User{} = account <- get_reported(objects),
358 # Remove the reported user from the object list.
359 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
366 additional: %{"cc" => [account.ap_id]}
368 |> ActivityPub.flag()
372 # disallow objects with bogus IDs
373 def handle_incoming(%{"id" => nil}, _options), do: :error
374 def handle_incoming(%{"id" => ""}, _options), do: :error
375 # length of https:// = 8, should validate better, but good enough for now.
376 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
379 # TODO: validate those with a Ecto scheme
383 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
386 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer"] do
387 actor = Containment.get_actor(data)
390 Map.put(data, "actor", actor)
393 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
394 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
395 object = fix_object(object, options)
401 context: object["conversation"],
403 published: data["published"],
412 with {:ok, created_activity} <- ActivityPub.create(params) do
413 reply_depth = (options[:depth] || 0) + 1
415 if Federator.allowed_thread_distance?(reply_depth) do
416 for reply_id <- replies(object) do
417 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
419 "depth" => reply_depth
424 {:ok, created_activity}
427 %Activity{} = activity -> {:ok, activity}
433 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
436 actor = Containment.get_actor(data)
439 Map.put(data, "actor", actor)
442 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
443 reply_depth = (options[:depth] || 0) + 1
444 options = Keyword.put(options, :depth, reply_depth)
445 object = fix_object(object, options)
453 published: data["published"],
454 additional: Map.take(data, ["cc", "id"])
457 ActivityPub.listen(params)
464 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
467 with %User{local: true} = followed <-
468 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
469 {:ok, %User{} = follower} <-
470 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
471 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
472 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
473 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
474 {_, false} <- {:user_locked, User.locked?(followed)},
475 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
477 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
478 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
479 ActivityPub.accept(%{
480 to: [follower.ap_id],
486 {:user_blocked, true} ->
487 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
488 {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
490 ActivityPub.reject(%{
491 to: [follower.ap_id],
497 {:follow, {:error, _}} ->
498 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
499 {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
501 ActivityPub.reject(%{
502 to: [follower.ap_id],
508 {:user_locked, true} ->
509 {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
521 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
524 with actor <- Containment.get_actor(data),
525 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
526 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
527 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
528 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
529 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
530 ActivityPub.accept(%{
531 to: follow_activity.data["to"],
534 object: follow_activity.data["id"],
544 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
547 with actor <- Containment.get_actor(data),
548 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
549 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
550 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
551 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
552 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
554 ActivityPub.reject(%{
555 to: follow_activity.data["to"],
558 object: follow_activity.data["id"],
568 @misskey_reactions %{
582 @doc "Rewrite misskey likes into EmojiReacts"
586 "_misskey_reaction" => reaction
591 |> Map.put("type", "EmojiReact")
592 |> Map.put("content", @misskey_reactions[reaction] || reaction)
593 |> handle_incoming(options)
597 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
600 with actor <- Containment.get_actor(data),
601 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
602 {:ok, object} <- get_obj_helper(object_id),
603 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
612 "type" => "EmojiReact",
613 "object" => object_id,
620 with actor <- Containment.get_actor(data),
621 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
622 {:ok, object} <- get_obj_helper(object_id),
623 {:ok, activity, _object} <-
624 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
632 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
635 with actor <- Containment.get_actor(data),
636 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
637 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
638 public <- Visibility.is_public?(data),
639 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
647 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
651 when object_type in [
657 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
658 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
661 |> User.upgrade_changeset(new_user_data, true)
662 |> User.update_and_set_cache()
664 ActivityPub.update(%{
666 to: data["to"] || [],
667 cc: data["cc"] || [],
670 activity_id: data["id"]
679 # TODO: We presently assume that any actor on the same origin domain as the object being
680 # deleted has the rights to delete that object. A better way to validate whether or not
681 # the object should be deleted is to refetch the object URI, which should return either
682 # an error or a tombstone. This would allow us to verify that a deletion actually took
685 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
688 object_id = Utils.get_ap_id(object_id)
690 with actor <- Containment.get_actor(data),
691 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
692 {:ok, object} <- get_obj_helper(object_id),
693 :ok <- Containment.contain_origin(actor.ap_id, object.data),
695 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
699 case User.get_cached_by_ap_id(object_id) do
700 %User{ap_id: ^actor} = user ->
715 "object" => %{"type" => "Announce", "object" => object_id},
721 with actor <- Containment.get_actor(data),
722 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
723 {:ok, object} <- get_obj_helper(object_id),
724 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
734 "object" => %{"type" => "Follow", "object" => followed},
740 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
741 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
742 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
743 User.unfollow(follower, followed)
753 "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
759 with actor <- Containment.get_actor(data),
760 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
761 {:ok, activity, _} <-
762 ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
775 "object" => %{"type" => "Block", "object" => blocked},
781 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
782 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
783 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
784 User.unblock(blocker, blocked)
792 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
795 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
796 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
797 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
798 User.unfollow(blocker, blocked)
799 User.block(blocker, blocked)
809 "object" => %{"type" => "Like", "object" => object_id},
815 with actor <- Containment.get_actor(data),
816 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
817 {:ok, object} <- get_obj_helper(object_id),
818 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
825 # For Undos that don't have the complete object attached, try to find it in our database.
833 when is_binary(object) do
834 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
836 |> Map.put("object", data)
837 |> handle_incoming(options)
846 "actor" => origin_actor,
847 "object" => origin_actor,
848 "target" => target_actor
852 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
853 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
854 true <- origin_actor in target_user.also_known_as do
855 ActivityPub.move(origin_user, target_user, false)
861 def handle_incoming(_, _), do: :error
863 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
864 def get_obj_helper(id, options \\ []) do
865 case Object.normalize(id, true, options) do
866 %Object{} = object -> {:ok, object}
871 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
872 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
875 when attributed_to == ap_id do
876 with {:ok, activity} <-
881 "actor" => attributed_to,
884 {:ok, Object.normalize(activity)}
886 _ -> get_obj_helper(object_id)
890 def get_embedded_obj_helper(object_id, _) do
891 get_obj_helper(object_id)
894 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
895 with false <- String.starts_with?(in_reply_to, "http"),
896 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
897 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
903 def set_reply_to_uri(obj), do: obj
906 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
907 Based on Mastodon's ActivityPub::NoteSerializer#replies.
909 def set_replies(obj_data) do
911 with limit when limit > 0 <-
912 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
913 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
915 |> Object.self_replies()
916 |> select([o], fragment("?->>'id'", o.data))
923 set_replies(obj_data, replies_uris)
926 defp set_replies(obj, []) do
930 defp set_replies(obj, replies_uris) do
931 replies_collection = %{
932 "type" => "Collection",
933 "items" => replies_uris
936 Map.merge(obj, %{"replies" => replies_collection})
939 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
943 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
947 def replies(_), do: []
949 # Prepares the object of an outgoing create activity.
950 def prepare_object(object) do
957 |> prepare_attachments
961 |> strip_internal_fields
962 |> strip_internal_tags
968 # internal -> Mastodon
971 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
972 when activity_type in ["Create", "Listen"] do
975 |> Object.normalize()
981 |> Map.put("object", object)
982 |> Map.merge(Utils.make_json_ld_header())
988 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
991 |> Object.normalize()
994 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
995 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
997 data |> maybe_fix_object_url
1002 |> strip_internal_fields
1003 |> Map.merge(Utils.make_json_ld_header())
1004 |> Map.delete("bcc")
1009 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
1010 # because of course it does.
1011 def prepare_outgoing(%{"type" => "Accept"} = data) do
1012 with follow_activity <- Activity.normalize(data["object"]) do
1014 "actor" => follow_activity.actor,
1015 "object" => follow_activity.data["object"],
1016 "id" => follow_activity.data["id"],
1022 |> Map.put("object", object)
1023 |> Map.merge(Utils.make_json_ld_header())
1029 def prepare_outgoing(%{"type" => "Reject"} = data) do
1030 with follow_activity <- Activity.normalize(data["object"]) do
1032 "actor" => follow_activity.actor,
1033 "object" => follow_activity.data["object"],
1034 "id" => follow_activity.data["id"],
1040 |> Map.put("object", object)
1041 |> Map.merge(Utils.make_json_ld_header())
1047 def prepare_outgoing(%{"type" => _type} = data) do
1050 |> strip_internal_fields
1051 |> maybe_fix_object_url
1052 |> Map.merge(Utils.make_json_ld_header())
1057 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1058 with false <- String.starts_with?(object, "http"),
1059 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1060 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1062 Map.put(data, "object", external_url)
1065 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1073 def maybe_fix_object_url(data), do: data
1075 def add_hashtags(object) do
1077 (object["tag"] || [])
1079 # Expand internal representation tags into AS2 tags.
1080 tag when is_binary(tag) ->
1082 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1083 "name" => "##{tag}",
1087 # Do not process tags which are already AS2 tag objects.
1088 tag when is_map(tag) ->
1092 Map.put(object, "tag", tags)
1095 def add_mention_tags(object) do
1098 |> Utils.get_notified_from_object()
1099 |> Enum.map(&build_mention_tag/1)
1101 tags = object["tag"] || []
1103 Map.put(object, "tag", tags ++ mentions)
1106 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1107 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1110 def take_emoji_tags(%User{emoji: emoji}) do
1112 |> Enum.flat_map(&Map.to_list/1)
1113 |> Enum.map(&build_emoji_tag/1)
1116 # TODO: we should probably send mtime instead of unix epoch time for updated
1117 def add_emoji_tags(%{"emoji" => emoji} = object) do
1118 tags = object["tag"] || []
1120 out = Enum.map(emoji, &build_emoji_tag/1)
1122 Map.put(object, "tag", tags ++ out)
1125 def add_emoji_tags(object), do: object
1127 defp build_emoji_tag({name, url}) do
1129 "icon" => %{"url" => url, "type" => "Image"},
1130 "name" => ":" <> name <> ":",
1132 "updated" => "1970-01-01T00:00:00Z",
1137 def set_conversation(object) do
1138 Map.put(object, "conversation", object["context"])
1141 def set_sensitive(object) do
1142 tags = object["tag"] || []
1143 Map.put(object, "sensitive", "nsfw" in tags)
1146 def set_type(%{"type" => "Answer"} = object) do
1147 Map.put(object, "type", "Note")
1150 def set_type(object), do: object
1152 def add_attributed_to(object) do
1153 attributed_to = object["attributedTo"] || object["actor"]
1154 Map.put(object, "attributedTo", attributed_to)
1157 def prepare_attachments(object) do
1159 (object["attachment"] || [])
1160 |> Enum.map(fn data ->
1161 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1162 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
1165 Map.put(object, "attachment", attachments)
1168 def strip_internal_fields(object) do
1170 |> Map.drop(Pleroma.Constants.object_internal_fields())
1173 defp strip_internal_tags(%{"tag" => tags} = object) do
1174 tags = Enum.filter(tags, fn x -> is_map(x) end)
1176 Map.put(object, "tag", tags)
1179 defp strip_internal_tags(object), do: object
1181 def perform(:user_upgrade, user) do
1182 # we pass a fake user so that the followers collection is stripped away
1183 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1187 where: ^old_follower_address in a.recipients,
1192 "array_replace(?,?,?)",
1194 ^old_follower_address,
1195 ^user.follower_address
1200 |> Repo.update_all([])
1203 def upgrade_user_from_ap_id(ap_id) do
1204 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1205 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1206 already_ap <- User.ap_enabled?(user),
1207 {:ok, user} <- upgrade_user(user, data) do
1208 if not already_ap do
1209 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1214 %User{} = user -> {:ok, user}
1219 defp upgrade_user(user, data) do
1221 |> User.upgrade_changeset(data, true)
1222 |> User.update_and_set_cache()
1225 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1226 Map.put(data, "url", url["href"])
1229 def maybe_fix_user_url(data), do: data
1231 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)