logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: 46c7c2380c9f923b6c9e1521b025fd45aed0ae37
parent: 440b459cd14778e155cd6a3550847b1277fbd1f1
Author: lambda <pleromagit@rogerbraun.net>
Date:   Mon, 27 Aug 2018 08:29:25 +0000

Merge branch 'feature/relay' into 'develop'

message relay

Closes #144

See merge request pleroma/pleroma!264

Diffstat:

Mconfig/config.exs1+
Alib/mix/tasks/relay_follow.ex15+++++++++++++++
Alib/mix/tasks/relay_unfollow.ex15+++++++++++++++
Mlib/pleroma/user.ex30+++++++++++++++++++++++++++---
Mlib/pleroma/web/activity_pub/activity_pub.ex13++++++++++++-
Mlib/pleroma/web/activity_pub/activity_pub_controller.ex12++++++++++++
Alib/pleroma/web/activity_pub/relay.ex44++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/activity_pub/utils.ex27++++++++++++++++++++++++++-
Mlib/pleroma/web/activity_pub/views/user_view.ex29+++++++++++++++++++++++++++++
Mlib/pleroma/web/federator/federator.ex6++++++
Mlib/pleroma/web/router.ex12++++++++++++
Mtest/user_test.exs2+-
Mtest/web/twitter_api/twitter_api_controller_test.exs3++-
13 files changed, 202 insertions(+), 7 deletions(-)

diff --git a/config/config.exs b/config/config.exs @@ -61,6 +61,7 @@ config :pleroma, :instance, upload_limit: 16_000_000, registrations_open: true, federating: true, + allow_relay: true, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, quarantined_instances: [] diff --git a/lib/mix/tasks/relay_follow.ex b/lib/mix/tasks/relay_follow.ex @@ -0,0 +1,15 @@ +defmodule Mix.Tasks.RelayFollow do + use Mix.Task + require Logger + alias Pleroma.Web.ActivityPub.Relay + + @shortdoc "Follows a remote relay" + def run([target]) do + Mix.Task.run("app.start") + + :ok = Relay.follow(target) + + # put this task to sleep to allow the genserver to push out the messages + :timer.sleep(500) + end +end diff --git a/lib/mix/tasks/relay_unfollow.ex b/lib/mix/tasks/relay_unfollow.ex @@ -0,0 +1,15 @@ +defmodule Mix.Tasks.RelayUnfollow do + use Mix.Task + require Logger + alias Pleroma.Web.ActivityPub.Relay + + @shortdoc "Follows a remote relay" + def run([target]) do + Mix.Task.run("app.start") + + :ok = Relay.unfollow(target) + + # put this task to sleep to allow the genserver to push out the messages + :timer.sleep(500) + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex @@ -77,7 +77,7 @@ defmodule Pleroma.User do changes = %User{} |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar]) - |> validate_required([:name, :ap_id, :nickname]) + |> validate_required([:name, :ap_id]) |> unique_constraint(:nickname) |> validate_format(:nickname, @email_regex) |> validate_length(:bio, max: 5000) @@ -516,7 +516,8 @@ defmodule Pleroma.User do u.nickname, u.name ) - } + }, + where: not is_nil(u.nickname) ) q = @@ -595,7 +596,11 @@ defmodule Pleroma.User do end def local_user_query() do - from(u in User, where: u.local == true) + from( + u in User, + where: u.local == true, + where: not is_nil(u.nickname) + ) end def deactivate(%User{} = user) do @@ -654,6 +659,25 @@ defmodule Pleroma.User do end end + def get_or_create_instance_user do + relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay" + + if user = get_by_ap_id(relay_uri) do + user + else + changes = + %User{} + |> cast(%{}, [:ap_id, :nickname, :local]) + |> put_change(:ap_id, relay_uri) + |> put_change(:nickname, nil) + |> put_change(:local, true) + |> put_change(:follower_address, relay_uri <> "/followers") + + {:ok, user} = Repo.insert(changes) + user + end + end + # AP style def public_key_from_info(%{ "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -572,12 +572,23 @@ 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 diff --git 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 @@ -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/utils.ex b/lib/pleroma/web/activity_pub/utils.ex @@ -306,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, @@ -360,7 +378,12 @@ 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 + 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: [] @@ -369,6 +392,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do end end + def add_announce_to_object(_, object), do: {:ok, object} + def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do announcements = if is_list(object.data["announcements"]), do: object.data["announcements"], else: [] diff --git 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"]) diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Activity alias Pleroma.Web.{WebFinger, Websub} alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils require Logger @@ -69,6 +70,11 @@ defmodule Pleroma.Web.Federator do Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end) Pleroma.Web.Salmon.publish(actor, activity) + + if Mix.env() != :test do + Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) + Pleroma.Web.ActivityPub.Relay.publish(activity) + end end Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.Router do @instance Application.get_env(:pleroma, :instance) @federating Keyword.get(@instance, :federating) + @allow_relay Keyword.get(@instance, :allow_relay) @public Keyword.get(@instance, :public) @registrations_open Keyword.get(@instance, :registrations_open) @@ -293,6 +294,10 @@ defmodule Pleroma.Web.Router do get("/externalprofile/show", TwitterAPI.Controller, :external_profile) end + pipeline :ap_relay do + plug(:accepts, ["activity+json"]) + end + pipeline :ostatus do plug(:accepts, ["xml", "atom", "html", "activity+json"]) end @@ -329,6 +334,13 @@ defmodule Pleroma.Web.Router do end if @federating do + if @allow_relay do + scope "/relay", Pleroma.Web.ActivityPub do + pipe_through(:ap_relay) + get("/", ActivityPubController, :relay) + end + end + scope "/", Pleroma.Web.ActivityPub do pipe_through(:activitypub) post("/users/:nickname/inbox", ActivityPubController, :inbox) diff --git a/test/user_test.exs b/test/user_test.exs @@ -220,7 +220,7 @@ defmodule Pleroma.UserTest do end test "it has required fields" do - [:name, :nickname, :ap_id] + [:name, :ap_id] |> Enum.each(fn field -> cs = User.remote_user_creation(Map.delete(@valid_remote, field)) refute cs.valid? diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs @@ -77,7 +77,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do conn = conn_with_creds |> post(request_path, %{status: " "}) assert json_response(conn, 400) == error_response - conn = conn_with_creds |> post(request_path, %{status: "Nice meme."}) + # we post with visibility private in order to avoid triggering relay + conn = conn_with_creds |> post(request_path, %{status: "Nice meme.", visibility: "private"}) assert json_response(conn, 200) == ActivityRepresenter.to_map(Repo.one(Activity), %{user: user})