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