Send Webmentions to linked URLs, excluding mentions
This commit is contained in:
parent
7e38b460c6
commit
de7a1e617d
|
@ -38,10 +38,12 @@ config :http_signatures, adapter: Clacks.SignatureAdapter
|
||||||
config :clacks, Oban,
|
config :clacks, Oban,
|
||||||
repo: Clacks.Repo,
|
repo: Clacks.Repo,
|
||||||
prune: {:maxlen, 10_000},
|
prune: {:maxlen, 10_000},
|
||||||
queues: [federate: 10]
|
queues: [federate: 5, send_webmention: 1]
|
||||||
|
|
||||||
config :floki, :html_parser, Floki.HTMLParser.FastHtml
|
config :floki, :html_parser, Floki.HTMLParser.FastHtml
|
||||||
|
|
||||||
|
config :clacks, :send_webmentions, true
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{Mix.env()}.exs"
|
import_config "#{Mix.env()}.exs"
|
||||||
|
|
|
@ -13,18 +13,30 @@ defmodule Clacks.ActivityPub.Helper do
|
||||||
:error
|
:error
|
||||||
|
|
||||||
{:ok, activity} ->
|
{:ok, activity} ->
|
||||||
worker =
|
if Application.get_env(:clacks, :send_webmentions, true) do
|
||||||
%{id: activity.id, actor_id: actor.id}
|
Clacks.Worker.SendWebmention.enqueue_for_activity(activity)
|
||||||
|> Clacks.Worker.Federate.new()
|
end
|
||||||
|
|
||||||
case Oban.insert(worker) do
|
case enqueue_federate_job(activity, actor) do
|
||||||
{:ok, _} ->
|
:ok -> {:ok, activity}
|
||||||
{:ok, activity}
|
:error -> :error
|
||||||
|
|
||||||
{:error, changeset} ->
|
|
||||||
Logger.error("Couldn't save federate job: #{inspect(changeset)}")
|
|
||||||
:error
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec enqueue_federate_job(Activity.t(), Actor.t()) :: :ok | :error
|
||||||
|
defp enqueue_federate_job(activity, actor) do
|
||||||
|
worker =
|
||||||
|
%{id: activity.id, actor_id: actor.id}
|
||||||
|
|> Clacks.Worker.Federate.new()
|
||||||
|
|
||||||
|
case Oban.insert(worker) do
|
||||||
|
{:ok, _} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
Logger.error("Couldn't save federate job: #{inspect(changeset)}")
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
defmodule Clacks.HTTP do
|
defmodule Clacks.HTTP do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@spec get(url :: String.t(), headers :: [{String.t(), String.t()}]) ::
|
@spec get(url :: String.t(), headers :: HTTPoison.headers()) ::
|
||||||
{:ok, HTTPoison.Response.t()} | {:error, String.t()}
|
{:ok, HTTPoison.Response.t()} | {:error, String.t()}
|
||||||
def get(url, headers \\ []) do
|
def get(url, headers \\ []) do
|
||||||
fetch(:get, url, headers)
|
fetch(:get, url, headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec head(url :: String.t(), headers :: [{String.t(), String.t()}]) ::
|
@spec head(url :: String.t(), headers :: HTTPoison.headers()) ::
|
||||||
{:ok, HTTPoison.Response.t()} | {:error, String.t()}
|
{:ok, HTTPoison.Response.t()} | {:error, String.t()}
|
||||||
def head(url, headers \\ []) do
|
def head(url, headers \\ []) do
|
||||||
fetch(:head, url, headers)
|
fetch(:head, url, headers)
|
||||||
|
@ -27,7 +27,9 @@ defmodule Clacks.HTTP do
|
||||||
|> Enum.find(fn {name, _value} -> String.downcase(name) == "location" end)
|
|> Enum.find(fn {name, _value} -> String.downcase(name) == "location" end)
|
||||||
|> case do
|
|> case do
|
||||||
{_, new_url} ->
|
{_, new_url} ->
|
||||||
new_url = URI.merge(URI.parse(url), URI.parse(new_url))
|
new_url =
|
||||||
|
URI.merge(URI.parse(url), URI.parse(new_url))
|
||||||
|
|> URI.to_string()
|
||||||
|
|
||||||
Logger.debug("Got 301 redirect from #{url} to #{new_url}")
|
Logger.debug("Got 301 redirect from #{url} to #{new_url}")
|
||||||
fetch(method, new_url, headers)
|
fetch(method, new_url, headers)
|
||||||
|
@ -49,4 +51,10 @@ defmodule Clacks.HTTP do
|
||||||
{:error, inspect(reason)}
|
{:error, inspect(reason)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec post(String.t(), any(), HTTPoison.headers()) ::
|
||||||
|
{:ok, HTTPoison.Response.t()} | {:error, HTTPoison.Error.t()}
|
||||||
|
def post(url, body, headers \\ []) do
|
||||||
|
HTTPoison.post(url, body, headers)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,7 +56,7 @@ defmodule Clacks.Webmention.Endpoint do
|
||||||
uri_reference =
|
uri_reference =
|
||||||
value
|
value
|
||||||
|> String.trim()
|
|> String.trim()
|
||||||
|> String.slice(1..-1)
|
|> String.slice(1..-2)
|
||||||
|
|
||||||
{_, rel} =
|
{_, rel} =
|
||||||
params
|
params
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
defmodule Clacks.Worker.SendWebmention do
|
||||||
|
use Oban.Worker, queue: :send_webmention
|
||||||
|
alias Clacks.Activity
|
||||||
|
alias ClacksWeb.Router.Helpers, as: Routes
|
||||||
|
alias ClacksWeb.Endpoint
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@spec enqueue_for_activity(Activity.t()) :: :ok
|
||||||
|
def enqueue_for_activity(
|
||||||
|
%Activity{data: %{"type" => "Create", "object" => %{"content" => content} = note}} =
|
||||||
|
activity
|
||||||
|
) do
|
||||||
|
tags = Map.get(note, "tag", [])
|
||||||
|
tag_hrefs = Enum.map(tags, fn %{"href" => href} -> href end)
|
||||||
|
|
||||||
|
case Floki.parse_fragment(content) do
|
||||||
|
{:ok, html_tree} ->
|
||||||
|
# todo: should this also skip anchors with the .mention class?
|
||||||
|
Floki.find(html_tree, "a")
|
||||||
|
|> Enum.each(fn {"a", attrs, _} ->
|
||||||
|
case Enum.find(attrs, fn {name, _} -> name == "href" end) do
|
||||||
|
{"href", link} ->
|
||||||
|
IO.inspect("Found link: #{link}")
|
||||||
|
maybe_enqueue_for_candidate(activity, tag_hrefs, link)
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.warn("Unable to parse HTML for sending Webmentions: #{reason}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec maybe_enqueue_for_candidate(Activity.t(), [String.t()], String.t()) :: boolean()
|
||||||
|
defp maybe_enqueue_for_candidate(
|
||||||
|
%Activity{data: %{"object" => %{"in_reply_to" => in_reply_to}}} = activity,
|
||||||
|
activity_tag_hrefs,
|
||||||
|
link
|
||||||
|
) do
|
||||||
|
# todo: checking in_reply_to != link won't be adequate after we support replying via Webmention to non-AP posts
|
||||||
|
with false <- in_reply_to == link,
|
||||||
|
false <- Enum.member?(activity_tag_hrefs, link),
|
||||||
|
%URI{scheme: scheme} when scheme in ["http", "https"] <- URI.parse(link) do
|
||||||
|
enqueue(activity, link) == :ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_enqueue_for_candidate(activity, activity_tag_hrefs, link) do
|
||||||
|
with false <- Enum.member?(activity_tag_hrefs, link),
|
||||||
|
%URI{scheme: scheme} when scheme in ["http", "https"] <- URI.parse(link) do
|
||||||
|
enqueue(activity, link) == :ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec enqueue(Activity.t(), String.t()) :: :ok | :error
|
||||||
|
defp enqueue(activity, link) do
|
||||||
|
worker = __MODULE__.new(%{activity: activity.id, link: link})
|
||||||
|
|
||||||
|
case Oban.insert(worker) do
|
||||||
|
{:ok, _} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("Couldn't save send Webmention job: #{inspect(reason)}")
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%{"activity" => activity_id, "link" => link}, _job) do
|
||||||
|
case Clacks.Webmention.Endpoint.find_endpoint(link) do
|
||||||
|
nil ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
endpoint ->
|
||||||
|
endpoint = URI.to_string(endpoint)
|
||||||
|
|
||||||
|
source = Routes.activities_url(Endpoint, :get, activity_id)
|
||||||
|
target = URI.encode_www_form(link)
|
||||||
|
body = "source=#{source}&target=#{target}"
|
||||||
|
|
||||||
|
case Clacks.HTTP.post(endpoint, body, [
|
||||||
|
{"Content-Type", "application/x-www-form-urlencoded"}
|
||||||
|
]) do
|
||||||
|
{:ok, %HTTPoison.Response{status_code: status_code}} when status_code in 200..299 ->
|
||||||
|
Logger.debug("Successfully sent Webmention to '#{link}'")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %HTTPoison.Response{status_code: status_code}} ->
|
||||||
|
Logger.error(
|
||||||
|
"Unhandled status code #{status_code} for sending Webmention to '#{link}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error, "Unhandled status code for Webmention '#{link}': #{status_code}"}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
Logger.error("Failed sending Webmention to '#{link}': #{inspect(error)}")
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,11 +4,9 @@ defmodule ClacksWeb.ActivitiesController do
|
||||||
alias ClacksWeb.Router.Helpers, as: Routes
|
alias ClacksWeb.Router.Helpers, as: Routes
|
||||||
alias ClacksWeb.Endpoint
|
alias ClacksWeb.Endpoint
|
||||||
|
|
||||||
def get(conn, _params) do
|
def get(conn, %{"id" => id}) do
|
||||||
ap_id = current_url(conn, %{})
|
case Activity.get(id) do
|
||||||
|
%Activity{local: true, data: data} ->
|
||||||
case Activity.get_by_ap_id(ap_id) do
|
|
||||||
%Activity{local: true, id: id, data: data} ->
|
|
||||||
case conn.assigns[:format] do
|
case conn.assigns[:format] do
|
||||||
"activity+json" ->
|
"activity+json" ->
|
||||||
json(conn, data)
|
json(conn, data)
|
||||||
|
@ -29,16 +27,4 @@ defmodule ClacksWeb.ActivitiesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_status(conn, %{"id" => status_id}) do
|
|
||||||
case Activity.get(status_id) do
|
|
||||||
%Activity{local: true, data: data} ->
|
|
||||||
json(conn, data)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
conn
|
|
||||||
|> put_status(404)
|
|
||||||
|> json(%{error: "Not Found"})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<div class="<%= assigns[:class] || "status h-entry" %>">
|
<div class="<%= assigns[:class] || "status h-entry" %>">
|
||||||
<div class="status-meta">
|
<div class="status-meta">
|
||||||
<h2 class="status-author-nickname">
|
<div class="p-author h-card">
|
||||||
<a href="<%= local_actor_link(@author) %>" class="p-author">
|
<h2 class="status-author-nickname">
|
||||||
<%= @author.data["preferredUsername"] %>
|
<a href="<%= local_actor_link(@author) %>">
|
||||||
</a>
|
<%= @author.data["preferredUsername"] %>
|
||||||
</h2>
|
</a>
|
||||||
<h3 class="status-author-username">
|
</h2>
|
||||||
<a href="<%= @author.ap_id %>">
|
<h3 class="status-author-username">
|
||||||
<%= display_username(@author) %>
|
<a href="<%= @author.ap_id %>" class="p-name u-url">
|
||||||
</a>
|
<%= display_username(@author) %>
|
||||||
</h3>
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<p class="status-meta-right">
|
<p class="status-meta-right">
|
||||||
<time datetime="<%= @note["published"] %>" class="dt-published"><%= display_timestamp(@note["published"]) %></time>
|
<time datetime="<%= @note["published"] %>" class="dt-published"><%= display_timestamp(@note["published"]) %></time>
|
||||||
<a href="<%= @note["url"] || @note["id"] %>" class="status-permalink u-url">Permalink</a>
|
<a href="<%= @note["url"] || @note["id"] %>" class="status-permalink u-url">Permalink</a>
|
||||||
|
|
Loading…
Reference in New Issue