diff options
| -rw-r--r-- | config/config.exs | 1 | ||||
| -rw-r--r-- | lib/mix/tasks/relay_follow.ex | 15 | ||||
| -rw-r--r-- | lib/mix/tasks/relay_unfollow.ex | 15 | ||||
| -rw-r--r-- | lib/pleroma/user.ex | 30 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 13 | ||||
| -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/utils.ex | 27 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/views/user_view.ex | 29 | ||||
| -rw-r--r-- | lib/pleroma/web/federator/federator.ex | 6 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 12 | ||||
| -rw-r--r-- | test/user_test.exs | 2 | ||||
| -rw-r--r-- | test/web/twitter_api/twitter_api_controller_test.exs | 3 | 
13 files changed, 202 insertions, 7 deletions
| diff --git a/config/config.exs b/config/config.exs index 6b1e31398..eaf20e8f9 100644 --- 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 new file mode 100644 index 000000000..ac6f20924 --- /dev/null +++ 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 new file mode 100644 index 000000000..4621ace83 --- /dev/null +++ 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 index 7d7f3b23e..88293a4f3 100644 --- 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 index 298d7817a..68b398786 100644 --- 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 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/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 7cdc1656b..0664b5a2e 100644 --- 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 index fc76f2940..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"]) diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index ccefb0bdf..078f3ec11 100644 --- 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 index 68e159f6a..927323794 100644 --- 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 index 352a16687..5c61b0930 100644 --- 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 index 3a035e298..87bcdaf71 100644 --- 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}) | 
