forked from mirrors/akkoma
Merge branch 'feature/relay' into 'develop'
message relay Closes #144 See merge request pleroma/pleroma!264
This commit is contained in:
commit
46c7c2380c
|
@ -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: []
|
||||||
|
|
15
lib/mix/tasks/relay_follow.ex
Normal file
15
lib/mix/tasks/relay_follow.ex
Normal 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
|
15
lib/mix/tasks/relay_unfollow.ex
Normal file
15
lib/mix/tasks/relay_unfollow.ex
Normal 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
|
|
@ -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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
"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
|
||||||
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
|
||||||
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)
|
||||||
|
|
44
lib/pleroma/web/activity_pub/relay.ex
Normal file
44
lib/pleroma/web/activity_pub/relay.ex
Normal 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
|
|
@ -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(
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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})
|
||||||
|
|
Loading…
Reference in a new issue