X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Factivity_pub%2Ftransmogrifier.ex;h=48c3aec970cbbfc2a8049ee67513a590123aa334;hb=d3248e13e3fb13ca5b841ba31ca6fa5f3f65b501;hp=eaa716ceadc7987e2dd4c3d98f90cb867031b0da;hpb=502ba33d01bc73cc40fc6734c086fa4b58a76634;p=akkoma diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index eaa716cea..48c3aec97 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -13,31 +13,106 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do require Logger + def get_actor(%{"actor" => actor}) when is_binary(actor) do + actor + end + + def get_actor(%{"actor" => actor}) when is_list(actor) do + if is_binary(Enum.at(actor, 0)) do + Enum.at(actor, 0) + else + Enum.find(actor, fn %{"type" => type} -> type == "Person" end) + |> Map.get("id") + end + end + + def get_actor(%{"actor" => actor}) when is_map(actor) do + actor["id"] + end + + @doc """ + Checks that an imported AP object's actor matches the domain it came from. + """ + def contain_origin(id, %{"actor" => actor} = params) do + id_uri = URI.parse(id) + actor_uri = URI.parse(get_actor(params)) + + if id_uri.host == actor_uri.host do + :ok + else + :error + end + end + @doc """ Modifies an incoming AP object (mastodon format) to our internal format. """ def fix_object(object) do object - |> Map.put("actor", object["attributedTo"]) + |> fix_actor |> fix_attachments |> fix_context |> fix_in_reply_to |> fix_emoji |> fix_tag + |> fix_content_map + |> fix_likes + |> fix_addressing + end + + def fix_addressing_list(map, field) do + if is_binary(map[field]) do + map + |> Map.put(field, [map[field]]) + else + map + end + end + + def fix_addressing(map) do + map + |> fix_addressing_list("to") + |> fix_addressing_list("cc") + |> fix_addressing_list("bto") + |> fix_addressing_list("bcc") + end + + def fix_actor(%{"attributedTo" => actor} = object) do + object + |> Map.put("actor", get_actor(%{"actor" => actor})) + end + + def fix_likes(%{"likes" => likes} = object) + when is_bitstring(likes) do + # Check for standardisation + # This is what Peertube does + # curl -H 'Accept: application/activity+json' $likes | jq .totalItems + object + |> Map.put("likes", []) + |> Map.put("like_count", 0) + end + + def fix_likes(object) do + object end def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do case ActivityPub.fetch_object_from_id(in_reply_to_id) do {:ok, replied_object} -> - activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) - - object - |> Map.put("inReplyTo", replied_object.data["id"]) - |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) - |> Map.put("inReplyToStatusId", activity.id) - |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) - |> Map.put("context", replied_object.data["context"] || object["conversation"]) + with %Activity{} = activity <- + Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do + object + |> Map.put("inReplyTo", replied_object.data["id"]) + |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) + |> Map.put("inReplyToStatusId", activity.id) + |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) + |> Map.put("context", replied_object.data["context"] || object["conversation"]) + else + e -> + Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}") + object + end e -> Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}") @@ -48,8 +123,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(object), do: object def fix_context(object) do + context = object["context"] || object["conversation"] || Utils.generate_context_id() + object - |> Map.put("context", object["conversation"]) + |> Map.put("context", context) + |> Map.put("conversation", context) end def fix_attachments(object) do @@ -102,10 +180,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("tag", combined) end + # content map usually only has one language so this will do for now. + def fix_content_map(%{"contentMap" => content_map} = object) do + content_groups = Map.to_list(content_map) + {_, content} = Enum.at(content_groups, 0) + + object + |> Map.put("content", content) + end + + def fix_content_map(object), do: object + + # disallow objects with bogus IDs + def handle_incoming(%{"id" => nil}), do: :error + def handle_incoming(%{"id" => ""}), do: :error + # length of https:// = 8, should validate better, but good enough for now. + def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error + # TODO: validate those with a Ecto scheme # - tags # - emoji - def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do + def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) + when objtype in ["Article", "Note", "Video"] do + actor = get_actor(data) + + data = + Map.put(data, "actor", actor) + |> fix_addressing + with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]), %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do object = fix_object(data["object"]) @@ -137,29 +239,45 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), %User{} = follower <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do - ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true}) + if not User.locked?(followed) do + ActivityPub.accept(%{ + to: [follower.ap_id], + actor: followed.ap_id, + object: data, + local: true + }) + + User.follow(follower, followed) + end - User.follow(follower, followed) {:ok, activity} else _e -> :error end end - defp get_follow_activity(follow_object) do - cond do - is_map(follow_object) -> - {:ok, follow_object} + defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do + with true <- id =~ "follows", + %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), + %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do + {:ok, activity} + else + _ -> {:error, nil} + end + end - is_binary(follow_object) -> - object = get_obj_helper(follow_object) || ActivityPub.fetch_object_from_id(follow_object) - if object do - {:ok, object} - else - {:error, nil} - end + defp mastodon_follow_hack(_), do: {:error, nil} + + defp get_follow_activity(follow_object, followed) do + with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object), + {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do + {:ok, activity} + else + # Can't find the activity. This might a Mastodon 2.3 "Accept" + {:activity, nil} -> + mastodon_follow_hack(follow_object, followed) - true -> + _ -> {:error, nil} end end @@ -168,11 +286,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data ) do with %User{} = followed <- User.get_or_fetch_by_ap_id(actor), - {:ok, follow_activity} <- get_follow_activity(follow_object), - %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity["actor"]) do - User.follow(follower, followed) + {:ok, follow_activity} <- get_follow_activity(follow_object, followed), + %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), + {:ok, activity} <- + ActivityPub.accept(%{ + to: follow_activity.data["to"], + type: "Accept", + actor: followed.ap_id, + object: follow_activity.data["id"], + local: false + }) do + if not User.following?(follower, followed) do + {:ok, follower} = User.follow(follower, followed) + end - {:ok, data} + {:ok, activity} + else + _e -> :error end end @@ -180,11 +310,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data ) do with %User{} = followed <- User.get_or_fetch_by_ap_id(actor), - {:ok, follow_activity} <- get_follow_activity(follow_object), - %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity["actor"]), - {:ok, follow_activity} <- Utils.fetch_latest_follow(follower, followed), - {:ok, activity} <- ActivityPub.delete(follow_activity, false) do + {:ok, follow_activity} <- get_follow_activity(follow_object, followed), + %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), + {:ok, activity} <- + ActivityPub.accept(%{ + to: follow_activity.data["to"], + type: "Accept", + actor: followed.ap_id, + object: follow_activity.data["id"], + local: false + }) do + User.unfollow(follower, followed) + {:ok, activity} + else + _e -> :error end end @@ -215,18 +355,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} = + %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} = data - ) do + ) + when object_type in ["Person", "Application", "Service", "Organization"] do with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) banner = new_user_data[:info]["banner"] + locked = new_user_data[:info]["locked"] || false update_data = new_user_data |> Map.take([:name, :bio, :avatar]) - |> Map.put(:info, Map.merge(actor.info, %{"banner" => banner})) + |> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked})) actor |> User.upgrade_changeset(update_data) @@ -250,11 +392,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data ) do - object_id = - case object_id do - %{"id" => id} -> id - id -> id - end + object_id = Utils.get_ap_id(object_id) with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- @@ -277,7 +415,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), - {:ok, activity, _, _} <- ActivityPub.unannounce(actor, object, id, false) do + {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do {:ok, activity} else _e -> :error @@ -302,6 +440,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end + @ap_config Application.get_env(:pleroma, :activitypub) + @accept_blocks Keyword.get(@ap_config, :accept_blocks) + def handle_incoming( %{ "type" => "Undo", @@ -310,7 +451,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "id" => id } = _data ) do - with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), + with true <- @accept_blocks, + %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker), {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do User.unblock(blocker, blocked) @@ -323,7 +465,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data ) do - with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), + with true <- @accept_blocks, + %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), %User{} = blocker = User.get_or_fetch_by_ap_id(blocker), {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do User.unfollow(blocker, blocked) @@ -352,13 +495,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - # TODO - # Accept - def handle_incoming(_), do: :error def get_obj_helper(id) do - if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil + if object = Object.normalize(id), do: {:ok, object}, else: nil end def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do @@ -403,6 +543,44 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end + # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs, + # because of course it does. + def prepare_outgoing(%{"type" => "Accept"} = data) do + with follow_activity <- Activity.normalize(data["object"]) do + object = %{ + "actor" => follow_activity.actor, + "object" => follow_activity.data["object"], + "id" => follow_activity.data["id"], + "type" => "Follow" + } + + data = + data + |> Map.put("object", object) + |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + + {:ok, data} + end + end + + def prepare_outgoing(%{"type" => "Reject"} = data) do + with follow_activity <- Activity.normalize(data["object"]) do + object = %{ + "actor" => follow_activity.actor, + "object" => follow_activity.data["object"], + "id" => follow_activity.data["id"], + "type" => "Follow" + } + + data = + data + |> Map.put("object", object) + |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + + {:ok, data} + end + end + def prepare_outgoing(%{"type" => _type} = data) do data = data