1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
7 A module to handle coding from internal to wire ActivityPub and back.
11 alias Pleroma.Object.Containment
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
23 Modifies an incoming AP object (mastodon format) to our internal format.
25 def fix_object(object) do
40 def fix_summary(%{"summary" => nil} = object) do
42 |> Map.put("summary", "")
45 def fix_summary(%{"summary" => _} = object) do
46 # summary is present, nothing to do
50 def fix_summary(object) do
52 |> Map.put("summary", "")
55 def fix_addressing_list(map, field) do
57 is_binary(map[field]) ->
58 Map.put(map, field, [map[field]])
61 Map.put(map, field, [])
68 def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
71 |> Enum.filter(fn x -> x in explicit_mentions end)
75 |> Enum.filter(fn x -> x not in explicit_mentions end)
82 |> Map.put("to", explicit_to)
83 |> Map.put("cc", final_cc)
86 def fix_explicit_addressing(object, _explicit_mentions), do: object
88 # if directMessage flag is set to true, leave the addressing alone
89 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
91 def fix_explicit_addressing(object) do
94 |> Utils.determine_explicit_mentions()
96 explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
99 |> fix_explicit_addressing(explicit_mentions)
102 # if as:Public is addressed, then make sure the followers collection is also addressed
103 # so that the activities will be delivered to local users.
104 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
105 recipients = to ++ cc
107 if followers_collection not in recipients do
109 "https://www.w3.org/ns/activitystreams#Public" in cc ->
110 to = to ++ [followers_collection]
111 Map.put(object, "to", to)
113 "https://www.w3.org/ns/activitystreams#Public" in to ->
114 cc = cc ++ [followers_collection]
115 Map.put(object, "cc", cc)
125 def fix_implicit_addressing(object, _), do: object
127 def fix_addressing(object) do
128 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
129 followers_collection = User.ap_followers(user)
132 |> fix_addressing_list("to")
133 |> fix_addressing_list("cc")
134 |> fix_addressing_list("bto")
135 |> fix_addressing_list("bcc")
136 |> fix_explicit_addressing
137 |> fix_implicit_addressing(followers_collection)
140 def fix_actor(%{"attributedTo" => actor} = object) do
142 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
145 # Check for standardisation
146 # This is what Peertube does
147 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
148 # Prismo returns only an integer (count) as "likes"
149 def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
151 |> Map.put("likes", [])
152 |> Map.put("like_count", 0)
155 def fix_likes(object) do
159 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
160 when not is_nil(in_reply_to) do
163 is_bitstring(in_reply_to) ->
166 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
169 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
170 Enum.at(in_reply_to, 0)
172 # Maybe I should output an error too?
177 case get_obj_helper(in_reply_to_id) do
178 {:ok, replied_object} ->
179 with %Activity{} = _activity <-
180 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
182 |> Map.put("inReplyTo", replied_object.data["id"])
183 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
184 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
185 |> Map.put("context", replied_object.data["context"] || object["conversation"])
188 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
193 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
198 def fix_in_reply_to(object), do: object
200 def fix_context(object) do
201 context = object["context"] || object["conversation"] || Utils.generate_context_id()
204 |> Map.put("context", context)
205 |> Map.put("conversation", context)
208 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
211 |> Enum.map(fn data ->
212 media_type = data["mediaType"] || data["mimeType"]
213 href = data["url"] || data["href"]
215 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
218 |> Map.put("mediaType", media_type)
219 |> Map.put("url", url)
223 |> Map.put("attachment", attachments)
226 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
227 Map.put(object, "attachment", [attachment])
231 def fix_attachments(object), do: object
233 def fix_url(%{"url" => url} = object) when is_map(url) do
235 |> Map.put("url", url["href"])
238 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
239 first_element = Enum.at(url, 0)
243 |> Enum.filter(fn x -> is_map(x) end)
244 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
248 |> Map.put("attachment", [first_element])
249 |> Map.put("url", link_element["href"])
252 def fix_url(%{"type" => object_type, "url" => url} = object)
253 when object_type != "Video" and is_list(url) do
254 first_element = Enum.at(url, 0)
258 is_bitstring(first_element) -> first_element
259 is_map(first_element) -> first_element["href"] || ""
264 |> Map.put("url", url_string)
267 def fix_url(object), do: object
269 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
270 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
274 |> Enum.reduce(%{}, fn data, mapping ->
275 name = String.trim(data["name"], ":")
277 mapping |> Map.put(name, data["icon"]["url"])
280 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
281 emoji = Map.merge(object["emoji"] || %{}, emoji)
284 |> Map.put("emoji", emoji)
287 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
288 name = String.trim(tag["name"], ":")
289 emoji = %{name => tag["icon"]["url"]}
292 |> Map.put("emoji", emoji)
295 def fix_emoji(object), do: object
297 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
300 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
301 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
303 combined = tag ++ tags
306 |> Map.put("tag", combined)
309 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
310 combined = [tag, String.slice(hashtag, 1..-1)]
313 |> Map.put("tag", combined)
316 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
318 def fix_tag(object), do: object
320 # content map usually only has one language so this will do for now.
321 def fix_content_map(%{"contentMap" => content_map} = object) do
322 content_groups = Map.to_list(content_map)
323 {_, content} = Enum.at(content_groups, 0)
326 |> Map.put("content", content)
329 def fix_content_map(object), do: object
331 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
332 with true <- id =~ "follows",
333 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
334 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
341 defp mastodon_follow_hack(_, _), do: {:error, nil}
343 defp get_follow_activity(follow_object, followed) do
344 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
345 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
348 # Can't find the activity. This might a Mastodon 2.3 "Accept"
350 mastodon_follow_hack(follow_object, followed)
357 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
359 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
360 with context <- data["context"] || Utils.generate_context_id(),
361 content <- data["content"] || "",
362 %User{} = actor <- User.get_cached_by_ap_id(actor),
364 # Reduce the object list to find the reported user.
366 Enum.reduce_while(objects, nil, fn ap_id, _ ->
367 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
374 # Remove the reported user from the object list.
375 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
383 "cc" => [account.ap_id]
387 ActivityPub.flag(params)
391 # disallow objects with bogus IDs
392 def handle_incoming(%{"id" => nil}), do: :error
393 def handle_incoming(%{"id" => ""}), do: :error
394 # length of https:// = 8, should validate better, but good enough for now.
395 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
397 # TODO: validate those with a Ecto scheme
400 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
401 when objtype in ["Article", "Note", "Video", "Page"] do
402 actor = Containment.get_actor(data)
405 Map.put(data, "actor", actor)
408 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
409 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
410 object = fix_object(data["object"])
416 context: object["conversation"],
418 published: data["published"],
427 ActivityPub.create(params)
429 %Activity{} = activity -> {:ok, activity}
435 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
437 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
438 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
439 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
440 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
441 {:user_blocked, false} <-
442 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
443 {:user_locked, false} <- {:user_locked, User.locked?(followed)},
444 {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
445 ActivityPub.accept(%{
446 to: [follower.ap_id],
452 {:user_blocked, true} ->
453 {:ok, _} = Utils.update_follow_state(activity, "reject")
455 ActivityPub.reject(%{
456 to: [follower.ap_id],
462 {:follow, {:error, _}} ->
463 {:ok, _} = Utils.update_follow_state(activity, "reject")
465 ActivityPub.reject(%{
466 to: [follower.ap_id],
472 {:user_locked, true} ->
484 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
486 with actor <- Containment.get_actor(data),
487 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
488 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
489 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
490 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
492 ActivityPub.accept(%{
493 to: follow_activity.data["to"],
496 object: follow_activity.data["id"],
499 if not User.following?(follower, followed) do
500 {:ok, _follower} = User.follow(follower, followed)
510 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
512 with actor <- Containment.get_actor(data),
513 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
514 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
515 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
516 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
518 ActivityPub.reject(%{
519 to: follow_activity.data["to"],
522 object: follow_activity.data["id"],
525 User.unfollow(follower, followed)
534 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
536 with actor <- Containment.get_actor(data),
537 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
538 {:ok, object} <- get_obj_helper(object_id),
539 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
547 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
549 with actor <- Containment.get_actor(data),
550 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
551 {:ok, object} <- get_obj_helper(object_id),
552 public <- Visibility.is_public?(data),
553 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
561 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
564 when object_type in ["Person", "Application", "Service", "Organization"] do
565 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
566 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
568 banner = new_user_data[:info]["banner"]
569 locked = new_user_data[:info]["locked"] || false
573 |> Map.take([:name, :bio, :avatar])
574 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
577 |> User.upgrade_changeset(update_data)
578 |> User.update_and_set_cache()
580 ActivityPub.update(%{
582 to: data["to"] || [],
583 cc: data["cc"] || [],
594 # TODO: We presently assume that any actor on the same origin domain as the object being
595 # deleted has the rights to delete that object. A better way to validate whether or not
596 # the object should be deleted is to refetch the object URI, which should return either
597 # an error or a tombstone. This would allow us to verify that a deletion actually took
600 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
602 object_id = Utils.get_ap_id(object_id)
604 with actor <- Containment.get_actor(data),
605 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
606 {:ok, object} <- get_obj_helper(object_id),
607 :ok <- Containment.contain_origin(actor.ap_id, object.data),
608 {:ok, activity} <- ActivityPub.delete(object, false) do
618 "object" => %{"type" => "Announce", "object" => object_id},
623 with actor <- Containment.get_actor(data),
624 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
625 {:ok, object} <- get_obj_helper(object_id),
626 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
636 "object" => %{"type" => "Follow", "object" => followed},
641 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
642 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
643 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
644 User.unfollow(follower, followed)
654 "object" => %{"type" => "Block", "object" => blocked},
659 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
660 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
661 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
662 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
663 User.unblock(blocker, blocked)
671 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
673 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
674 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
675 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
676 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
677 User.unfollow(blocker, blocked)
678 User.block(blocker, blocked)
688 "object" => %{"type" => "Like", "object" => object_id},
693 with actor <- Containment.get_actor(data),
694 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
695 {:ok, object} <- get_obj_helper(object_id),
696 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
703 def handle_incoming(_), do: :error
705 def get_obj_helper(id) do
706 if object = Object.normalize(id), do: {:ok, object}, else: nil
709 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
710 with false <- String.starts_with?(in_reply_to, "http"),
711 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
712 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
718 def set_reply_to_uri(obj), do: obj
720 # Prepares the object of an outgoing create activity.
721 def prepare_object(object) do
729 |> prepare_attachments
732 |> strip_internal_fields
733 |> strip_internal_tags
738 # internal -> Mastodon
741 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
744 |> Object.normalize()
750 |> Map.put("object", object)
751 |> Map.merge(Utils.make_json_ld_header())
757 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
758 # because of course it does.
759 def prepare_outgoing(%{"type" => "Accept"} = data) do
760 with follow_activity <- Activity.normalize(data["object"]) do
762 "actor" => follow_activity.actor,
763 "object" => follow_activity.data["object"],
764 "id" => follow_activity.data["id"],
770 |> Map.put("object", object)
771 |> Map.merge(Utils.make_json_ld_header())
777 def prepare_outgoing(%{"type" => "Reject"} = data) do
778 with follow_activity <- Activity.normalize(data["object"]) do
780 "actor" => follow_activity.actor,
781 "object" => follow_activity.data["object"],
782 "id" => follow_activity.data["id"],
788 |> Map.put("object", object)
789 |> Map.merge(Utils.make_json_ld_header())
795 def prepare_outgoing(%{"type" => _type} = data) do
798 |> strip_internal_fields
799 |> maybe_fix_object_url
800 |> Map.merge(Utils.make_json_ld_header())
805 def maybe_fix_object_url(data) do
806 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
807 case get_obj_helper(data["object"]) do
808 {:ok, relative_object} ->
809 if relative_object.data["external_url"] do
812 |> Map.put("object", relative_object.data["external_url"])
818 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
826 def add_hashtags(object) do
828 (object["tag"] || [])
830 # Expand internal representation tags into AS2 tags.
831 tag when is_binary(tag) ->
833 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
838 # Do not process tags which are already AS2 tag objects.
839 tag when is_map(tag) ->
844 |> Map.put("tag", tags)
847 def add_mention_tags(object) do
850 |> Utils.get_notified_from_object()
851 |> Enum.map(fn user ->
852 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
855 tags = object["tag"] || []
858 |> Map.put("tag", tags ++ mentions)
861 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
862 user_info = add_emoji_tags(user_info)
865 |> Map.put(:info, user_info)
868 # TODO: we should probably send mtime instead of unix epoch time for updated
869 def add_emoji_tags(%{"emoji" => emoji} = object) do
870 tags = object["tag"] || []
874 |> Enum.map(fn {name, url} ->
876 "icon" => %{"url" => url, "type" => "Image"},
877 "name" => ":" <> name <> ":",
879 "updated" => "1970-01-01T00:00:00Z",
885 |> Map.put("tag", tags ++ out)
888 def add_emoji_tags(object) do
892 def set_conversation(object) do
893 Map.put(object, "conversation", object["context"])
896 def set_sensitive(object) do
897 tags = object["tag"] || []
898 Map.put(object, "sensitive", "nsfw" in tags)
901 def add_attributed_to(object) do
902 attributed_to = object["attributedTo"] || object["actor"]
905 |> Map.put("attributedTo", attributed_to)
908 def add_likes(%{"id" => id, "like_count" => likes} = object) do
910 "id" => "#{id}/likes",
911 "first" => "#{id}/likes?page=1",
912 "type" => "OrderedCollection",
913 "totalItems" => likes
917 |> Map.put("likes", likes)
920 def add_likes(object) do
924 def prepare_attachments(object) do
926 (object["attachment"] || [])
927 |> Enum.map(fn data ->
928 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
929 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
933 |> Map.put("attachment", attachments)
936 defp strip_internal_fields(object) do
941 "announcement_count",
944 "deleted_activity_id"
948 defp strip_internal_tags(%{"tag" => tags} = object) do
951 |> Enum.filter(fn x -> is_map(x) end)
954 |> Map.put("tag", tags)
957 defp strip_internal_tags(object), do: object
959 def perform(:user_upgrade, user) do
960 # we pass a fake user so that the followers collection is stripped away
961 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
966 where: ^old_follower_address in u.following,
971 "array_replace(?,?,?)",
973 ^old_follower_address,
974 ^user.follower_address
980 Repo.update_all(q, [])
982 maybe_retire_websub(user.ap_id)
987 where: ^old_follower_address in a.recipients,
992 "array_replace(?,?,?)",
994 ^old_follower_address,
995 ^user.follower_address
1001 Repo.update_all(q, [])
1004 def upgrade_user_from_ap_id(ap_id) do
1005 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1006 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1007 already_ap <- User.ap_enabled?(user),
1008 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1009 unless already_ap do
1010 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1015 %User{} = user -> {:ok, user}
1020 def maybe_retire_websub(ap_id) do
1021 # some sanity checks
1022 if is_binary(ap_id) && String.length(ap_id) > 8 do
1025 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1026 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1033 def maybe_fix_user_url(data) do
1034 if is_map(data["url"]) do
1035 Map.put(data, "url", data["url"]["href"])
1041 def maybe_fix_user_object(data) do
1043 |> maybe_fix_user_url