diff options
Diffstat (limited to 'lib/pleroma/web/activity_pub')
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 70 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub_controller.ex | 12 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/relay.ex | 44 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/transmogrifier.ex | 50 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/utils.ex | 47 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/views/user_view.ex | 32 |
6 files changed, 237 insertions, 18 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index ec605b694..81c11dd76 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -12,8 +12,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do @instance Application.get_env(:pleroma, :instance) - def get_recipients(data) do - (data["to"] || []) ++ (data["cc"] || []) + # For Announce activities, we filter the recipients based on following status for any actors + # that match actual users. See issue #164 for more information about why this is necessary. + defp get_recipients(%{"type" => "Announce"} = data) do + to = data["to"] || [] + cc = data["cc"] || [] + recipients = to ++ cc + actor = User.get_cached_by_ap_id(data["actor"]) + + recipients + |> Enum.filter(fn recipient -> + case User.get_cached_by_ap_id(recipient) do + nil -> + true + + user -> + User.following?(user, actor) + end + end) + + {recipients, to, cc} + end + + defp get_recipients(data) do + to = data["to"] || [] + cc = data["cc"] || [] + recipients = to ++ cc + {recipients, to, cc} end defp check_actor_is_active(actor) do @@ -35,12 +60,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do :ok <- check_actor_is_active(map["actor"]), {:ok, map} <- MRF.filter(map), :ok <- insert_full_object(map) do + {recipients, _, _} = get_recipients(map) + {:ok, activity} = Repo.insert(%Activity{ data: map, local: local, actor: map["actor"], - recipients: get_recipients(map) + recipients: recipients }) Notification.create_notifications(activity) @@ -381,6 +408,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_tag(query, _), do: query + defp restrict_to_cc(query, recipients_to, recipients_cc) do + from( + activity in query, + where: + fragment( + "(?->'to' \\?| ?) or (?->'cc' \\?| ?)", + activity.data, + ^recipients_to, + activity.data, + ^recipients_cc + ) + ) + end + defp restrict_recipients(query, [], _user), do: query defp restrict_recipients(query, recipients, nil) do @@ -522,6 +563,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> Enum.reverse() end + def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do + fetch_activities_query([], opts) + |> restrict_to_cc(recipients_to, recipients_cc) + |> Repo.all() + |> Enum.reverse() + end + def upload(file) do data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media]) Repo.insert(%Object{data: data}) @@ -554,18 +602,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "locked" => locked }, avatar: avatar, - nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}", name: data["name"], follower_address: data["followers"], bio: data["summary"] } + # nickname can be nil because of virtual actors + user_data = + if data["preferredUsername"] do + Map.put( + user_data, + :nickname, + "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" + ) + else + Map.put(user_data, :nickname, nil) + end + {:ok, user_data} end def fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, %{status_code: 200, body: body}} <- - @httpoison.get(ap_id, Accept: "application/activity+json"), + @httpoison.get(ap_id, [Accept: "application/activity+json"], follow_redirect: true), {:ok, data} <- Jason.decode(body) do user_data_from_user_object(data) else @@ -688,6 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "actor" => data["attributedTo"], "object" => data }, + :ok <- Transmogrifier.contain_origin(id, params), {:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, Object.normalize(activity.data["object"])} else diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index d337532d0..52b2a467e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -3,6 +3,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.{User, Object} alias Pleroma.Web.ActivityPub.{ObjectView, UserView} alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.Federator require Logger @@ -107,6 +108,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do json(conn, "ok") end + def relay(conn, params) do + with %User{} = user <- Relay.get_actor(), + {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("user.json", %{user: user})) + else + nil -> {:error, :not_found} + end + end + def errors(conn, {:error, :not_found}) do conn |> put_status(404) diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex new file mode 100644 index 000000000..d30853d62 --- /dev/null +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -0,0 +1,44 @@ +defmodule Pleroma.Web.ActivityPub.Relay do + alias Pleroma.{User, Object, Activity} + alias Pleroma.Web.ActivityPub.ActivityPub + require Logger + + def get_actor do + User.get_or_create_instance_user() + end + + def follow(target_instance) do + with %User{} = local_user <- get_actor(), + %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), + {:ok, activity} <- ActivityPub.follow(local_user, target_user) do + Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") + else + e -> Logger.error("error: #{inspect(e)}") + end + + :ok + end + + def unfollow(target_instance) do + with %User{} = local_user <- get_actor(), + %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), + {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do + Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") + else + e -> Logger.error("error: #{inspect(e)}") + end + + :ok + end + + def publish(%Activity{data: %{"type" => "Create"}} = activity) do + with %User{} = user <- get_actor(), + %Object{} = object <- Object.normalize(activity.data["object"]["id"]) do + ActivityPub.announce(user, object) + else + e -> Logger.error("error: #{inspect(e)}") + end + end + + def publish(_), do: nil +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index e5fb6e033..4a3a82195 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -18,16 +18,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def get_actor(%{"actor" => actor}) when is_list(actor) do - Enum.at(actor, 0) + 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 - def get_actor(%{"actor" => actor_list}) do - Enum.find(actor_list, fn %{"type" => type} -> type == "Person" end) - |> Map.get("id") + @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 """ @@ -42,6 +56,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_emoji |> fix_tag |> fix_content_map + |> fix_likes |> fix_addressing end @@ -67,6 +82,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> 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 @@ -94,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 @@ -159,11 +191,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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" => objtype} = object} = data) - when objtype in ["Article", "Note"] do + when objtype in ["Article", "Note", "Video"] do actor = get_actor(data) data = diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 8b41a3bec..0664b5a2e 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -128,7 +128,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data}) - when is_map(object_data) and type in ["Article", "Note"] do + when is_map(object_data) and type in ["Article", "Note", "Video"] do with {:ok, _} <- Object.create(object_data) do :ok end @@ -204,13 +204,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do end def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do - with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do + likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] + + with likes <- [actor | likes] |> Enum.uniq() do update_likes_in_object(likes, object) end end def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do - with likes <- (object.data["likes"] || []) |> List.delete(actor) do + likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] + + with likes <- likes |> List.delete(actor) do update_likes_in_object(likes, object) end end @@ -302,6 +306,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Make announce activity data for the given actor and object """ + # for relayed messages, we only want to send to subscribers + def make_announce_data( + %User{ap_id: ap_id, nickname: nil} = user, + %Object{data: %{"id" => id}} = object, + activity_id + ) do + data = %{ + "type" => "Announce", + "actor" => ap_id, + "object" => id, + "to" => [user.follower_address], + "cc" => [], + "context" => object.data["context"] + } + + if activity_id, do: Map.put(data, "id", activity_id), else: data + end + def make_announce_data( %User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, @@ -356,14 +378,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do if activity_id, do: Map.put(data, "id", activity_id), else: data end - def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do - with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do + def add_announce_to_object( + %Activity{ + data: %{"actor" => actor, "cc" => ["https://www.w3.org/ns/activitystreams#Public"]} + }, + object + ) do + announcements = + if is_list(object.data["announcements"]), do: object.data["announcements"], else: [] + + with announcements <- [actor | announcements] |> Enum.uniq() do update_element_in_object("announcement", announcements, object) end end + def add_announce_to_object(_, object), do: {:ok, object} + def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do - with announcements <- (object.data["announcements"] || []) |> List.delete(actor) do + announcements = + if is_list(object.data["announcements"]), do: object.data["announcements"], else: [] + + with announcements <- announcements |> List.delete(actor) do update_element_in_object("announcement", announcements, object) end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 0b1d5a9fa..16419e1b7 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -9,6 +9,35 @@ defmodule Pleroma.Web.ActivityPub.UserView do alias Pleroma.Web.ActivityPub.Utils import Ecto.Query + # the instance itself is not a Person, but instead an Application + def render("user.json", %{user: %{nickname: nil} = user}) do + {:ok, user} = WebFinger.ensure_keys_present(user) + {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) + public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) + public_key = :public_key.pem_encode([public_key]) + + %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => user.ap_id, + "type" => "Application", + "following" => "#{user.ap_id}/following", + "followers" => "#{user.ap_id}/followers", + "inbox" => "#{user.ap_id}/inbox", + "name" => "Pleroma", + "summary" => "Virtual actor for Pleroma relay", + "url" => user.ap_id, + "manuallyApprovesFollowers" => false, + "publicKey" => %{ + "id" => "#{user.ap_id}#main-key", + "owner" => user.ap_id, + "publicKeyPem" => public_key + }, + "endpoints" => %{ + "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox" + } + } + end + def render("user.json", %{user: user}) do {:ok, user} = WebFinger.ensure_keys_present(user) {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) @@ -42,7 +71,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do "image" => %{ "type" => "Image", "url" => User.banner_url(user) - } + }, + "tag" => user.info["source_data"]["tag"] || [] } |> Map.merge(Utils.make_json_ld_header()) end |