Merge branch 'feature/relay' into 'develop'

message relay

Closes #144

See merge request pleroma/pleroma!264
This commit is contained in:
lambda 2018-08-27 08:29:25 +00:00
commit 46c7c2380c
13 changed files with 202 additions and 7 deletions

View file

@ -61,6 +61,7 @@
upload_limit: 16_000_000,
registrations_open: true,
federating: true,
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: []

View file

@ -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

View file

@ -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

View file

@ -77,7 +77,7 @@ def remote_user_creation(params) 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 @@ def search(query, resolve) do
u.nickname,
u.name
)
}
},
where: not is_nil(u.nickname)
)
q =
@ -595,7 +596,11 @@ def unblock_domain(user, domain) 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 @@ def get_or_fetch_by_ap_id(ap_id) 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}}

View file

@ -572,12 +572,23 @@ def user_data_from_user_object(data) 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

View file

@ -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 @@ def inbox(conn, params) 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)

View file

@ -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

View file

@ -306,6 +306,24 @@ def get_existing_announce(actor, %{data: %{"id" => id}}) 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 @@ def make_unlike_data(
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 @@ def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) 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: []

View file

@ -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"])

View file

@ -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 @@ def handle(:publish, activity) 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)

View file

@ -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 @@ def user_fetcher(username_or_email) 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 @@ def user_fetcher(username_or_email) 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)

View file

@ -220,7 +220,7 @@ test "it enforces the fqn format for nicknames" 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?

View file

@ -77,7 +77,8 @@ test "with credentials", %{conn: conn, user: user} 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})