Packages
indieweb
0.0.59
0.0.66
0.0.65
0.0.64
0.0.63
0.0.62
0.0.61
0.0.60
0.0.59
0.0.58
0.0.56
0.0.55
0.0.54
0.0.53
0.0.51
0.0.50
0.0.49
0.0.48
0.0.47
0.0.46
0.0.42
0.0.41
0.0.40
0.0.39
0.0.38
0.0.37
0.0.36
0.0.35
0.0.34
0.0.33
0.0.32
0.0.31
0.0.30
0.0.29
0.0.28
0.0.27
0.0.26
0.0.25
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.3
0.0.2
0.0.1
Collection of common IndieWeb utilites like authorship resolution, Webmention, post type discovery and IndieAuth.
Current section
Files
Jump to
Current section
Files
lib/indieweb/webmention.ex
defmodule IndieWeb.Webmention do
@moduledoc """
General facilities around Webmentions.
This module provides methods around handling receiving and sending Webmentions.
"""
require Logger
alias IndieWeb.{Http, LinkRel}
defmodule URIAdapter do
@moduledoc """
Facility for handling URI generation of Webmention logic.
"""
@doc "Defines a means of generating a URI from the provided value."
@callback to_source_url(object :: any()) :: nil | URI.t()
@doc "Defines a means of obtaining a target from a URI."
@callback from_source_url(uri :: URI.t()) :: nil | any()
end
defmodule SendResponse do
@moduledoc false
@enforce_keys ~w(target source code)a
defstruct ~w(target source code body headers)a
@type t() :: %__MODULE__{
target: binary(),
source: binary(),
code: non_neg_integer(),
body: binary(),
headers: map()
}
end
defmodule ReceiveResponse do
@moduledoc false
@enforce_keys ~w(target source resource arguments)a
defstruct ~w(target source resource arguments)a
@type t() :: %__MODULE__{
target: binary(),
source: binary(),
resource: any(),
arguments: keyword()
}
end
@doc "Defines the adpater to use to resolve URI and source content."
@spec url_adapter() :: __MODULE__.URIAdapter.t()
def url_adapter, do: Application.get_env(:indieweb, :webmention_url_adapter)
@doc "Determines the resource from the provided target URI."
@spec resolve_target_from_url(binary()) ::
{:ok, any()} | {:error, :no_target} | {:error, :no_adapter}
def resolve_target_from_url(target_url) do
if url_adapter() != nil do
case url_adapter().from_source_url(target_url) do
nil -> {:error, :no_target}
target -> {:ok, target}
end
else
{:error, :no_adapter}
end
end
@doc "Generates a source URL for the provided resource."
@spec resolve_source_url(binary()) ::
{:ok, URI.t()}
| {:error, :no_source}
| {:error, :no_adapter}
def resolve_source_url(source) do
if url_adapter() != nil do
case url_adapter().to_source_url(source) do
nil -> {:error, :no_source}
%URI{} = url -> {:ok, url}
end
else
{:error, :no_adapter}
end
end
@doc """
Finds the Webmention endpoint of the provided URI.
This employs the [Webmention endpoint discovery algorithm][1] to find
the proper endpoint to send Webmentions for the URI in question.
[1]: https://www.w3.org/TR/webmention/#sender-discovers-receiver-webmention-endpoint
TODO: Add User-Agent information (by allowing to pass in header options)
TODO: This hits every endpoint with a HEAD when it's called - need to cache the value.
NOTE: This makes an assumption that the endpoint is self-hosted by default.
NOTE: This assumes that a site can only have one Webmention endpoint.
"""
@spec discover_endpoint(binary) ::
{:ok, binary()} | {:error, reason: :no_endpoint_found}
def discover_endpoint(page_url) do
endpoint =
page_url
|> LinkRel.find("webmention")
|> resolve_endpoint_urls(page_url)
|> Enum.reject(&is_nil/1)
|> Enum.reject(&(&1 == ""))
|> List.first()
if is_nil(endpoint) do
{:error, reason: :no_endpoint_found}
else
{:ok, endpoint}
end
end
@doc """
Sends a Webmention to the provided URI.
This determines the endpoint to send [Webmentions][1] to (using `discover_endpoint/1`) and
sends the request using the HTTP client provided.
[1]: https://www.w3.org/TR/webmention
"""
@spec send(binary(), binary(), map()) ::
{:ok, SendResponse.t()}
| {:error, :webmention_send_failure, reason: any()}
def send(target_url, source, opts \\ %{}) do
with(
{:ok, endpoint_url} <- discover_endpoint(target_url),
{:ok, resp} <- direct_send!(endpoint_url, target_url, source, opts)
) do
{:ok, resp}
else
{:error, error} -> {:error, :webmention_send_failure, error}
end
end
@doc """
Explicitly sends a Webmention.
Sends a Webmention to the provided `endpoint` for `target` from `source`.
"""
@spec direct_send!(binary(), binary(), binary(), keyword()) ::
{:ok, SendResponse.t()} | {:error, atom(), keyword()}
def direct_send!(endpoint, target_url, source, opts \\ %{}) do
Logger.debug("Attempting to send Webmention.",
endpoint: endpoint,
target_url: target_url,
source: source,
options: inspect(opts)
)
with(
{:ok, source_url} <- resolve_source_url(source),
{:ok, %IndieWeb.Http.Response{code: code, body: body, headers: headers}}
when code >= 200 and code < 400 <-
Http.post_encoded(endpoint,
body:
Map.merge(
%{"source" => URI.to_string(source_url), "target" => target_url},
opts
)
)
) do
send_resp = %SendResponse{
target: target_url,
source: source_url,
code: code,
body: body,
headers: headers
}
{:ok, send_resp}
else
{:error, %IndieWeb.Http.Response{} = resp} ->
{:error, reason: :unexpected_response, response: resp}
{:ok, %IndieWeb.Http.Response{} = resp} ->
{:error, reason: :unexpected_response, response: resp}
{:error, :no_source} ->
{:error, reason: :invalid_source}
{:error, :no_adapter} ->
{:error, reason: :no_parser_available}
{:error, error} ->
{:error, reason: :unexpected, result: error}
{:ok, result} ->
{:error, reason: :unexpected, result: result}
end
end
@doc """
Parses properties of an incoming Webmention.
This aims to resolve the target of an incoming Webmention and determine if there's
a valid action to take from it.
"""
@spec receive(map()) ::
{:ok, ReceiveResponse.t()}
| {:error, :webmention_receive_failure, reason: any()}
def receive(args) do
source_url = args[:source]
target_url = args[:target]
Logger.debug("Attempting to receive and resolve Webmention.",
target: target_url,
source: source_url
)
case resolve_target_from_url(target_url) do
{:ok, target} ->
{:ok,
%ReceiveResponse{
source: source_url,
target: target_url,
resource: target,
arguments: Map.drop(args, ~w(source target)a)
}}
{:error, error} ->
{:error, :webmention_receive_failure, reason: error}
end
end
defp resolve_endpoint_urls(rel_urls, base_uri) do
rel_urls
|> Enum.map(&IndieWeb.Http.make_absolute_uri(&1, base_uri))
|> Enum.map(&IndieWeb.URL.canonalize/1)
|> Enum.uniq()
|> Enum.reject(&is_nil/1)
end
end
# SPDX-License-Identifier: AGPL-3.0-only