Merge branch 'feature/federator-behaviours' into 'develop'

federator modularization (outgoing)

See merge request pleroma/pleroma!1140
This commit is contained in:
lambda 2019-05-13 18:58:04 +00:00
commit b14a314ce7
14 changed files with 378 additions and 222 deletions

View file

@ -212,6 +212,11 @@
registrations_open: true, registrations_open: true,
federating: true, federating: true,
federation_reachability_timeout_days: 7, federation_reachability_timeout_days: 7,
federation_publisher_modules: [
Pleroma.Web.ActivityPub.Publisher,
Pleroma.Web.Websub,
Pleroma.Web.Salmon
],
allow_relay: true, allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true, public: true,

View file

@ -5,7 +5,6 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Instances
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
@ -15,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query
@ -24,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
# For Announce activities, we filter the recipients based on following status for any actors # For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary. # that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do defp get_recipients(%{"type" => "Announce"} = data) do
@ -961,89 +957,6 @@ def make_user_from_nickname(nickname) do
end end
end end
def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
def publish(actor, activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Federator.publish_single_ap(%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date =
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
# filter out broken threads # filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user) entire_thread_visible_for_user?(activity, user)

View file

@ -0,0 +1,152 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Instances
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@moduledoc """
ActivityPub outgoing federation module.
"""
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def is_representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
_e ->
false
end
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `inbox`: the inbox to publish to
* `json`: the JSON message body representing the ActivityPub message
* `actor`: the actor which is signing the message
* `id`: the ActivityStreams URI of the message
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date =
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
defp should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
@doc """
Publishes an activity to all relevant peers.
"""
def publish(%User{} = actor, %Activity{} = activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Pleroma.Web.Federator.Publisher.enqueue_one(
__MODULE__,
%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
}
)
end)
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
end

View file

@ -7,13 +7,10 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.User alias Pleroma.User
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
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.OStatus
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub alias Pleroma.Web.Websub
@ -42,14 +39,6 @@ def publish(activity, priority \\ 1) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority) PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
end end
def publish_single_ap(params) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
end
def publish_single_websub(websub) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
end
def verify_websub(websub) do def verify_websub(websub) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub]) PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end end
@ -62,10 +51,6 @@ def refresh_subscriptions do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions]) PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end end
def publish_single_salmon(params) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
end
# Job Worker Callbacks # Job Worker Callbacks
def perform(:refresh_subscriptions) do def perform(:refresh_subscriptions) do
@ -95,23 +80,7 @@ def perform(:publish, activity) do
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} = WebFinger.ensure_keys_present(actor)
if Visibility.is_public?(activity) do Publisher.publish(actor, activity)
if OStatus.is_representable?(activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity)
end
if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
end
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity)
end end
end end
@ -153,20 +122,6 @@ def perform(:incoming_ap_doc, params) do
end end
end end
def perform(:publish_single_salmon, params) do
Salmon.send_to_user(params)
end
def perform(:publish_single_ap, params) do
case ActivityPub.publish_one(params) do
{:ok, _} ->
:ok
{:error, _} ->
RetryQueue.enqueue(params, ActivityPub)
end
end
def perform( def perform(
:publish_single_websub, :publish_single_websub,
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params %{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params

View file

@ -0,0 +1,95 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Federator.RetryQueue
require Logger
@moduledoc """
Defines the contract used by federation implementations to publish messages to
their peers.
"""
@doc """
Determine whether an activity can be relayed using the federation module.
"""
@callback is_representable?(Pleroma.Activity.t()) :: boolean()
@doc """
Relays an activity to a specified peer, determined by the parameters. The
parameters used are controlled by the federation module.
"""
@callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
@doc """
Enqueue publishing a single activity.
"""
@spec enqueue_one(module(), Map.t()) :: :ok
def enqueue_one(module, %{} = params),
do: PleromaJobQueue.enqueue(:federation_outgoing, __MODULE__, [:publish_one, module, params])
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
def perform(:publish_one, module, params) do
case apply(module, :publish_one, [params]) do
{:ok, _} ->
:ok
{:error, _e} ->
RetryQueue.enqueue(params, module)
end
end
def perform(type, _, _) do
Logger.debug("Unknown task: #{type}")
{:error, "Don't know what to do with this"}
end
@doc """
Relays an activity to all specified peers.
"""
@callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()}
@spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok
def publish(%User{} = user, %Activity{} = activity) do
Config.get([:instance, :federation_publisher_modules])
|> Enum.each(fn module ->
if module.is_representable?(activity) do
Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}")
module.publish(user, activity)
end
end)
:ok
end
@doc """
Gathers links used by an outgoing federation module for WebFinger output.
"""
@callback gather_webfinger_links(Pleroma.User.t()) :: list()
@spec gather_webfinger_links(Pleroma.User.t()) :: list()
def gather_webfinger_links(%User{} = user) do
Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links ->
links ++ module.gather_webfinger_links(user)
end)
end
@doc """
Gathers nodeinfo protocol names supported by the federation module.
"""
@callback gather_nodeinfo_protocol_names() :: list()
@spec gather_nodeinfo_protocol_names() :: list()
def gather_nodeinfo_protocol_names do
Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links ->
links ++ module.gather_nodeinfo_protocol_names()
end)
end
end

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.Federator.Publisher
plug(Pleroma.Web.FederatingPlug) plug(Pleroma.Web.FederatingPlug)
@ -137,7 +138,7 @@ def raw_nodeinfo do
name: Pleroma.Application.name() |> String.downcase(), name: Pleroma.Application.name() |> String.downcase(),
version: Pleroma.Application.version() version: Pleroma.Application.version()
}, },
protocols: ["ostatus", "activitypub"], protocols: Publisher.gather_nodeinfo_protocol_names(),
services: %{ services: %{
inbound: [], inbound: [],
outbound: [] outbound: []

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OStatus.DeleteHandler alias Pleroma.Web.OStatus.DeleteHandler
alias Pleroma.Web.OStatus.FollowHandler alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.NoteHandler alias Pleroma.Web.OStatus.NoteHandler
@ -30,7 +31,7 @@ def is_representable?(%Activity{} = activity) do
is_nil(object) -> is_nil(object) ->
false false
object.data["type"] == "Note" -> Visibility.is_public?(activity) && object.data["type"] == "Note" ->
true true
true -> true ->

View file

@ -3,12 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon do defmodule Pleroma.Web.Salmon do
@behaviour Pleroma.Web.Federator.Publisher
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise use Bitwise
alias Pleroma.Activity
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.XML alias Pleroma.Web.XML
@ -165,12 +171,12 @@ def remote_users(%{data: %{"to" => to} = data}) do
end end
@doc "Pushes an activity to remote account." @doc "Pushes an activity to remote account."
def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params), def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
do: send_to_user(Map.put(params, :recipient, salmon)) do: publish_one(Map.put(params, :recipient, salmon))
def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
with {:ok, %{status: code}} when code in 200..299 <- with {:ok, %{status: code}} when code in 200..299 <-
poster.( @httpoison.post(
url, url,
feed, feed,
[{"Content-Type", "application/magic-envelope+xml"}] [{"Content-Type", "application/magic-envelope+xml"}]
@ -184,11 +190,11 @@ def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is
e -> e ->
unless params[:unreachable_since], do: Instances.set_reachable(url) unless params[:unreachable_since], do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
:error {:error, "Unreachable instance"}
end end
end end
def send_to_user(_), do: :noop def publish_one(_), do: :noop
@supported_activities [ @supported_activities [
"Create", "Create",
@ -199,13 +205,19 @@ def send_to_user(_), do: :noop
"Delete" "Delete"
] ]
def is_representable?(%Activity{data: %{"type" => type}} = activity)
when type in @supported_activities,
do: Visibility.is_public?(activity)
def is_representable?(_), do: false
@doc """ @doc """
Publishes an activity to remote accounts Publishes an activity to remote accounts
""" """
@spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none @spec publish(User.t(), Pleroma.Activity.t()) :: none
def publish(user, activity, poster \\ &@httpoison.post/3) def publish(user, activity)
def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster) def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true) feed = ActivityRepresenter.to_simple_form(activity, user, true)
@ -229,15 +241,29 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity
|> Enum.each(fn remote_user -> |> Enum.each(fn remote_user ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
Pleroma.Web.Federator.publish_single_salmon(%{ Publisher.enqueue_one(__MODULE__, %{
recipient: remote_user, recipient: remote_user,
feed: feed, feed: feed,
poster: poster,
unreachable_since: reachable_urls_metadata[remote_user.info.salmon] unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
}) })
end) end)
end end
end end
def publish(%{id: id}, _, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
def gather_webfinger_links(%User{} = user) do
{:ok, _private, public} = keys_from_pem(user.info.keys)
magic_key = encode_key(public)
[
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
%{
"rel" => "magic-public-key",
"href" => "data:application/magic-public-key,#{magic_key}"
}
]
end
def gather_nodeinfo_protocol_names, do: []
end end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.WebFinger do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.OStatus alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon alias Pleroma.Web.Salmon
alias Pleroma.Web.XML alias Pleroma.Web.XML
alias Pleroma.XmlBuilder alias Pleroma.XmlBuilder
@ -50,70 +50,40 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
end end
end end
defp gather_links(%User{} = user) do
[
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
}
] ++ Publisher.gather_webfinger_links(user)
end
def represent_user(user, "JSON") do def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public)
%{ %{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id], "aliases" => [user.ap_id],
"links" => [ "links" => gather_links(user)
%{
"rel" => "http://schemas.google.com/g/2010#updates-from",
"type" => "application/atom+xml",
"href" => OStatus.feed_path(user)
},
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
},
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
%{
"rel" => "magic-public-key",
"href" => "data:application/magic-public-key,#{magic_key}"
},
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
]
} }
end end
def represent_user(user, "XML") do def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public) links =
gather_links(user)
|> Enum.map(fn link -> {:Link, link} end)
{ {
:XRD, :XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[ [
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
{:Alias, user.ap_id}, {:Alias, user.ap_id}
{:Link, ] ++ links
%{
rel: "http://schemas.google.com/g/2010#updates-from",
type: "application/atom+xml",
href: OStatus.feed_path(user)
}},
{:Link,
%{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link,
%{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
{:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
{:Link,
%{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
]
} }
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end

View file

@ -4,10 +4,14 @@
defmodule Pleroma.Web.Websub do defmodule Pleroma.Web.Websub do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.FeedRepresenter alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Router.Helpers
@ -18,6 +22,8 @@ defmodule Pleroma.Web.Websub do
import Ecto.Query import Ecto.Query
@behaviour Pleroma.Web.Federator.Publisher
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
def verify(subscription, getter \\ &@httpoison.get/3) do def verify(subscription, getter \\ &@httpoison.get/3) do
@ -56,6 +62,13 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
"Undo", "Undo",
"Delete" "Delete"
] ]
def is_representable?(%Activity{data: %{"type" => type}} = activity)
when type in @supported_activities,
do: Visibility.is_public?(activity)
def is_representable?(_), do: false
def publish(topic, user, %{data: %{"type" => type}} = activity) def publish(topic, user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do when type in @supported_activities do
response = response =
@ -88,12 +101,14 @@ def publish(topic, user, %{data: %{"type" => type}} = activity)
unreachable_since: reachable_callbacks_metadata[sub.callback] unreachable_since: reachable_callbacks_metadata[sub.callback]
} }
Federator.publish_single_websub(data) Publisher.enqueue_one(__MODULE__, data)
end) end)
end end
def publish(_, _, _), do: "" def publish(_, _, _), do: ""
def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
def sign(secret, doc) do def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase() :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
end end
@ -299,4 +314,20 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} =
{:error, response} {:error, response}
end end
end end
def gather_webfinger_links(%User{} = user) do
[
%{
"rel" => "http://schemas.google.com/g/2010#updates-from",
"type" => "application/atom+xml",
"href" => OStatus.feed_path(user)
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
]
end
def gather_nodeinfo_protocol_names, do: ["ostatus"]
end end

View file

@ -35,6 +35,7 @@ def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(co
defp make_open_tag(tag, attributes) do defp make_open_tag(tag, attributes) do
attributes_string = attributes_string =
for {attribute, value} <- attributes do for {attribute, value} <- attributes do
value = String.replace(value, "\"", "&quot;")
"#{attribute}=\"#{value}\"" "#{attribute}=\"#{value}\""
end end
|> Enum.join(" ") |> Enum.join(" ")

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -963,8 +964,7 @@ test "it filters broken threads" do
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity, private_activity_1, private_activity_3] == assert [public_activity, private_activity_1, private_activity_3] == activities
activities
assert length(activities) == 3 assert length(activities) == 3
@ -1057,7 +1057,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_reachable(inbox)) assert called(Instances.set_reachable(inbox))
end end
@ -1070,7 +1070,7 @@ test "it can create a Flag activity" do
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = assert {:ok, _} =
ActivityPub.publish_one(%{ Publisher.publish_one(%{
inbox: inbox, inbox: inbox,
json: "{}", json: "{}",
actor: actor, actor: actor,
@ -1089,7 +1089,7 @@ test "it can create a Flag activity" do
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = assert {:ok, _} =
ActivityPub.publish_one(%{ Publisher.publish_one(%{
inbox: inbox, inbox: inbox,
json: "{}", json: "{}",
actor: actor, actor: actor,
@ -1107,8 +1107,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox" inbox = "http://404.site/users/nick1/inbox"
assert {:error, _} = assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox)) assert called(Instances.set_unreachable(inbox))
end end
@ -1120,8 +1119,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox" inbox = "http://connrefused.site/users/nick1/inbox"
assert {:error, _} = assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox)) assert called(Instances.set_unreachable(inbox))
end end
@ -1133,7 +1131,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
refute called(Instances.set_unreachable(inbox)) refute called(Instances.set_unreachable(inbox))
end end
@ -1146,7 +1144,7 @@ test "it can create a Flag activity" do
inbox = "http://connrefused.site/users/nick1/inbox" inbox = "http://connrefused.site/users/nick1/inbox"
assert {:error, _} = assert {:error, _} =
ActivityPub.publish_one(%{ Publisher.publish_one(%{
inbox: inbox, inbox: inbox,
json: "{}", json: "{}",
actor: actor, actor: actor,

View file

@ -58,7 +58,7 @@ test "with relays deactivated, it does not publish to the relay", %{
describe "Targets reachability filtering in `publish`" do describe "Targets reachability filtering in `publish`" do
test_with_mock "it federates only to reachable instances via AP", test_with_mock "it federates only to reachable instances via AP",
Federator, Pleroma.Web.ActivityPub.Publisher,
[:passthrough], [:passthrough],
[] do [] do
user = insert(:user) user = insert(:user)
@ -88,13 +88,18 @@ test "with relays deactivated, it does not publish to the relay", %{
{:ok, _activity} = {:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
assert called(Federator.publish_single_ap(%{inbox: inbox1, unreachable_since: dt})) assert called(
Pleroma.Web.ActivityPub.Publisher.publish_one(%{
inbox: inbox1,
unreachable_since: dt
})
)
refute called(Federator.publish_single_ap(%{inbox: inbox2})) refute called(Pleroma.Web.ActivityPub.Publisher.publish_one(%{inbox: inbox2}))
end end
test_with_mock "it federates only to reachable instances via Websub", test_with_mock "it federates only to reachable instances via Websub",
Federator, Pleroma.Web.Websub,
[:passthrough], [:passthrough],
[] do [] do
user = insert(:user) user = insert(:user)
@ -122,17 +127,17 @@ test "with relays deactivated, it does not publish to the relay", %{
{:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"}) {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"})
assert called( assert called(
Federator.publish_single_websub(%{ Pleroma.Web.Websub.publish_one(%{
callback: sub2.callback, callback: sub2.callback,
unreachable_since: dt unreachable_since: dt
}) })
) )
refute called(Federator.publish_single_websub(%{callback: sub1.callback})) refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback}))
end end
test_with_mock "it federates only to reachable instances via Salmon", test_with_mock "it federates only to reachable instances via Salmon",
Federator, Pleroma.Web.Salmon,
[:passthrough], [:passthrough],
[] do [] do
user = insert(:user) user = insert(:user)
@ -162,13 +167,13 @@ test "with relays deactivated, it does not publish to the relay", %{
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
assert called( assert called(
Federator.publish_single_salmon(%{ Pleroma.Web.Salmon.publish_one(%{
recipient: remote_user2, recipient: remote_user2,
unreachable_since: dt unreachable_since: dt
}) })
) )
refute called(Federator.publish_single_websub(%{recipient: remote_user1})) refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1}))
end end
end end

View file

@ -7,7 +7,9 @@ defmodule Pleroma.Web.Salmon.SalmonTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon alias Pleroma.Web.Salmon
import Mock
import Pleroma.Factory import Pleroma.Factory
@magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
@ -77,7 +79,10 @@ test "it gets a magic key" do
"RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB" "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB"
end end
test "it pushes an activity to remote accounts it's addressed to" do test_with_mock "it pushes an activity to remote accounts it's addressed to",
Publisher,
[:passthrough],
[] do
user_data = %{ user_data = %{
info: %{ info: %{
salmon: "http://test-example.org/salmon" salmon: "http://test-example.org/salmon"
@ -102,10 +107,8 @@ test "it pushes an activity to remote accounts it's addressed to" do
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
poster = fn url, _data, _headers -> Salmon.publish(user, activity)
assert url == "http://test-example.org/salmon"
end
Salmon.publish(user, activity, poster) assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user}))
end end
end end