diff --git a/config/config.exs b/config/config.exs index a4567e5..0fd79b2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -38,10 +38,12 @@ config :http_signatures, adapter: Clacks.SignatureAdapter config :clacks, Oban, repo: Clacks.Repo, prune: {:maxlen, 10_000}, - queues: [federate: 10] + queues: [federate: 5, send_webmention: 1] config :floki, :html_parser, Floki.HTMLParser.FastHtml +config :clacks, :send_webmentions, true + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/clacks/activitypub/helper.ex b/lib/clacks/activitypub/helper.ex index 6b11936..28c19af 100644 --- a/lib/clacks/activitypub/helper.ex +++ b/lib/clacks/activitypub/helper.ex @@ -13,18 +13,30 @@ defmodule Clacks.ActivityPub.Helper do :error {:ok, activity} -> - worker = - %{id: activity.id, actor_id: actor.id} - |> Clacks.Worker.Federate.new() + if Application.get_env(:clacks, :send_webmentions, true) do + Clacks.Worker.SendWebmention.enqueue_for_activity(activity) + end - case Oban.insert(worker) do - {:ok, _} -> - {:ok, activity} - - {:error, changeset} -> - Logger.error("Couldn't save federate job: #{inspect(changeset)}") - :error + case enqueue_federate_job(activity, actor) do + :ok -> {:ok, activity} + :error -> :error 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 diff --git a/lib/clacks/http.ex b/lib/clacks/http.ex index 1acf9c6..ff0f291 100644 --- a/lib/clacks/http.ex +++ b/lib/clacks/http.ex @@ -1,13 +1,13 @@ defmodule Clacks.HTTP do 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()} def get(url, headers \\ []) do fetch(:get, url, headers) 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()} def head(url, headers \\ []) do fetch(:head, url, headers) @@ -27,7 +27,9 @@ defmodule Clacks.HTTP do |> Enum.find(fn {name, _value} -> String.downcase(name) == "location" end) |> case do {_, 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}") fetch(method, new_url, headers) @@ -49,4 +51,10 @@ defmodule Clacks.HTTP do {:error, inspect(reason)} 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 diff --git a/lib/clacks/webmention/endpoint.ex b/lib/clacks/webmention/endpoint.ex index 7395e47..368b7fe 100644 --- a/lib/clacks/webmention/endpoint.ex +++ b/lib/clacks/webmention/endpoint.ex @@ -56,7 +56,7 @@ defmodule Clacks.Webmention.Endpoint do uri_reference = value |> String.trim() - |> String.slice(1..-1) + |> String.slice(1..-2) {_, rel} = params diff --git a/lib/clacks/worker/send_webmention.ex b/lib/clacks/worker/send_webmention.ex new file mode 100644 index 0000000..4065a97 --- /dev/null +++ b/lib/clacks/worker/send_webmention.ex @@ -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 diff --git a/lib/clacks_web/controllers/activities_controller.ex b/lib/clacks_web/controllers/activities_controller.ex index e054a20..c76ed66 100644 --- a/lib/clacks_web/controllers/activities_controller.ex +++ b/lib/clacks_web/controllers/activities_controller.ex @@ -4,11 +4,9 @@ defmodule ClacksWeb.ActivitiesController do alias ClacksWeb.Router.Helpers, as: Routes alias ClacksWeb.Endpoint - def get(conn, _params) do - ap_id = current_url(conn, %{}) - - case Activity.get_by_ap_id(ap_id) do - %Activity{local: true, id: id, data: data} -> + def get(conn, %{"id" => id}) do + case Activity.get(id) do + %Activity{local: true, data: data} -> case conn.assigns[:format] do "activity+json" -> json(conn, data) @@ -29,16 +27,4 @@ defmodule ClacksWeb.ActivitiesController do 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 diff --git a/lib/clacks_web/templates/frontend/_status.html.eex b/lib/clacks_web/templates/frontend/_status.html.eex index da8a049..76a7194 100644 --- a/lib/clacks_web/templates/frontend/_status.html.eex +++ b/lib/clacks_web/templates/frontend/_status.html.eex @@ -1,15 +1,17 @@
">
-

- - <%= @author.data["preferredUsername"] %> - -

-

- - <%= display_username(@author) %> - -

+

" class="status-permalink u-url">Permalink