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.EarmarkRenderer
11 alias Pleroma.FollowingRelationship
13 alias Pleroma.Object.Containment
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.ObjectValidator
19 alias Pleroma.Web.ActivityPub.ObjectValidators.Types
20 alias Pleroma.Web.ActivityPub.Pipeline
21 alias Pleroma.Web.ActivityPub.Utils
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.Federator
24 alias Pleroma.Workers.TransmogrifierWorker
29 require Pleroma.Constants
32 Modifies an incoming AP object (mastodon format) to our internal format.
34 def fix_object(object, options \\ []) do
36 |> strip_internal_fields
41 |> fix_in_reply_to(options)
51 def fix_summary(%{"summary" => nil} = object) do
52 Map.put(object, "summary", "")
55 def fix_summary(%{"summary" => _} = object) do
56 # summary is present, nothing to do
60 def fix_summary(object), do: Map.put(object, "summary", "")
62 def fix_addressing_list(map, field) do
64 is_binary(map[field]) ->
65 Map.put(map, field, [map[field]])
68 Map.put(map, field, [])
75 def fix_explicit_addressing(
76 %{"to" => to, "cc" => cc} = object,
80 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
82 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
86 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
90 |> Map.put("to", explicit_to)
91 |> Map.put("cc", final_cc)
94 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
96 # if directMessage flag is set to true, leave the addressing alone
97 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
99 def fix_explicit_addressing(object) do
100 explicit_mentions = Utils.determine_explicit_mentions(object)
102 %User{follower_address: follower_collection} =
104 |> Containment.get_actor()
105 |> User.get_cached_by_ap_id()
110 Pleroma.Constants.as_public(),
114 fix_explicit_addressing(object, explicit_mentions, follower_collection)
117 # if as:Public is addressed, then make sure the followers collection is also addressed
118 # so that the activities will be delivered to local users.
119 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
120 recipients = to ++ cc
122 if followers_collection not in recipients do
124 Pleroma.Constants.as_public() in cc ->
125 to = to ++ [followers_collection]
126 Map.put(object, "to", to)
128 Pleroma.Constants.as_public() in to ->
129 cc = cc ++ [followers_collection]
130 Map.put(object, "cc", cc)
140 def fix_implicit_addressing(object, _), do: object
142 def fix_addressing(object) do
143 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
144 followers_collection = User.ap_followers(user)
147 |> fix_addressing_list("to")
148 |> fix_addressing_list("cc")
149 |> fix_addressing_list("bto")
150 |> fix_addressing_list("bcc")
151 |> fix_explicit_addressing()
152 |> fix_implicit_addressing(followers_collection)
155 def fix_actor(%{"attributedTo" => actor} = object) do
156 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
159 def fix_in_reply_to(object, options \\ [])
161 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
162 when not is_nil(in_reply_to) do
163 in_reply_to_id = prepare_in_reply_to(in_reply_to)
164 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
165 depth = (options[:depth] || 0) + 1
167 if Federator.allowed_thread_distance?(depth) do
168 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
169 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
171 |> Map.put("inReplyTo", replied_object.data["id"])
172 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
173 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
174 |> Map.put("context", replied_object.data["context"] || object["conversation"])
177 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
185 def fix_in_reply_to(object, _options), do: object
187 defp prepare_in_reply_to(in_reply_to) do
189 is_bitstring(in_reply_to) ->
192 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
195 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
196 Enum.at(in_reply_to, 0)
203 def fix_context(object) do
204 context = object["context"] || object["conversation"] || Utils.generate_context_id()
207 |> Map.put("context", context)
208 |> Map.put("conversation", context)
211 defp add_if_present(map, _key, nil), do: map
213 defp add_if_present(map, key, value) do
214 Map.put(map, key, value)
217 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
219 Enum.map(attachment, fn data ->
222 is_list(data["url"]) -> List.first(data["url"])
223 is_map(data["url"]) -> data["url"]
229 is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
230 is_binary(data["mediaType"]) -> data["mediaType"]
231 is_binary(data["mimeType"]) -> data["mimeType"]
237 is_map(url) && is_binary(url["href"]) -> url["href"]
238 is_binary(data["url"]) -> data["url"]
239 is_binary(data["href"]) -> data["href"]
244 |> add_if_present("mediaType", media_type)
245 |> add_if_present("type", Map.get(url || %{}, "type"))
247 %{"url" => [attachment_url]}
248 |> add_if_present("mediaType", media_type)
249 |> add_if_present("type", data["type"])
250 |> add_if_present("name", data["name"])
253 Map.put(object, "attachment", attachments)
256 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
258 |> Map.put("attachment", [attachment])
262 def fix_attachments(object), do: object
264 def fix_url(%{"url" => url} = object) when is_map(url) do
265 Map.put(object, "url", url["href"])
268 def fix_url(%{"type" => object_type, "url" => url} = object)
269 when object_type in ["Video", "Audio"] and is_list(url) do
270 first_element = Enum.at(url, 0)
272 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
275 |> Map.put("attachment", [first_element])
276 |> Map.put("url", link_element["href"])
279 def fix_url(%{"type" => object_type, "url" => url} = object)
280 when object_type != "Video" and is_list(url) do
281 first_element = Enum.at(url, 0)
285 is_bitstring(first_element) -> first_element
286 is_map(first_element) -> first_element["href"] || ""
290 Map.put(object, "url", url_string)
293 def fix_url(object), do: object
295 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
298 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
299 |> Enum.reduce(%{}, fn data, mapping ->
300 name = String.trim(data["name"], ":")
302 Map.put(mapping, name, data["icon"]["url"])
305 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
306 emoji = Map.merge(object["emoji"] || %{}, emoji)
308 Map.put(object, "emoji", emoji)
311 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
312 name = String.trim(tag["name"], ":")
313 emoji = %{name => tag["icon"]["url"]}
315 Map.put(object, "emoji", emoji)
318 def fix_emoji(object), do: object
320 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
323 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
324 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
326 Map.put(object, "tag", tag ++ tags)
329 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
330 combined = [tag, String.slice(hashtag, 1..-1)]
332 Map.put(object, "tag", combined)
335 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
337 def fix_tag(object), do: object
339 # content map usually only has one language so this will do for now.
340 def fix_content_map(%{"contentMap" => content_map} = object) do
341 content_groups = Map.to_list(content_map)
342 {_, content} = Enum.at(content_groups, 0)
344 Map.put(object, "content", content)
347 def fix_content_map(object), do: object
349 def fix_type(object, options \\ [])
351 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
352 when is_binary(reply_id) do
353 with true <- Federator.allowed_thread_distance?(options[:depth]),
354 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
355 Map.put(object, "type", "Answer")
361 def fix_type(object, _), do: object
363 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
364 when is_binary(content) do
367 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
368 |> Pleroma.HTML.filter_tags()
370 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
373 defp fix_content(object), do: object
375 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
376 with true <- id =~ "follows",
377 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
378 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
385 defp mastodon_follow_hack(_, _), do: {:error, nil}
387 defp get_follow_activity(follow_object, followed) do
388 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
389 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
392 # Can't find the activity. This might a Mastodon 2.3 "Accept"
394 mastodon_follow_hack(follow_object, followed)
401 # Reduce the object list to find the reported user.
402 defp get_reported(objects) do
403 Enum.reduce_while(objects, nil, fn ap_id, _ ->
404 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
412 def handle_incoming(data, options \\ [])
414 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
416 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
417 with context <- data["context"] || Utils.generate_context_id(),
418 content <- data["content"] || "",
419 %User{} = actor <- User.get_cached_by_ap_id(actor),
420 # Reduce the object list to find the reported user.
421 %User{} = account <- get_reported(objects),
422 # Remove the reported user from the object list.
423 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
430 additional: %{"cc" => [account.ap_id]}
432 |> ActivityPub.flag()
436 # disallow objects with bogus IDs
437 def handle_incoming(%{"id" => nil}, _options), do: :error
438 def handle_incoming(%{"id" => ""}, _options), do: :error
439 # length of https:// = 8, should validate better, but good enough for now.
440 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
443 # TODO: validate those with a Ecto scheme
447 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
450 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
451 actor = Containment.get_actor(data)
454 Map.put(data, "actor", actor)
457 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
458 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
459 object = fix_object(object, options)
465 context: object["conversation"],
467 published: data["published"],
476 with {:ok, created_activity} <- ActivityPub.create(params) do
477 reply_depth = (options[:depth] || 0) + 1
479 if Federator.allowed_thread_distance?(reply_depth) do
480 for reply_id <- replies(object) do
481 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
483 "depth" => reply_depth
488 {:ok, created_activity}
491 %Activity{} = activity -> {:ok, activity}
497 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
500 actor = Containment.get_actor(data)
503 Map.put(data, "actor", actor)
506 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
507 reply_depth = (options[:depth] || 0) + 1
508 options = Keyword.put(options, :depth, reply_depth)
509 object = fix_object(object, options)
517 published: data["published"],
518 additional: Map.take(data, ["cc", "id"])
521 ActivityPub.listen(params)
528 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
531 with %User{local: true} = followed <-
532 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
533 {:ok, %User{} = follower} <-
534 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
535 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
536 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
537 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
538 {_, false} <- {:user_locked, User.locked?(followed)},
539 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
541 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
542 {:ok, _relationship} <-
543 FollowingRelationship.update(follower, followed, :follow_accept) do
544 ActivityPub.accept(%{
545 to: [follower.ap_id],
551 {:user_blocked, true} ->
552 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
553 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
555 ActivityPub.reject(%{
556 to: [follower.ap_id],
562 {:follow, {:error, _}} ->
563 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
564 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
566 ActivityPub.reject(%{
567 to: [follower.ap_id],
573 {:user_locked, true} ->
574 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
586 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
589 with actor <- Containment.get_actor(data),
590 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
591 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
592 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
593 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
594 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
595 ActivityPub.accept(%{
596 to: follow_activity.data["to"],
599 object: follow_activity.data["id"],
609 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
612 with actor <- Containment.get_actor(data),
613 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
614 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
615 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
616 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
617 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
619 ActivityPub.reject(%{
620 to: follow_activity.data["to"],
623 object: follow_activity.data["id"],
633 @misskey_reactions %{
647 @doc "Rewrite misskey likes into EmojiReacts"
651 "_misskey_reaction" => reaction
656 |> Map.put("type", "EmojiReact")
657 |> Map.put("content", @misskey_reactions[reaction] || reaction)
658 |> handle_incoming(options)
662 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
665 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
666 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
671 def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
672 with :ok <- ObjectValidator.fetch_actor_and_object(data),
673 {:ok, activity, _meta} <-
674 Pipeline.common_pipeline(data, local: false) do
682 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
685 with actor <- Containment.get_actor(data),
686 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
687 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
688 public <- Visibility.is_public?(data),
689 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
697 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
701 when object_type in [
707 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
708 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
711 |> User.remote_user_changeset(new_user_data)
712 |> User.update_and_set_cache()
714 ActivityPub.update(%{
716 to: data["to"] || [],
717 cc: data["cc"] || [],
720 activity_id: data["id"]
730 %{"type" => "Delete"} = data,
733 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
736 {:error, {:validate_object, _}} = e ->
737 # Check if we have a create activity for this
738 with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
739 %Activity{data: %{"actor" => actor}} <-
740 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
741 # We have one, insert a tombstone and retry
742 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
743 {:ok, _tombstone} <- Object.create(tombstone_data) do
744 handle_incoming(data)
754 "object" => %{"type" => "Follow", "object" => followed},
760 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
761 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
762 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
763 User.unfollow(follower, followed)
773 "object" => %{"type" => type}
777 when type in ["Like", "EmojiReact", "Announce", "Block"] do
778 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
783 # For Undos that don't have the complete object attached, try to find it in our database.
791 when is_binary(object) do
792 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
794 |> Map.put("object", data)
795 |> handle_incoming(options)
802 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
805 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
806 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
807 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
808 User.unfollow(blocker, blocked)
809 User.block(blocker, blocked)
819 "actor" => origin_actor,
820 "object" => origin_actor,
821 "target" => target_actor
825 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
826 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
827 true <- origin_actor in target_user.also_known_as do
828 ActivityPub.move(origin_user, target_user, false)
834 def handle_incoming(_, _), do: :error
836 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
837 def get_obj_helper(id, options \\ []) do
838 case Object.normalize(id, true, options) do
839 %Object{} = object -> {:ok, object}
844 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
845 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
848 when attributed_to == ap_id do
849 with {:ok, activity} <-
854 "actor" => attributed_to,
857 {:ok, Object.normalize(activity)}
859 _ -> get_obj_helper(object_id)
863 def get_embedded_obj_helper(object_id, _) do
864 get_obj_helper(object_id)
867 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
868 with false <- String.starts_with?(in_reply_to, "http"),
869 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
870 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
876 def set_reply_to_uri(obj), do: obj
879 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
880 Based on Mastodon's ActivityPub::NoteSerializer#replies.
882 def set_replies(obj_data) do
884 with limit when limit > 0 <-
885 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
886 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
888 |> Object.self_replies()
889 |> select([o], fragment("?->>'id'", o.data))
896 set_replies(obj_data, replies_uris)
899 defp set_replies(obj, []) do
903 defp set_replies(obj, replies_uris) do
904 replies_collection = %{
905 "type" => "Collection",
906 "items" => replies_uris
909 Map.merge(obj, %{"replies" => replies_collection})
912 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
916 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
920 def replies(_), do: []
922 # Prepares the object of an outgoing create activity.
923 def prepare_object(object) do
930 |> prepare_attachments
934 |> strip_internal_fields
935 |> strip_internal_tags
941 # internal -> Mastodon
944 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
945 when activity_type in ["Create", "Listen"] do
948 |> Object.normalize()
954 |> Map.put("object", object)
955 |> Map.merge(Utils.make_json_ld_header())
961 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
964 |> Object.normalize()
967 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
968 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
970 data |> maybe_fix_object_url
975 |> strip_internal_fields
976 |> Map.merge(Utils.make_json_ld_header())
982 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
983 # because of course it does.
984 def prepare_outgoing(%{"type" => "Accept"} = data) do
985 with follow_activity <- Activity.normalize(data["object"]) do
987 "actor" => follow_activity.actor,
988 "object" => follow_activity.data["object"],
989 "id" => follow_activity.data["id"],
995 |> Map.put("object", object)
996 |> Map.merge(Utils.make_json_ld_header())
1002 def prepare_outgoing(%{"type" => "Reject"} = data) do
1003 with follow_activity <- Activity.normalize(data["object"]) do
1005 "actor" => follow_activity.actor,
1006 "object" => follow_activity.data["object"],
1007 "id" => follow_activity.data["id"],
1013 |> Map.put("object", object)
1014 |> Map.merge(Utils.make_json_ld_header())
1020 def prepare_outgoing(%{"type" => _type} = data) do
1023 |> strip_internal_fields
1024 |> maybe_fix_object_url
1025 |> Map.merge(Utils.make_json_ld_header())
1030 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1031 with false <- String.starts_with?(object, "http"),
1032 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1033 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1035 Map.put(data, "object", external_url)
1038 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1046 def maybe_fix_object_url(data), do: data
1048 def add_hashtags(object) do
1050 (object["tag"] || [])
1052 # Expand internal representation tags into AS2 tags.
1053 tag when is_binary(tag) ->
1055 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1056 "name" => "##{tag}",
1060 # Do not process tags which are already AS2 tag objects.
1061 tag when is_map(tag) ->
1065 Map.put(object, "tag", tags)
1068 def add_mention_tags(object) do
1069 {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
1070 potential_receivers = enabled_receivers ++ disabled_receivers
1071 mentions = Enum.map(potential_receivers, &build_mention_tag/1)
1073 tags = object["tag"] || []
1074 Map.put(object, "tag", tags ++ mentions)
1077 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1078 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1081 def take_emoji_tags(%User{emoji: emoji}) do
1084 |> Enum.map(&build_emoji_tag/1)
1087 # TODO: we should probably send mtime instead of unix epoch time for updated
1088 def add_emoji_tags(%{"emoji" => emoji} = object) do
1089 tags = object["tag"] || []
1091 out = Enum.map(emoji, &build_emoji_tag/1)
1093 Map.put(object, "tag", tags ++ out)
1096 def add_emoji_tags(object), do: object
1098 defp build_emoji_tag({name, url}) do
1100 "icon" => %{"url" => url, "type" => "Image"},
1101 "name" => ":" <> name <> ":",
1103 "updated" => "1970-01-01T00:00:00Z",
1108 def set_conversation(object) do
1109 Map.put(object, "conversation", object["context"])
1112 def set_sensitive(%{"sensitive" => true} = object) do
1116 def set_sensitive(object) do
1117 tags = object["tag"] || []
1118 Map.put(object, "sensitive", "nsfw" in tags)
1121 def set_type(%{"type" => "Answer"} = object) do
1122 Map.put(object, "type", "Note")
1125 def set_type(object), do: object
1127 def add_attributed_to(object) do
1128 attributed_to = object["attributedTo"] || object["actor"]
1129 Map.put(object, "attributedTo", attributed_to)
1132 # TODO: Revisit this
1133 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1135 def prepare_attachments(object) do
1138 |> Map.get("attachment", [])
1139 |> Enum.map(fn data ->
1140 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1144 "mediaType" => media_type,
1145 "name" => data["name"],
1146 "type" => "Document"
1150 Map.put(object, "attachment", attachments)
1153 def strip_internal_fields(object) do
1154 Map.drop(object, Pleroma.Constants.object_internal_fields())
1157 defp strip_internal_tags(%{"tag" => tags} = object) do
1158 tags = Enum.filter(tags, fn x -> is_map(x) end)
1160 Map.put(object, "tag", tags)
1163 defp strip_internal_tags(object), do: object
1165 def perform(:user_upgrade, user) do
1166 # we pass a fake user so that the followers collection is stripped away
1167 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1171 where: ^old_follower_address in a.recipients,
1176 "array_replace(?,?,?)",
1178 ^old_follower_address,
1179 ^user.follower_address
1184 |> Repo.update_all([])
1187 def upgrade_user_from_ap_id(ap_id) do
1188 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1189 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1190 {:ok, user} <- update_user(user, data) do
1191 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1194 %User{} = user -> {:ok, user}
1199 defp update_user(user, data) do
1201 |> User.remote_user_changeset(data)
1202 |> User.update_and_set_cache()
1205 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1206 Map.put(data, "url", url["href"])
1209 def maybe_fix_user_url(data), do: data
1211 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)