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.ObjectValidator
18 alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
19 alias Pleroma.Web.ActivityPub.Pipeline
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.Federator
23 alias Pleroma.Workers.TransmogrifierWorker
28 require Pleroma.Constants
31 Modifies an incoming AP object (mastodon format) to our internal format.
33 def fix_object(object, options \\ []) do
35 |> strip_internal_fields
40 |> fix_in_reply_to(options)
50 def fix_summary(%{"summary" => nil} = object) do
51 Map.put(object, "summary", "")
54 def fix_summary(%{"summary" => _} = object) do
55 # summary is present, nothing to do
59 def fix_summary(object), do: Map.put(object, "summary", "")
61 def fix_addressing_list(map, field) do
63 is_binary(map[field]) ->
64 Map.put(map, field, [map[field]])
67 Map.put(map, field, [])
74 def fix_explicit_addressing(
75 %{"to" => to, "cc" => cc} = object,
79 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
81 explicit_cc = Enum.filter(to, 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
99 explicit_mentions = Utils.determine_explicit_mentions(object)
101 %User{follower_address: follower_collection} =
103 |> Containment.get_actor()
104 |> User.get_cached_by_ap_id()
109 Pleroma.Constants.as_public(),
113 fix_explicit_addressing(object, explicit_mentions, follower_collection)
116 # if as:Public is addressed, then make sure the followers collection is also addressed
117 # so that the activities will be delivered to local users.
118 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
119 recipients = to ++ cc
121 if followers_collection not in recipients do
123 Pleroma.Constants.as_public() in cc ->
124 to = to ++ [followers_collection]
125 Map.put(object, "to", to)
127 Pleroma.Constants.as_public() in to ->
128 cc = cc ++ [followers_collection]
129 Map.put(object, "cc", cc)
139 def fix_implicit_addressing(object, _), do: object
141 def fix_addressing(object) do
142 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
143 followers_collection = User.ap_followers(user)
146 |> fix_addressing_list("to")
147 |> fix_addressing_list("cc")
148 |> fix_addressing_list("bto")
149 |> fix_addressing_list("bcc")
150 |> fix_explicit_addressing()
151 |> fix_implicit_addressing(followers_collection)
154 def fix_actor(%{"attributedTo" => actor} = object) do
155 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
158 def fix_in_reply_to(object, options \\ [])
160 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
161 when not is_nil(in_reply_to) do
162 in_reply_to_id = prepare_in_reply_to(in_reply_to)
163 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
164 depth = (options[:depth] || 0) + 1
166 if Federator.allowed_thread_distance?(depth) do
167 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
168 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
170 |> Map.put("inReplyTo", replied_object.data["id"])
171 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
172 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
173 |> Map.put("context", replied_object.data["context"] || object["conversation"])
176 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
184 def fix_in_reply_to(object, _options), do: object
186 defp prepare_in_reply_to(in_reply_to) do
188 is_bitstring(in_reply_to) ->
191 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
194 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
195 Enum.at(in_reply_to, 0)
202 def fix_context(object) do
203 context = object["context"] || object["conversation"] || Utils.generate_context_id()
206 |> Map.put("context", context)
207 |> Map.put("conversation", context)
210 defp add_if_present(map, _key, nil), do: map
212 defp add_if_present(map, key, value) do
213 Map.put(map, key, value)
216 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
218 Enum.map(attachment, fn data ->
221 is_list(data["url"]) -> List.first(data["url"])
222 is_map(data["url"]) -> data["url"]
228 is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
229 is_binary(data["mediaType"]) -> data["mediaType"]
230 is_binary(data["mimeType"]) -> data["mimeType"]
236 is_map(url) && is_binary(url["href"]) -> url["href"]
237 is_binary(data["url"]) -> data["url"]
238 is_binary(data["href"]) -> data["href"]
243 |> add_if_present("mediaType", media_type)
244 |> add_if_present("type", Map.get(url || %{}, "type"))
246 %{"url" => [attachment_url]}
247 |> add_if_present("mediaType", media_type)
248 |> add_if_present("type", data["type"])
249 |> add_if_present("name", data["name"])
252 Map.put(object, "attachment", attachments)
255 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
257 |> Map.put("attachment", [attachment])
261 def fix_attachments(object), do: object
263 def fix_url(%{"url" => url} = object) when is_map(url) do
264 Map.put(object, "url", url["href"])
267 def fix_url(%{"type" => object_type, "url" => url} = object)
268 when object_type in ["Video", "Audio"] and is_list(url) do
269 first_element = Enum.at(url, 0)
271 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
274 |> Map.put("attachment", [first_element])
275 |> Map.put("url", link_element["href"])
278 def fix_url(%{"type" => object_type, "url" => url} = object)
279 when object_type != "Video" and is_list(url) do
280 first_element = Enum.at(url, 0)
284 is_bitstring(first_element) -> first_element
285 is_map(first_element) -> first_element["href"] || ""
289 Map.put(object, "url", url_string)
292 def fix_url(object), do: object
294 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
297 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
298 |> Enum.reduce(%{}, fn data, mapping ->
299 name = String.trim(data["name"], ":")
301 Map.put(mapping, name, data["icon"]["url"])
304 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
305 emoji = Map.merge(object["emoji"] || %{}, emoji)
307 Map.put(object, "emoji", emoji)
310 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
311 name = String.trim(tag["name"], ":")
312 emoji = %{name => tag["icon"]["url"]}
314 Map.put(object, "emoji", emoji)
317 def fix_emoji(object), do: object
319 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
322 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
323 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
325 Map.put(object, "tag", tag ++ tags)
328 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
329 combined = [tag, String.slice(hashtag, 1..-1)]
331 Map.put(object, "tag", combined)
334 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
336 def fix_tag(object), do: object
338 # content map usually only has one language so this will do for now.
339 def fix_content_map(%{"contentMap" => content_map} = object) do
340 content_groups = Map.to_list(content_map)
341 {_, content} = Enum.at(content_groups, 0)
343 Map.put(object, "content", content)
346 def fix_content_map(object), do: object
348 def fix_type(object, options \\ [])
350 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
351 when is_binary(reply_id) do
352 with true <- Federator.allowed_thread_distance?(options[:depth]),
353 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
354 Map.put(object, "type", "Answer")
360 def fix_type(object, _), do: object
362 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
363 when is_binary(content) do
366 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
367 |> Pleroma.HTML.filter_tags()
369 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
372 defp fix_content(object), do: object
374 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
375 with true <- id =~ "follows",
376 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
377 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
384 defp mastodon_follow_hack(_, _), do: {:error, nil}
386 defp get_follow_activity(follow_object, followed) do
387 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
388 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
391 # Can't find the activity. This might a Mastodon 2.3 "Accept"
393 mastodon_follow_hack(follow_object, followed)
400 # Reduce the object list to find the reported user.
401 defp get_reported(objects) do
402 Enum.reduce_while(objects, nil, fn ap_id, _ ->
403 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
411 def handle_incoming(data, options \\ [])
413 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
415 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
416 with context <- data["context"] || Utils.generate_context_id(),
417 content <- data["content"] || "",
418 %User{} = actor <- User.get_cached_by_ap_id(actor),
419 # Reduce the object list to find the reported user.
420 %User{} = account <- get_reported(objects),
421 # Remove the reported user from the object list.
422 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
429 additional: %{"cc" => [account.ap_id]}
431 |> ActivityPub.flag()
435 # disallow objects with bogus IDs
436 def handle_incoming(%{"id" => nil}, _options), do: :error
437 def handle_incoming(%{"id" => ""}, _options), do: :error
438 # length of https:// = 8, should validate better, but good enough for now.
439 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
442 # TODO: validate those with a Ecto scheme
446 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
449 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
450 actor = Containment.get_actor(data)
453 Map.put(data, "actor", actor)
456 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
457 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
458 object = fix_object(object, options)
464 context: object["conversation"],
466 published: data["published"],
475 with {:ok, created_activity} <- ActivityPub.create(params) do
476 reply_depth = (options[:depth] || 0) + 1
478 if Federator.allowed_thread_distance?(reply_depth) do
479 for reply_id <- replies(object) do
480 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
482 "depth" => reply_depth
487 {:ok, created_activity}
490 %Activity{} = activity -> {:ok, activity}
496 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
499 actor = Containment.get_actor(data)
502 Map.put(data, "actor", actor)
505 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
506 reply_depth = (options[:depth] || 0) + 1
507 options = Keyword.put(options, :depth, reply_depth)
508 object = fix_object(object, options)
516 published: data["published"],
517 additional: Map.take(data, ["cc", "id"])
520 ActivityPub.listen(params)
527 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
530 with %User{local: true} = followed <-
531 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
532 {:ok, %User{} = follower} <-
533 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
534 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
535 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
536 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
537 {_, false} <- {:user_locked, User.locked?(followed)},
538 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
540 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
541 {:ok, _relationship} <-
542 FollowingRelationship.update(follower, followed, :follow_accept) do
543 ActivityPub.accept(%{
544 to: [follower.ap_id],
550 {:user_blocked, true} ->
551 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
552 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
554 ActivityPub.reject(%{
555 to: [follower.ap_id],
561 {:follow, {:error, _}} ->
562 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
563 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
565 ActivityPub.reject(%{
566 to: [follower.ap_id],
572 {:user_locked, true} ->
573 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
585 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
588 with actor <- Containment.get_actor(data),
589 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
590 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
591 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
592 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
593 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
594 ActivityPub.accept(%{
595 to: follow_activity.data["to"],
598 object: follow_activity.data["id"],
608 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
611 with actor <- Containment.get_actor(data),
612 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
613 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
614 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
615 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
616 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
618 ActivityPub.reject(%{
619 to: follow_activity.data["to"],
622 object: follow_activity.data["id"],
632 @misskey_reactions %{
646 @doc "Rewrite misskey likes into EmojiReacts"
650 "_misskey_reaction" => reaction
655 |> Map.put("type", "EmojiReact")
656 |> Map.put("content", @misskey_reactions[reaction] || reaction)
657 |> handle_incoming(options)
660 def handle_incoming(%{"type" => "Like"} = data, _options) do
661 with {_, {:ok, cast_data_sym}} <-
663 data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
664 cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
665 :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
666 {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
667 {_, {:ok, cast_data}} <-
668 {:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
669 {_, {:ok, activity, _meta}} <-
670 {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
679 "type" => "EmojiReact",
680 "object" => object_id,
687 with actor <- Containment.get_actor(data),
688 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
689 {:ok, object} <- get_obj_helper(object_id),
690 {:ok, activity, _object} <-
691 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
699 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
702 with actor <- Containment.get_actor(data),
703 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
704 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
705 public <- Visibility.is_public?(data),
706 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
714 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
718 when object_type in [
724 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
725 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
728 |> User.remote_user_changeset(new_user_data)
729 |> User.update_and_set_cache()
731 ActivityPub.update(%{
733 to: data["to"] || [],
734 cc: data["cc"] || [],
737 activity_id: data["id"]
747 %{"type" => "Delete"} = data,
750 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
758 "object" => %{"type" => "Announce", "object" => object_id},
764 with actor <- Containment.get_actor(data),
765 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
766 {:ok, object} <- get_obj_helper(object_id),
767 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
777 "object" => %{"type" => "Follow", "object" => followed},
783 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
784 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
785 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
786 User.unfollow(follower, followed)
796 "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
802 with actor <- Containment.get_actor(data),
803 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
804 {:ok, activity, _} <-
805 ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
818 "object" => %{"type" => "Block", "object" => blocked},
824 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
825 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
826 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
827 User.unblock(blocker, blocked)
835 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
838 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
839 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
840 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
841 User.unfollow(blocker, blocked)
842 User.block(blocker, blocked)
852 "object" => %{"type" => "Like", "object" => object_id},
858 with actor <- Containment.get_actor(data),
859 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
860 {:ok, object} <- get_obj_helper(object_id),
861 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
868 # For Undos that don't have the complete object attached, try to find it in our database.
876 when is_binary(object) do
877 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
879 |> Map.put("object", data)
880 |> handle_incoming(options)
889 "actor" => origin_actor,
890 "object" => origin_actor,
891 "target" => target_actor
895 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
896 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
897 true <- origin_actor in target_user.also_known_as do
898 ActivityPub.move(origin_user, target_user, false)
904 def handle_incoming(_, _), do: :error
906 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
907 def get_obj_helper(id, options \\ []) do
908 case Object.normalize(id, true, options) do
909 %Object{} = object -> {:ok, object}
914 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
915 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
918 when attributed_to == ap_id do
919 with {:ok, activity} <-
924 "actor" => attributed_to,
927 {:ok, Object.normalize(activity)}
929 _ -> get_obj_helper(object_id)
933 def get_embedded_obj_helper(object_id, _) do
934 get_obj_helper(object_id)
937 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
938 with false <- String.starts_with?(in_reply_to, "http"),
939 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
940 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
946 def set_reply_to_uri(obj), do: obj
949 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
950 Based on Mastodon's ActivityPub::NoteSerializer#replies.
952 def set_replies(obj_data) do
954 with limit when limit > 0 <-
955 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
956 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
958 |> Object.self_replies()
959 |> select([o], fragment("?->>'id'", o.data))
966 set_replies(obj_data, replies_uris)
969 defp set_replies(obj, []) do
973 defp set_replies(obj, replies_uris) do
974 replies_collection = %{
975 "type" => "Collection",
976 "items" => replies_uris
979 Map.merge(obj, %{"replies" => replies_collection})
982 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
986 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
990 def replies(_), do: []
992 # Prepares the object of an outgoing create activity.
993 def prepare_object(object) do
1000 |> prepare_attachments
1004 |> strip_internal_fields
1005 |> strip_internal_tags
1011 # internal -> Mastodon
1014 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
1015 when activity_type in ["Create", "Listen"] do
1018 |> Object.normalize()
1024 |> Map.put("object", object)
1025 |> Map.merge(Utils.make_json_ld_header())
1026 |> Map.delete("bcc")
1031 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
1034 |> Object.normalize()
1037 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
1038 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
1040 data |> maybe_fix_object_url
1045 |> strip_internal_fields
1046 |> Map.merge(Utils.make_json_ld_header())
1047 |> Map.delete("bcc")
1052 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
1053 # because of course it does.
1054 def prepare_outgoing(%{"type" => "Accept"} = data) do
1055 with follow_activity <- Activity.normalize(data["object"]) do
1057 "actor" => follow_activity.actor,
1058 "object" => follow_activity.data["object"],
1059 "id" => follow_activity.data["id"],
1065 |> Map.put("object", object)
1066 |> Map.merge(Utils.make_json_ld_header())
1072 def prepare_outgoing(%{"type" => "Reject"} = data) do
1073 with follow_activity <- Activity.normalize(data["object"]) do
1075 "actor" => follow_activity.actor,
1076 "object" => follow_activity.data["object"],
1077 "id" => follow_activity.data["id"],
1083 |> Map.put("object", object)
1084 |> Map.merge(Utils.make_json_ld_header())
1090 def prepare_outgoing(%{"type" => _type} = data) do
1093 |> strip_internal_fields
1094 |> maybe_fix_object_url
1095 |> Map.merge(Utils.make_json_ld_header())
1100 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1101 with false <- String.starts_with?(object, "http"),
1102 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1103 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1105 Map.put(data, "object", external_url)
1108 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1116 def maybe_fix_object_url(data), do: data
1118 def add_hashtags(object) do
1120 (object["tag"] || [])
1122 # Expand internal representation tags into AS2 tags.
1123 tag when is_binary(tag) ->
1125 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1126 "name" => "##{tag}",
1130 # Do not process tags which are already AS2 tag objects.
1131 tag when is_map(tag) ->
1135 Map.put(object, "tag", tags)
1138 def add_mention_tags(object) do
1139 {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
1140 potential_receivers = enabled_receivers ++ disabled_receivers
1141 mentions = Enum.map(potential_receivers, &build_mention_tag/1)
1143 tags = object["tag"] || []
1144 Map.put(object, "tag", tags ++ mentions)
1147 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1148 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1151 def take_emoji_tags(%User{emoji: emoji}) do
1154 |> Enum.map(&build_emoji_tag/1)
1157 # TODO: we should probably send mtime instead of unix epoch time for updated
1158 def add_emoji_tags(%{"emoji" => emoji} = object) do
1159 tags = object["tag"] || []
1161 out = Enum.map(emoji, &build_emoji_tag/1)
1163 Map.put(object, "tag", tags ++ out)
1166 def add_emoji_tags(object), do: object
1168 defp build_emoji_tag({name, url}) do
1170 "icon" => %{"url" => url, "type" => "Image"},
1171 "name" => ":" <> name <> ":",
1173 "updated" => "1970-01-01T00:00:00Z",
1178 def set_conversation(object) do
1179 Map.put(object, "conversation", object["context"])
1182 def set_sensitive(object) do
1183 tags = object["tag"] || []
1184 Map.put(object, "sensitive", "nsfw" in tags)
1187 def set_type(%{"type" => "Answer"} = object) do
1188 Map.put(object, "type", "Note")
1191 def set_type(object), do: object
1193 def add_attributed_to(object) do
1194 attributed_to = object["attributedTo"] || object["actor"]
1195 Map.put(object, "attributedTo", attributed_to)
1198 def prepare_attachments(object) do
1201 |> Map.get("attachment", [])
1202 |> Enum.map(fn data ->
1203 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1207 "mediaType" => media_type,
1208 "name" => data["name"],
1209 "type" => "Document"
1213 Map.put(object, "attachment", attachments)
1216 def strip_internal_fields(object) do
1217 Map.drop(object, Pleroma.Constants.object_internal_fields())
1220 defp strip_internal_tags(%{"tag" => tags} = object) do
1221 tags = Enum.filter(tags, fn x -> is_map(x) end)
1223 Map.put(object, "tag", tags)
1226 defp strip_internal_tags(object), do: object
1228 def perform(:user_upgrade, user) do
1229 # we pass a fake user so that the followers collection is stripped away
1230 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1234 where: ^old_follower_address in a.recipients,
1239 "array_replace(?,?,?)",
1241 ^old_follower_address,
1242 ^user.follower_address
1247 |> Repo.update_all([])
1250 def upgrade_user_from_ap_id(ap_id) do
1251 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1252 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1253 {:ok, user} <- update_user(user, data) do
1254 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1257 %User{} = user -> {:ok, user}
1262 defp update_user(user, data) do
1264 |> User.remote_user_changeset(data)
1265 |> User.update_and_set_cache()
1268 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1269 Map.put(data, "url", url["href"])
1272 def maybe_fix_user_url(data), do: data
1274 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1276 defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
1279 defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
1280 with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
1281 {:ok, Map.put(data, "context", context)}
1284 {:error, :no_context}
1288 defp ensure_context_presence(_) do
1289 {:error, :no_context}
1292 defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
1295 defp ensure_recipients_presence(%{"object" => object} = data) do
1296 case Object.normalize(object) do
1297 %{data: %{"actor" => actor}} ->
1300 |> Map.put("to", [actor])
1301 |> Map.put("cc", data["cc"] || [])
1306 {:error, :no_object}
1313 defp ensure_recipients_presence(_) do
1314 {:error, :no_object}