clacks/lib/clacks/webmention/endpoint.ex

120 lines
2.8 KiB
Elixir

defmodule Clacks.Webmention.Endpoint do
require Logger
@spec find_endpoint(url :: String.t()) :: URI.t() | nil
def find_endpoint(url) do
case find_endpoint_by_header(url) do
nil ->
find_endpoint_by_html(url)
:error ->
:error
endpoint ->
endpoint
end
end
defp find_endpoint_by_header(url) do
case Clacks.HTTP.head(url) do
{:ok, %HTTPoison.Response{headers: headers, request: %HTTPoison.Request{url: final_url}}} ->
headers
|> Enum.filter(fn {name, _} -> String.downcase(name) == "link" end)
|> webmention_link()
|> case do
nil ->
nil
str when is_binary(str) ->
URI.merge(final_url, str)
end
{:error, reason} ->
Logger.warn("Unable to find Webmention endpoint for '#{url}': #{reason}")
:error
end
end
defp webmention_link([]), do: nil
defp webmention_link([{_, value} | rest]) do
String.split(value, ",")
|> Enum.map(&parse_link_header/1)
|> Enum.find(fn {rels, _} -> "webmention" in rels end)
|> case do
nil ->
webmention_link(rest)
{_, res} ->
res
end
end
defp parse_link_header(value) do
[value | params] = String.split(value, ";")
uri_reference =
value
|> String.trim()
|> String.slice(1..-1)
{_, rel} =
params
|> Enum.map(fn str ->
str = String.trim(str)
[name | rest] = String.split(str, "=")
rest = Enum.join(rest, "=")
value =
if String.starts_with?(rest, "\"") do
{_, rest} = String.split_at(rest, 1)
if String.ends_with?(rest, "\"") do
{rest, _} = String.split_at(rest, -1)
rest
else
rest
end
else
rest
end
{name, value}
end)
|> Enum.find(fn {name, _} -> String.downcase(name) == "rel" end)
rels = String.split(rel, ~r/\s+/) |> Enum.map(&String.downcase/1)
{rels, uri_reference}
end
defp find_endpoint_by_html(url) do
case Clacks.HTTP.get(url) do
{:ok, %HTTPoison.Response{body: body, request: %HTTPoison.Request{url: final_url}}} ->
{:ok, doc} = Floki.parse_document(body)
Floki.find(doc, "link[rel~=webmention], a[rel~=webmention]")
|> Enum.reduce_while(nil, fn el, _acc ->
case Floki.attribute(el, "href") do
[href] when is_binary(href) ->
{:halt, href}
_ ->
{:cont, nil}
end
end)
|> case do
nil ->
nil
str when is_binary(str) ->
URI.merge(final_url, str)
end
{:error, reason} ->
Logger.warn("Unable to find Webmention endpoint for '#{url}': #{reason}")
:error
end
end
end