Commit 46c7c238 authored by lain's avatar lain

Merge branch 'feature/relay' into 'develop'

message relay

Closes #144

See merge request pleroma/pleroma!264
parents 440b459c 0f5bff8c
...@@ -61,6 +61,7 @@ ...@@ -61,6 +61,7 @@
upload_limit: 16_000_000, upload_limit: 16_000_000,
registrations_open: true, registrations_open: true,
federating: true, federating: true,
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true, public: true,
quarantined_instances: [] quarantined_instances: []
......
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
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
...@@ -77,7 +77,7 @@ def remote_user_creation(params) do ...@@ -77,7 +77,7 @@ def remote_user_creation(params) do
changes = changes =
%User{} %User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar]) |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> validate_required([:name, :ap_id, :nickname]) |> validate_required([:name, :ap_id])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex) |> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000) |> validate_length(:bio, max: 5000)
...@@ -516,7 +516,8 @@ def search(query, resolve) do ...@@ -516,7 +516,8 @@ def search(query, resolve) do
u.nickname, u.nickname,
u.name u.name
) )
} },
where: not is_nil(u.nickname)
) )
q = q =
...@@ -595,7 +596,11 @@ def unblock_domain(user, domain) do ...@@ -595,7 +596,11 @@ def unblock_domain(user, domain) do
end end
def local_user_query() do 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 end
def deactivate(%User{} = user) do def deactivate(%User{} = user) do
...@@ -654,6 +659,25 @@ def get_or_fetch_by_ap_id(ap_id) do ...@@ -654,6 +659,25 @@ def get_or_fetch_by_ap_id(ap_id) do
end end
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 # AP style
def public_key_from_info(%{ def public_key_from_info(%{
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}} "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
......
...@@ -572,12 +572,23 @@ def user_data_from_user_object(data) do ...@@ -572,12 +572,23 @@ def user_data_from_user_object(data) do
"locked" => locked "locked" => locked
}, },
avatar: avatar, avatar: avatar,
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
name: data["name"], name: data["name"],
follower_address: data["followers"], follower_address: data["followers"],
bio: data["summary"] 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} {:ok, user_data}
end end
......
...@@ -3,6 +3,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do ...@@ -3,6 +3,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.{User, Object} alias Pleroma.{User, Object}
alias Pleroma.Web.ActivityPub.{ObjectView, UserView} alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
require Logger require Logger
...@@ -107,6 +108,17 @@ def inbox(conn, params) do ...@@ -107,6 +108,17 @@ def inbox(conn, params) do
json(conn, "ok") json(conn, "ok")
end 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 def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(404) |> put_status(404)
......
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
...@@ -306,6 +306,24 @@ def get_existing_announce(actor, %{data: %{"id" => id}}) do ...@@ -306,6 +306,24 @@ def get_existing_announce(actor, %{data: %{"id" => id}}) do
@doc """ @doc """
Make announce activity data for the given actor and object 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( def make_announce_data(
%User{ap_id: ap_id} = user, %User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object, %Object{data: %{"id" => id}} = object,
...@@ -360,7 +378,12 @@ def make_unlike_data( ...@@ -360,7 +378,12 @@ def make_unlike_data(
if activity_id, do: Map.put(data, "id", activity_id), else: data if activity_id, do: Map.put(data, "id", activity_id), else: data
end 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 = announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: [] if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
...@@ -369,6 +392,8 @@ def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do ...@@ -369,6 +392,8 @@ def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
end end
end end
def add_announce_to_object(_, object), do: {:ok, object}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
announcements = announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: [] if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
......
...@@ -9,6 +9,35 @@ defmodule Pleroma.Web.ActivityPub.UserView do ...@@ -9,6 +9,35 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
import Ecto.Query 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 def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
......
...@@ -4,6 +4,7 @@ defmodule Pleroma.Web.Federator do ...@@ -4,6 +4,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub} alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
require Logger require Logger
...@@ -69,6 +70,11 @@ def handle(:publish, activity) do ...@@ -69,6 +70,11 @@ def handle(:publish, activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end) Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity) 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 end
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
......
...@@ -5,6 +5,7 @@ defmodule Pleroma.Web.Router do ...@@ -5,6 +5,7 @@ defmodule Pleroma.Web.Router do
@instance Application.get_env(:pleroma, :instance) @instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating) @federating Keyword.get(@instance, :federating)
@allow_relay Keyword.get(@instance, :allow_relay)
@public Keyword.get(@instance, :public) @public Keyword.get(@instance, :public)
@registrations_open Keyword.get(@instance, :registrations_open) @registrations_open Keyword.get(@instance, :registrations_open)
...@@ -293,6 +294,10 @@ def user_fetcher(username_or_email) do ...@@ -293,6 +294,10 @@ def user_fetcher(username_or_email) do
get("/externalprofile/show", TwitterAPI.Controller, :external_profile) get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end end
pipeline :ap_relay do
plug(:accepts, ["activity+json"])
end
pipeline :ostatus do pipeline :ostatus do
plug(:accepts, ["xml", "atom", "html", "activity+json"]) plug(:accepts, ["xml", "atom", "html", "activity+json"])
end end
...@@ -329,6 +334,13 @@ def user_fetcher(username_or_email) do ...@@ -329,6 +334,13 @@ def user_fetcher(username_or_email) do
end end
if @federating do 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 scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub) pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox) post("/users/:nickname/inbox", ActivityPubController, :inbox)
......
...@@ -220,7 +220,7 @@ test "it enforces the fqn format for nicknames" do ...@@ -220,7 +220,7 @@ test "it enforces the fqn format for nicknames" do
end end
test "it has required fields" do test "it has required fields" do
[:name, :nickname, :ap_id] [:name, :ap_id]
|> Enum.each(fn field -> |> Enum.each(fn field ->
cs = User.remote_user_creation(Map.delete(@valid_remote, field)) cs = User.remote_user_creation(Map.delete(@valid_remote, field))
refute cs.valid? refute cs.valid?
......
...@@ -77,7 +77,8 @@ test "with credentials", %{conn: conn, user: user} do ...@@ -77,7 +77,8 @@ test "with credentials", %{conn: conn, user: user} do
conn = conn_with_creds |> post(request_path, %{status: " "}) conn = conn_with_creds |> post(request_path, %{status: " "})
assert json_response(conn, 400) == error_response 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) == assert json_response(conn, 200) ==
ActivityRepresenter.to_map(Repo.one(Activity), %{user: user}) ActivityRepresenter.to_map(Repo.one(Activity), %{user: user})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment