Actor suspension refactoring
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -4,7 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Activity do
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
data: String.t(),
|
||||
data: map(),
|
||||
local: boolean,
|
||||
actor: Actor.t(),
|
||||
recipients: [String.t()]
|
||||
|
||||
@@ -143,11 +143,11 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
{:ok, _activity, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
{:error, "Gone"} ->
|
||||
{:error, "Gone", entity}
|
||||
{:error, :http_gone} ->
|
||||
{:error, :http_gone, entity}
|
||||
|
||||
{:error, "Not found"} ->
|
||||
{:error, "Not found", entity}
|
||||
{:error, :http_not_found} ->
|
||||
{:error, :http_not_found, entity}
|
||||
|
||||
{:error, "Object origin check failed"} ->
|
||||
{:error, "Object origin check failed"}
|
||||
|
||||
@@ -14,65 +14,62 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
||||
@doc """
|
||||
Getting an actor from url, eventually creating it if we don't have it locally or if it needs an update
|
||||
"""
|
||||
@spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||
@spec get_or_fetch_actor_by_url(url :: String.t(), preload :: boolean()) ::
|
||||
{:ok, Actor.t()}
|
||||
| {:error, make_actor_errors}
|
||||
| {:error, :no_internal_relay_actor}
|
||||
| {:error, :url_nil}
|
||||
def get_or_fetch_actor_by_url(url, preload \\ false)
|
||||
|
||||
def get_or_fetch_actor_by_url(nil, _preload), do: {:error, "Can't fetch a nil url"}
|
||||
def get_or_fetch_actor_by_url(nil, _preload), do: {:error, :url_nil}
|
||||
|
||||
def get_or_fetch_actor_by_url("https://www.w3.org/ns/activitystreams#Public", _preload) do
|
||||
with %Actor{url: url} <- Relay.get_actor() do
|
||||
get_or_fetch_actor_by_url(url)
|
||||
case Relay.get_actor() do
|
||||
%Actor{url: url} ->
|
||||
get_or_fetch_actor_by_url(url)
|
||||
|
||||
{:error, %Ecto.Changeset{}} ->
|
||||
{:error, :no_internal_relay_actor}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_or_fetch_actor_by_url(String.t(), boolean()) :: {:ok, Actor.t()} | {:error, any()}
|
||||
def get_or_fetch_actor_by_url(url, preload) do
|
||||
with {:ok, %Actor{} = cached_actor} <- Actors.get_actor_by_url(url, preload),
|
||||
false <- Actors.needs_update?(cached_actor) do
|
||||
{:ok, cached_actor}
|
||||
else
|
||||
_ ->
|
||||
# For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest
|
||||
case __MODULE__.make_actor_from_url(url, preload) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Could not fetch by AP id")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, "Could not fetch by AP id"}
|
||||
case Actors.get_actor_by_url(url, preload) do
|
||||
{:ok, %Actor{} = cached_actor} ->
|
||||
unless Actors.needs_update?(cached_actor) do
|
||||
{:ok, cached_actor}
|
||||
else
|
||||
__MODULE__.make_actor_from_url(url, preload)
|
||||
end
|
||||
|
||||
{:error, :actor_not_found} ->
|
||||
# For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest
|
||||
__MODULE__.make_actor_from_url(url, preload)
|
||||
end
|
||||
end
|
||||
|
||||
@type make_actor_errors :: Fetcher.fetch_actor_errors() | :actor_is_local
|
||||
|
||||
@doc """
|
||||
Create an actor locally by its URL (AP ID)
|
||||
"""
|
||||
@spec make_actor_from_url(String.t(), boolean()) ::
|
||||
{:ok, %Actor{}} | {:error, :actor_deleted} | {:error, :http_error} | {:error, any()}
|
||||
@spec make_actor_from_url(url :: String.t(), preload :: boolean()) ::
|
||||
{:ok, Actor.t()} | {:error, make_actor_errors}
|
||||
def make_actor_from_url(url, preload \\ false) do
|
||||
if are_same_origin?(url, Endpoint.url()) do
|
||||
{:error, "Can't make a local actor from URL"}
|
||||
{:error, :actor_is_local}
|
||||
else
|
||||
case Fetcher.fetch_and_prepare_actor_from_url(url) do
|
||||
# Just in case
|
||||
{:ok, {:error, _e}} ->
|
||||
raise ArgumentError, message: "Failed to make actor from url #{url}"
|
||||
|
||||
{:ok, data} ->
|
||||
{:ok, data} when is_map(data) ->
|
||||
Actors.upsert_actor(data, preload)
|
||||
|
||||
# Request returned 410
|
||||
{:error, :actor_deleted} ->
|
||||
Logger.info("Actor was deleted")
|
||||
Logger.info("Actor #{url} was deleted")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
{:error, :http_error} ->
|
||||
{:error, :http_error}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Failed to make actor from url #{url}")
|
||||
{:error, e}
|
||||
{:error, err} when err in [:http_error, :json_decode_error] ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -80,8 +77,8 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
||||
@doc """
|
||||
Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it
|
||||
"""
|
||||
@spec find_or_make_actor_from_nickname(String.t(), atom() | nil) ::
|
||||
{:ok, Actor.t()} | {:error, any()}
|
||||
@spec find_or_make_actor_from_nickname(nickname :: String.t(), type :: atom() | nil) ::
|
||||
{:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
|
||||
def find_or_make_actor_from_nickname(nickname, type \\ nil) do
|
||||
case Actors.get_actor_by_name_with_preload(nickname, type) do
|
||||
%Actor{url: actor_url} = actor ->
|
||||
@@ -96,20 +93,22 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_or_make_group_from_nickname(String.t()) :: tuple()
|
||||
@spec find_or_make_group_from_nickname(nick :: String.t()) ::
|
||||
{:error, make_actor_errors | WebFinger.finger_errors()}
|
||||
def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group)
|
||||
|
||||
@doc """
|
||||
Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it
|
||||
"""
|
||||
@spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()}
|
||||
@spec make_actor_from_nickname(nickname :: String.t(), preload :: boolean) ::
|
||||
{:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
|
||||
def make_actor_from_nickname(nickname, preload \\ false) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, url} when is_binary(url) ->
|
||||
make_actor_from_url(url, preload)
|
||||
|
||||
{:error, _e} ->
|
||||
{:error, "No ActivityPub URL found in WebFinger"}
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,39 +15,48 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2]
|
||||
|
||||
@spec fetch(String.t(), Keyword.t()) :: {:ok, map()}
|
||||
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
|
||||
|
||||
@spec fetch(String.t(), Keyword.t()) ::
|
||||
{:ok, map()}
|
||||
| {:ok, Tesla.Env.t()}
|
||||
| {:error, String.t()}
|
||||
| {:error, any()}
|
||||
| {:error, :invalid_url}
|
||||
def fetch(url, options \\ []) do
|
||||
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
with false <- address_invalid(url),
|
||||
date <- Signature.generate_date_header(),
|
||||
headers <-
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(on_behalf_of, url, date),
|
||||
client <-
|
||||
ActivityPubClient.client(headers: headers),
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
|
||||
ActivityPubClient.get(client, url) do
|
||||
{:ok, data}
|
||||
headers =
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(on_behalf_of, url, date)
|
||||
|
||||
client = ActivityPubClient.client(headers: headers)
|
||||
|
||||
if address_valid?(url) do
|
||||
case ActivityPubClient.get(client, url) do
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 ->
|
||||
{:ok, data}
|
||||
|
||||
{:ok, %Tesla.Env{status: 410}} ->
|
||||
Logger.debug("Resource at #{url} is 410 Gone")
|
||||
{:error, :http_gone}
|
||||
|
||||
{:ok, %Tesla.Env{status: 404}} ->
|
||||
Logger.debug("Resource at #{url} is 404 Gone")
|
||||
{:error, :http_not_found}
|
||||
|
||||
{:ok, %Tesla.Env{} = res} ->
|
||||
{:error, res}
|
||||
end
|
||||
else
|
||||
{:ok, %Tesla.Env{status: 410}} ->
|
||||
Logger.debug("Resource at #{url} is 410 Gone")
|
||||
{:error, "Gone"}
|
||||
|
||||
{:ok, %Tesla.Env{status: 404}} ->
|
||||
Logger.debug("Resource at #{url} is 404 Gone")
|
||||
{:error, "Not found"}
|
||||
|
||||
{:ok, %Tesla.Env{} = res} ->
|
||||
{:error, res}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
{:error, :invalid_url}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()}
|
||||
@spec fetch_and_create(String.t(), Keyword.t()) ::
|
||||
{:ok, map(), struct()} | {:error, :invalid_url} | {:error, String.t()} | {:error, any}
|
||||
def fetch_and_create(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
|
||||
@@ -69,12 +78,16 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
{:ok, data} when is_binary(data) ->
|
||||
{:error, "Failed to parse content as JSON"}
|
||||
|
||||
{:error, :invalid_url} ->
|
||||
{:error, :invalid_url}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()}
|
||||
@spec fetch_and_update(String.t(), Keyword.t()) ::
|
||||
{:ok, map(), struct()} | {:error, String.t()} | :error | {:error, any}
|
||||
def fetch_and_update(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
{:origin_check, true} <- {:origin_check, origin_check(url, data)},
|
||||
@@ -96,44 +109,46 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
end
|
||||
end
|
||||
|
||||
@type fetch_actor_errors :: :json_decode_error | :actor_deleted | :http_error
|
||||
|
||||
@doc """
|
||||
Fetching a remote actor's information through its AP ID
|
||||
"""
|
||||
@spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, map()} | {:error, atom()} | any()
|
||||
@spec fetch_and_prepare_actor_from_url(String.t()) ::
|
||||
{:ok, map()} | {:error, fetch_actor_errors}
|
||||
def fetch_and_prepare_actor_from_url(url) do
|
||||
Logger.debug("Fetching and preparing actor from url")
|
||||
Logger.debug(inspect(url))
|
||||
|
||||
res =
|
||||
with {:ok, %{status: 200, body: body}} <-
|
||||
Tesla.get(url,
|
||||
headers: [{"Accept", "application/activity+json"}],
|
||||
follow_redirect: true
|
||||
),
|
||||
:ok <- Logger.debug("response okay, now decoding json"),
|
||||
{:ok, data} <- Jason.decode(body) do
|
||||
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
|
||||
{:ok, ActorConverter.as_to_model_data(data)}
|
||||
else
|
||||
# Actor is gone, probably deleted
|
||||
{:ok, %{status: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
case Tesla.get(url,
|
||||
headers: [{"Accept", "application/activity+json"}],
|
||||
follow_redirect: true
|
||||
) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
Logger.debug("response okay, now decoding json")
|
||||
|
||||
{:ok, %Tesla.Env{}} ->
|
||||
Logger.info("Non 200 HTTP Code")
|
||||
{:error, :http_error}
|
||||
case Jason.decode(body) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
|
||||
{:ok, ActorConverter.as_to_model_data(data)}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
{:error, %Jason.DecodeError{} = e} ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, :json_decode_error}
|
||||
end
|
||||
|
||||
e ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
{:ok, %{status: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
res
|
||||
{:ok, %Tesla.Env{}} ->
|
||||
Logger.info("Non 200 HTTP Code")
|
||||
{:error, :http_error}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warn("Could not fetch actor at fetch #{url}, #{inspect(error)}")
|
||||
{:error, :http_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec origin_check(String.t(), map()) :: boolean()
|
||||
@@ -147,11 +162,14 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
end
|
||||
end
|
||||
|
||||
@spec address_invalid(String.t()) :: false | {:error, :invalid_url}
|
||||
defp address_invalid(address) do
|
||||
with %URI{host: host, scheme: scheme} <- URI.parse(address),
|
||||
true <- is_nil(host) or is_nil(scheme) do
|
||||
{:error, :invalid_url}
|
||||
@spec address_valid?(String.t()) :: boolean
|
||||
defp address_valid?(address) do
|
||||
case URI.parse(address) do
|
||||
%URI{host: host, scheme: scheme} ->
|
||||
is_valid_string(host) and is_valid_string(scheme)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,15 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
|
||||
|
||||
@type object :: %{id: String.t(), url: String.t()}
|
||||
|
||||
@type permissions_member_role :: nil | :member | :moderator | :administrator
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
access: permissions_member_role,
|
||||
create: permissions_member_role,
|
||||
update: permissions_member_role,
|
||||
delete: permissions_member_role
|
||||
}
|
||||
|
||||
@doc """
|
||||
Check that actor can access the object
|
||||
"""
|
||||
|
||||
@@ -8,13 +8,12 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils}
|
||||
alias Mobilizon.Service.ErrorReporting.Sentry
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Refresh a remote profile
|
||||
"""
|
||||
@spec refresh_profile(Actor.t()) :: {:ok, Actor.t()}
|
||||
@spec refresh_profile(Actor.t()) :: {:ok, Actor.t()} | {:error, fetch_actor_errors()} | {:error}
|
||||
def refresh_profile(%Actor{domain: nil}), do: {:error, "Can only refresh remote actors"}
|
||||
|
||||
def refresh_profile(%Actor{type: :Group, url: url, id: group_id} = group) do
|
||||
@@ -33,74 +32,84 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
end
|
||||
|
||||
def refresh_profile(%Actor{type: type, url: url}) when type in [:Person, :Application] do
|
||||
with {:ok, %Actor{outbox_url: outbox_url} = actor} <-
|
||||
ActivityPubActor.make_actor_from_url(url),
|
||||
:ok <- fetch_collection(outbox_url, Relay.get_actor()) do
|
||||
{:ok, actor}
|
||||
case ActivityPubActor.make_actor_from_url(url) do
|
||||
{:ok, %Actor{outbox_url: outbox_url} = actor} ->
|
||||
case fetch_collection(outbox_url, Relay.get_actor()) do
|
||||
:ok -> {:ok, actor}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok
|
||||
@type fetch_actor_errors :: ActivityPubActor.make_actor_errors() | fetch_collection_errors()
|
||||
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok | {:error, fetch_actor_errors}
|
||||
def fetch_group(group_url, %Actor{} = on_behalf_of) do
|
||||
with {:ok,
|
||||
%Actor{
|
||||
outbox_url: outbox_url,
|
||||
resources_url: resources_url,
|
||||
members_url: members_url,
|
||||
posts_url: posts_url,
|
||||
todos_url: todos_url,
|
||||
discussions_url: discussions_url,
|
||||
events_url: events_url
|
||||
}} <-
|
||||
ActivityPubActor.make_actor_from_url(group_url),
|
||||
:ok <- fetch_collection(outbox_url, on_behalf_of),
|
||||
:ok <- fetch_collection(members_url, on_behalf_of),
|
||||
:ok <- fetch_collection(resources_url, on_behalf_of),
|
||||
:ok <- fetch_collection(posts_url, on_behalf_of),
|
||||
:ok <- fetch_collection(todos_url, on_behalf_of),
|
||||
:ok <- fetch_collection(discussions_url, on_behalf_of),
|
||||
:ok <- fetch_collection(events_url, on_behalf_of) do
|
||||
:ok
|
||||
else
|
||||
{:error, :actor_deleted} ->
|
||||
{:error, :actor_deleted}
|
||||
case ActivityPubActor.make_actor_from_url(group_url) do
|
||||
{:ok,
|
||||
%Actor{
|
||||
outbox_url: outbox_url,
|
||||
resources_url: resources_url,
|
||||
members_url: members_url,
|
||||
posts_url: posts_url,
|
||||
todos_url: todos_url,
|
||||
discussions_url: discussions_url,
|
||||
events_url: events_url
|
||||
}} ->
|
||||
Logger.debug("Fetched group OK, now doing collections")
|
||||
|
||||
{:error, :http_error} ->
|
||||
{:error, :http_error}
|
||||
with :ok <- fetch_collection(outbox_url, on_behalf_of),
|
||||
:ok <- fetch_collection(members_url, on_behalf_of),
|
||||
:ok <- fetch_collection(resources_url, on_behalf_of),
|
||||
:ok <- fetch_collection(posts_url, on_behalf_of),
|
||||
:ok <- fetch_collection(todos_url, on_behalf_of),
|
||||
:ok <- fetch_collection(discussions_url, on_behalf_of),
|
||||
:ok <- fetch_collection(events_url, on_behalf_of) do
|
||||
:ok
|
||||
else
|
||||
{:error, err}
|
||||
when err in [:error, :process_error, :fetch_error, :collection_url_nil] ->
|
||||
Logger.debug("Error while fetching actor collection")
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error("Error while refreshing a group")
|
||||
|
||||
Sentry.capture_message("Error while refreshing a group",
|
||||
extra: %{group_url: group_url}
|
||||
)
|
||||
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
when err in [:actor_deleted, :http_error, :json_decode_error, :actor_is_local] ->
|
||||
Logger.debug("Error while making actor")
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
Logger.error("Error while refreshing a group")
|
||||
|
||||
Sentry.capture_message("Error while refreshing a group",
|
||||
extra: %{group_url: group_url}
|
||||
)
|
||||
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_collection(nil, _on_behalf_of), do: :error
|
||||
@typep fetch_collection_errors :: :process_error | :fetch_error | :collection_url_nil
|
||||
|
||||
@spec fetch_collection(String.t() | nil, any) ::
|
||||
:ok | {:error, fetch_collection_errors}
|
||||
def fetch_collection(nil, _on_behalf_of), do: {:error, :collection_url_nil}
|
||||
|
||||
def fetch_collection(collection_url, on_behalf_of) do
|
||||
Logger.debug("Fetching and preparing collection from url")
|
||||
Logger.debug(inspect(collection_url))
|
||||
|
||||
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of),
|
||||
:ok <- Logger.debug("Fetch ok, passing to process_collection"),
|
||||
:ok <- process_collection(data, on_behalf_of) do
|
||||
Logger.debug("Finished processing a collection")
|
||||
:ok
|
||||
case Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
Logger.debug("Fetch ok, passing to process_collection")
|
||||
|
||||
case process_collection(data, on_behalf_of) do
|
||||
:ok ->
|
||||
Logger.debug("Finished processing a collection")
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
Logger.debug("Failed to process collection #{collection_url}")
|
||||
{:error, :process_error}
|
||||
end
|
||||
|
||||
{:error, _err} ->
|
||||
Logger.debug("Failed to fetch collection #{collection_url}")
|
||||
{:error, :fetch_error}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,6 +136,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
|> Enum.each(&refresh_profile/1)
|
||||
end
|
||||
|
||||
@spec process_collection(map(), any()) :: :ok | :error
|
||||
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
|
||||
when type in ["OrderedCollection", "OrderedCollectionPage"] do
|
||||
Logger.debug(
|
||||
@@ -168,6 +178,8 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
defp process_collection(_, _), do: :error
|
||||
|
||||
# If we're handling an activity
|
||||
@spec handling_element(map()) :: {:ok, any, struct} | :error
|
||||
@spec handling_element(String.t()) :: {:ok, struct} | {:error, any()}
|
||||
defp handling_element(%{"type" => activity_type} = data)
|
||||
when activity_type in ["Create", "Update", "Delete"] do
|
||||
object = get_in(data, ["object"])
|
||||
|
||||
@@ -138,7 +138,8 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
|
||||
defp fetch_object(object) when is_binary(object), do: {object, object}
|
||||
|
||||
@spec fetch_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
@spec fetch_actor(String.t()) ::
|
||||
{:ok, String.t()} | {:error, WebFinger.finger_errors() | :bad_url}
|
||||
# Dirty hack
|
||||
defp fetch_actor("https://" <> address), do: fetch_actor(address)
|
||||
defp fetch_actor("http://" <> address), do: fetch_actor(address)
|
||||
@@ -154,26 +155,15 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
check_actor("relay@#{host}")
|
||||
|
||||
true ->
|
||||
{:error, "Bad URL"}
|
||||
{:error, :bad_url}
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
@spec check_actor(String.t()) :: {:ok, String.t()} | {:error, WebFinger.finger_errors()}
|
||||
defp check_actor(username_and_domain) do
|
||||
case Actors.get_actor_by_name(username_and_domain) do
|
||||
%Actor{url: url} -> {:ok, url}
|
||||
nil -> finger_actor(username_and_domain)
|
||||
end
|
||||
end
|
||||
|
||||
@spec finger_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
defp finger_actor(nickname) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, url} when is_binary(url) ->
|
||||
{:ok, url}
|
||||
|
||||
_e ->
|
||||
{:error, "No ActivityPub URL found in WebFinger"}
|
||||
nil -> WebFinger.finger(username_and_domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
@doc """
|
||||
Handle incoming activities
|
||||
"""
|
||||
@spec handle_incoming(map()) :: :error | {:ok, any(), struct()}
|
||||
def handle_incoming(%{"id" => nil}), do: :error
|
||||
def handle_incoming(%{"id" => ""}), do: :error
|
||||
|
||||
@@ -1107,7 +1108,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
|
||||
defp is_group_object_gone(object_id) do
|
||||
case ActivityPub.fetch_object_from_url(object_id, force: true) do
|
||||
{:error, error_message, object} when error_message in ["Gone", "Not found"] ->
|
||||
{:error, error_message, object} when error_message in [:http_gone, :http_not_found] ->
|
||||
{:ok, object}
|
||||
|
||||
# comments are just emptied
|
||||
|
||||
@@ -19,6 +19,10 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
require Logger
|
||||
import SweetXml
|
||||
|
||||
@doc """
|
||||
Returns the Web Host Metadata (for `/.well-known/host-meta`) representation for the instance, following RFC6414.
|
||||
"""
|
||||
@spec host_meta :: String.t()
|
||||
def host_meta do
|
||||
base_url = Endpoint.url()
|
||||
%URI{host: host} = URI.parse(base_url)
|
||||
@@ -47,6 +51,10 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
|> XmlBuilder.to_doc()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the Webfinger representation for the instance, following RFC7033.
|
||||
"""
|
||||
@spec webfinger(String.t(), String.t()) :: {:ok, map} | {:error, :actor_not_found}
|
||||
def webfinger(resource, "JSON") do
|
||||
host = Endpoint.host()
|
||||
regex = ~r/(acct:)?(?<name>\w+)@#{host}/
|
||||
@@ -61,11 +69,14 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
{:ok, represent_actor(actor, "JSON")}
|
||||
|
||||
_e ->
|
||||
{:error, "Couldn't find actor"}
|
||||
{:error, :actor_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return an `Mobilizon.Actors.Actor` Webfinger representation (as JSON)
|
||||
"""
|
||||
@spec represent_actor(Actor.t()) :: map()
|
||||
@spec represent_actor(Actor.t(), String.t()) :: map()
|
||||
def represent_actor(%Actor{} = actor), do: represent_actor(actor, "JSON")
|
||||
@@ -89,6 +100,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
}
|
||||
end
|
||||
|
||||
@spec maybe_add_avatar(list(map()), Actor.t()) :: list(map())
|
||||
defp maybe_add_avatar(data, %Actor{avatar: avatar}) when not is_nil(avatar) do
|
||||
data ++
|
||||
[
|
||||
@@ -102,6 +114,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
|
||||
defp maybe_add_avatar(data, _actor), do: data
|
||||
|
||||
@spec maybe_add_profile_page(list(map()), Actor.t()) :: list(map())
|
||||
defp maybe_add_profile_page(data, %Actor{type: :Group, url: url}) do
|
||||
data ++
|
||||
[
|
||||
@@ -115,35 +128,69 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
|
||||
defp maybe_add_profile_page(data, _actor), do: data
|
||||
|
||||
@type finger_errors ::
|
||||
:host_not_found | :address_invalid | :http_error | :webfinger_information_not_json
|
||||
|
||||
@doc """
|
||||
Finger an actor to retreive it's ActivityPub ID/URL
|
||||
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) with `find_webfinger_endpoint/1` and then performs a Webfinger query to get the ActivityPub ID associated to an actor.
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) and then performs a Webfinger query to get the ActivityPub ID associated to an actor.
|
||||
"""
|
||||
@spec finger(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
@spec finger(String.t()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, finger_errors}
|
||||
def finger(actor) do
|
||||
actor = String.trim_leading(actor, "@")
|
||||
|
||||
with address when is_binary(address) <- apply_webfinger_endpoint(actor),
|
||||
false <- address_invalid(address),
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
WebfingerClient.get(address),
|
||||
{:ok, %{"url" => url}} <- webfinger_from_json(body) do
|
||||
{:ok, url}
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't finger #{actor}")
|
||||
Logger.debug(inspect(e))
|
||||
{:error, e}
|
||||
case validate_endpoint(actor) do
|
||||
{:ok, address} ->
|
||||
case fetch_webfinger_data(address) do
|
||||
{:ok, %{"url" => url}} ->
|
||||
{:ok, url}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Couldn't process webfinger data for #{actor}")
|
||||
err
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Couldn't find webfinger endpoint for #{actor}")
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
"""
|
||||
@spec fetch_webfinger_data(String.t()) ::
|
||||
{:ok, map()} | {:error, :webfinger_information_not_json | :http_error}
|
||||
defp fetch_webfinger_data(address) do
|
||||
case WebfingerClient.get(address) do
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 ->
|
||||
webfinger_from_json(body)
|
||||
|
||||
_ ->
|
||||
{:error, :http_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_endpoint(String.t()) ::
|
||||
{:ok, String.t()} | {:error, :address_invalid | :host_not_found}
|
||||
defp validate_endpoint(actor) do
|
||||
case apply_webfinger_endpoint(actor) do
|
||||
address when is_binary(address) ->
|
||||
if address_invalid(address) do
|
||||
{:error, :address_invalid}
|
||||
else
|
||||
{:ok, address}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :host_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
@spec find_webfinger_endpoint(String.t()) ::
|
||||
{:ok, String.t()} | {:error, :link_not_found} | {:error, any()}
|
||||
def find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||
defp find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||
with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"),
|
||||
link_template when is_binary(link_template) <- find_link_from_template(body) do
|
||||
{:ok, link_template}
|
||||
|
||||
Reference in New Issue
Block a user