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)
661 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
664 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
665 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
670 def handle_incoming(%{"type" => "Like"} = data, _options) do
671 with {_, {:ok, cast_data_sym}} <-
673 data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
674 cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
675 :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
676 {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
677 {_, {:ok, cast_data}} <-
678 {:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
679 {_, {:ok, activity, _meta}} <-
680 {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
689 "type" => "EmojiReact",
690 "object" => object_id,
697 with actor <- Containment.get_actor(data),
698 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
699 {:ok, object} <- get_obj_helper(object_id),
700 {:ok, activity, _object} <-
701 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
709 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
712 with actor <- Containment.get_actor(data),
713 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
714 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
715 public <- Visibility.is_public?(data),
716 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
724 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
728 when object_type in [
734 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
735 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
738 |> User.remote_user_changeset(new_user_data)
739 |> User.update_and_set_cache()
741 ActivityPub.update(%{
743 to: data["to"] || [],
744 cc: data["cc"] || [],
747 activity_id: data["id"]
756 # TODO: We presently assume that any actor on the same origin domain as the object being
757 # deleted has the rights to delete that object. A better way to validate whether or not
758 # the object should be deleted is to refetch the object URI, which should return either
759 # an error or a tombstone. This would allow us to verify that a deletion actually took
762 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
765 object_id = Utils.get_ap_id(object_id)
767 with actor <- Containment.get_actor(data),
768 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
769 {:ok, object} <- get_obj_helper(object_id),
770 :ok <- Containment.contain_origin(actor.ap_id, object.data),
772 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
776 case User.get_cached_by_ap_id(object_id) do
777 %User{ap_id: ^actor} = user ->
792 "object" => %{"type" => "Announce", "object" => object_id},
798 with actor <- Containment.get_actor(data),
799 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
800 {:ok, object} <- get_obj_helper(object_id),
801 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
811 "object" => %{"type" => "Follow", "object" => followed},
817 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
818 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
819 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
820 User.unfollow(follower, followed)
830 "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
836 with actor <- Containment.get_actor(data),
837 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
838 {:ok, activity, _} <-
839 ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
852 "object" => %{"type" => "Block", "object" => blocked},
858 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
859 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
860 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
861 User.unblock(blocker, blocked)
869 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
872 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
873 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
874 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
875 User.unfollow(blocker, blocked)
876 User.block(blocker, blocked)
886 "object" => %{"type" => "Like", "object" => object_id},
892 with actor <- Containment.get_actor(data),
893 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
894 {:ok, object} <- get_obj_helper(object_id),
895 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
902 # For Undos that don't have the complete object attached, try to find it in our database.
910 when is_binary(object) do
911 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
913 |> Map.put("object", data)
914 |> handle_incoming(options)
923 "actor" => origin_actor,
924 "object" => origin_actor,
925 "target" => target_actor
929 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
930 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
931 true <- origin_actor in target_user.also_known_as do
932 ActivityPub.move(origin_user, target_user, false)
938 def handle_incoming(_, _), do: :error
940 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
941 def get_obj_helper(id, options \\ []) do
942 case Object.normalize(id, true, options) do
943 %Object{} = object -> {:ok, object}
948 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
949 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
952 when attributed_to == ap_id do
953 with {:ok, activity} <-
958 "actor" => attributed_to,
961 {:ok, Object.normalize(activity)}
963 _ -> get_obj_helper(object_id)
967 def get_embedded_obj_helper(object_id, _) do
968 get_obj_helper(object_id)
971 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
972 with false <- String.starts_with?(in_reply_to, "http"),
973 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
974 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
980 def set_reply_to_uri(obj), do: obj
983 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
984 Based on Mastodon's ActivityPub::NoteSerializer#replies.
986 def set_replies(obj_data) do
988 with limit when limit > 0 <-
989 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
990 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
992 |> Object.self_replies()
993 |> select([o], fragment("?->>'id'", o.data))
1000 set_replies(obj_data, replies_uris)
1003 defp set_replies(obj, []) do
1007 defp set_replies(obj, replies_uris) do
1008 replies_collection = %{
1009 "type" => "Collection",
1010 "items" => replies_uris
1013 Map.merge(obj, %{"replies" => replies_collection})
1016 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
1020 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
1024 def replies(_), do: []
1026 # Prepares the object of an outgoing create activity.
1027 def prepare_object(object) do
1033 |> add_attributed_to
1034 |> prepare_attachments
1038 |> strip_internal_fields
1039 |> strip_internal_tags
1045 # internal -> Mastodon
1048 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
1049 when activity_type in ["Create", "Listen"] do
1052 |> Object.normalize()
1058 |> Map.put("object", object)
1059 |> Map.merge(Utils.make_json_ld_header())
1060 |> Map.delete("bcc")
1065 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
1068 |> Object.normalize()
1071 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
1072 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
1074 data |> maybe_fix_object_url
1079 |> strip_internal_fields
1080 |> Map.merge(Utils.make_json_ld_header())
1081 |> Map.delete("bcc")
1086 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
1087 # because of course it does.
1088 def prepare_outgoing(%{"type" => "Accept"} = data) do
1089 with follow_activity <- Activity.normalize(data["object"]) do
1091 "actor" => follow_activity.actor,
1092 "object" => follow_activity.data["object"],
1093 "id" => follow_activity.data["id"],
1099 |> Map.put("object", object)
1100 |> Map.merge(Utils.make_json_ld_header())
1106 def prepare_outgoing(%{"type" => "Reject"} = data) do
1107 with follow_activity <- Activity.normalize(data["object"]) do
1109 "actor" => follow_activity.actor,
1110 "object" => follow_activity.data["object"],
1111 "id" => follow_activity.data["id"],
1117 |> Map.put("object", object)
1118 |> Map.merge(Utils.make_json_ld_header())
1124 def prepare_outgoing(%{"type" => _type} = data) do
1127 |> strip_internal_fields
1128 |> maybe_fix_object_url
1129 |> Map.merge(Utils.make_json_ld_header())
1134 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1135 with false <- String.starts_with?(object, "http"),
1136 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1137 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1139 Map.put(data, "object", external_url)
1142 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1150 def maybe_fix_object_url(data), do: data
1152 def add_hashtags(object) do
1154 (object["tag"] || [])
1156 # Expand internal representation tags into AS2 tags.
1157 tag when is_binary(tag) ->
1159 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1160 "name" => "##{tag}",
1164 # Do not process tags which are already AS2 tag objects.
1165 tag when is_map(tag) ->
1169 Map.put(object, "tag", tags)
1172 def add_mention_tags(object) do
1173 {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
1174 potential_receivers = enabled_receivers ++ disabled_receivers
1175 mentions = Enum.map(potential_receivers, &build_mention_tag/1)
1177 tags = object["tag"] || []
1178 Map.put(object, "tag", tags ++ mentions)
1181 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1182 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1185 def take_emoji_tags(%User{emoji: emoji}) do
1188 |> Enum.map(&build_emoji_tag/1)
1191 # TODO: we should probably send mtime instead of unix epoch time for updated
1192 def add_emoji_tags(%{"emoji" => emoji} = object) do
1193 tags = object["tag"] || []
1195 out = Enum.map(emoji, &build_emoji_tag/1)
1197 Map.put(object, "tag", tags ++ out)
1200 def add_emoji_tags(object), do: object
1202 defp build_emoji_tag({name, url}) do
1204 "icon" => %{"url" => url, "type" => "Image"},
1205 "name" => ":" <> name <> ":",
1207 "updated" => "1970-01-01T00:00:00Z",
1212 def set_conversation(object) do
1213 Map.put(object, "conversation", object["context"])
1216 def set_sensitive(object) do
1217 tags = object["tag"] || []
1218 Map.put(object, "sensitive", "nsfw" in tags)
1221 def set_type(%{"type" => "Answer"} = object) do
1222 Map.put(object, "type", "Note")
1225 def set_type(object), do: object
1227 def add_attributed_to(object) do
1228 attributed_to = object["attributedTo"] || object["actor"]
1229 Map.put(object, "attributedTo", attributed_to)
1232 def prepare_attachments(object) do
1235 |> Map.get("attachment", [])
1236 |> Enum.map(fn data ->
1237 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1241 "mediaType" => media_type,
1242 "name" => data["name"],
1243 "type" => "Document"
1247 Map.put(object, "attachment", attachments)
1250 def strip_internal_fields(object) do
1251 Map.drop(object, Pleroma.Constants.object_internal_fields())
1254 defp strip_internal_tags(%{"tag" => tags} = object) do
1255 tags = Enum.filter(tags, fn x -> is_map(x) end)
1257 Map.put(object, "tag", tags)
1260 defp strip_internal_tags(object), do: object
1262 def perform(:user_upgrade, user) do
1263 # we pass a fake user so that the followers collection is stripped away
1264 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1268 where: ^old_follower_address in a.recipients,
1273 "array_replace(?,?,?)",
1275 ^old_follower_address,
1276 ^user.follower_address
1281 |> Repo.update_all([])
1284 def upgrade_user_from_ap_id(ap_id) do
1285 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1286 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1287 {:ok, user} <- update_user(user, data) do
1288 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1291 %User{} = user -> {:ok, user}
1296 defp update_user(user, data) do
1298 |> User.remote_user_changeset(data)
1299 |> User.update_and_set_cache()
1302 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1303 Map.put(data, "url", url["href"])
1306 def maybe_fix_user_url(data), do: data
1308 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1310 defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
1313 defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
1314 with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
1315 {:ok, Map.put(data, "context", context)}
1318 {:error, :no_context}
1322 defp ensure_context_presence(_) do
1323 {:error, :no_context}
1326 defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
1329 defp ensure_recipients_presence(%{"object" => object} = data) do
1330 case Object.normalize(object) do
1331 %{data: %{"actor" => actor}} ->
1334 |> Map.put("to", [actor])
1335 |> Map.put("cc", data["cc"] || [])
1340 {:error, :no_object}
1347 defp ensure_recipients_presence(_) do
1348 {:error, :no_object}