Actor suspension refactoring

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2021-09-10 11:36:05 +02:00
parent e9fecc4d24
commit 75e254d8b4
23 changed files with 816 additions and 603 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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