From b6d7f45c60fe80b9cdaf0f7d7d5b55ee90e804f9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 25 Aug 2021 16:11:08 -0400 Subject: [PATCH] Some things that I've long since forgotten. Let's hope they work --- lib/clacks/activity.ex | 54 ++++++++++++++++--- lib/clacks/activitypub.ex | 8 +-- lib/clacks/activitypub/federator.ex | 52 +++++++++++++----- lib/clacks/activitypub/fetcher.ex | 2 +- lib/clacks/actor.ex | 9 +++- lib/clacks/inbox.ex | 13 ++++- lib/clacks/object.ex | 12 ++--- lib/clacks/timeline.ex | 3 ++ lib/clacks/user.ex | 7 ++- lib/clacks/user_actions_helper.ex | 12 +++-- lib/clacks/worker/federate.ex | 4 +- lib/clacks/worker/send_webmention.ex | 5 +- .../controllers/frontend_controller.ex | 14 ++--- .../templates/frontend/_timeline.html.eex | 4 +- .../templates/frontend/search.html.eex | 2 +- .../templates/frontend/status.html.eex | 4 +- lib/clacks_web/views/frontend_view.ex | 9 ++-- 17 files changed, 149 insertions(+), 65 deletions(-) diff --git a/lib/clacks/activity.ex b/lib/clacks/activity.ex index e4b955e..e4b9284 100644 --- a/lib/clacks/activity.ex +++ b/lib/clacks/activity.ex @@ -2,7 +2,7 @@ defmodule Clacks.Activity do require Logger use Ecto.Schema import Ecto.Changeset - alias Clacks.Repo + alias Clacks.{Repo, Object} import Ecto.Query @type t() :: %__MODULE__{} @@ -34,9 +34,16 @@ defmodule Clacks.Activity do }) end - @spec get(id :: String.t()) :: t() | nil - def get(id) do - Repo.get(__MODULE__, id) + @spec get(id :: String.t(), opts :: Keyword.t()) :: t() | nil + def get(id, opts \\ []) do + if Keyword.get(opts, :with_object, false) do + __MODULE__ + |> where([a], a.id == ^id) + |> preload_object() + |> Repo.one() + else + Repo.get(__MODULE__, id) + end end @spec get_by_ap_id(ap_id :: String.t(), force_refetch :: boolean()) :: t() | nil @@ -55,10 +62,32 @@ defmodule Clacks.Activity do @spec get_by_object_ap_id(object_id :: String.t()) :: t() | nil def get_by_object_ap_id(object_id) do - Repo.one( - from a in __MODULE__, - where: fragment("?->'object'->>'id'", a.data) == ^object_id + __MODULE__ + |> where( + [a], + fragment("COALESCE(?->'object'->>'id', ?->>'object')", a.data, a.data) == ^object_id ) + |> preload_object() + |> Repo.one() + end + + @spec join_with_object(Ecto.Queryable.t()) :: Ecto.Query.t() + def join_with_object(query) do + join(query, :inner, [a], o in Object, + as: :object, + on: + fragment("?->>'id' = COALESCE(?->'object'->>'id', ?->>'object')", o.data, a.data, a.data) + ) + end + + @spec preload_object(Ecto.Queryable.t()) :: Ecto.Query.t() + def preload_object(query) do + if Ecto.Query.has_named_binding?(query, :object) do + query + else + join_with_object(query) + end + |> preload([_a, object: object], object: object) end @spec fetch(ap_id :: String.t()) :: t() | nil @@ -91,4 +120,15 @@ defmodule Clacks.Activity do end end end + + @spec data_with_object(activity :: Activity.t()) :: map() + def data_with_object(%__MODULE__{ + data: %{"object" => object_id} = data, + object: %Object{data: object_data} + }) + when is_binary(object_id) do + Map.put(data, "object", object_data) + end + + def data_with_object(%___MODULE__{data: data}), do: data end diff --git a/lib/clacks/activitypub.ex b/lib/clacks/activitypub.ex index 5c991d1..d482bf1 100644 --- a/lib/clacks/activitypub.ex +++ b/lib/clacks/activitypub.ex @@ -78,20 +78,20 @@ defmodule Clacks.ActivityPub do } end - @spec create( + @spec internal_create( object :: map(), id :: String.t() | nil, actor :: String.t() | nil, to :: [String.t()] | nil, cc :: [String.t()] | nil ) :: map() - def create(object, id \\ nil, actor \\ nil, to \\ nil, cc \\ nil) do + def internal_create(object, id \\ nil, actor \\ nil, to \\ nil, cc \\ nil) do %{ "@context" => @context, "id" => id || activity_id(Ecto.UUID.generate()), "actor" => actor || object["actor"], "type" => "Create", - "object" => object, + "object" => object["id"], "to" => to || object["to"], "cc" => cc || object["cc"] } @@ -103,7 +103,7 @@ defmodule Clacks.ActivityPub do %{ "@context" => @context, "type" => "Create", - "object" => object, + "object" => object["id"], "actor" => object["actor"] || object["attributedTo"], "to" => object["to"], "cc" => object["cc"] diff --git a/lib/clacks/activitypub/federator.ex b/lib/clacks/activitypub/federator.ex index 1a0cbb3..f665e5d 100644 --- a/lib/clacks/activitypub/federator.ex +++ b/lib/clacks/activitypub/federator.ex @@ -1,19 +1,39 @@ defmodule Clacks.ActivityPub.Federator do require Logger - alias Clacks.{Repo, Actor, User, Keys} + alias Clacks.{Repo, Actor, User, Keys, Activity} import Ecto.Query @public "https://www.w3.org/ns/activitystreams#Public" - @spec federate_to_followers(activity :: map(), actor :: Actor.t()) :: :ok | {:error, any()} - def federate_to_followers(activity, actor) do - Repo.all( - from a in Actor, where: fragment("?->>'id'", a.data) in ^actor.followers, select: a.data - ) - |> Enum.map(&inbox_for(activity, &1)) + @spec federate_to_involved(activity :: Activity.t(), actor :: Actor.t()) :: + :ok | {:error, any()} + def federate_to_involved(%Activity{data: %{"to" => to, "cc" => cc}} = activity, actor) do + activity_for_federating = Activity.data_with_object(activity) + + addressed = + (to ++ cc) + |> Enum.uniq() + |> List.delete(@public) + + addressed_actors = + if actor.data["followers"] in addressed do + addressed = List.delete(addressed, actor.data["followers"]) + [actor.followers | addressed] + else + addressed + end + + Actor + |> where([a], a.ap_id in ^addressed_actors) + |> select([a], %{ + shared_inbox: fragment("?->'endpoints'->>'sharedInbox'", a.data), + inbox: fragment("?->>'inbox'", a.data) + }) + |> Repo.all() + |> Enum.map(&inbox_for(activity_for_federating, &1)) |> Enum.uniq() |> Enum.reduce_while(:ok, fn inbox, _acc -> - case federate(activity, inbox) do + case do_federate(activity_for_federating, inbox) do {:error, _} = err -> {:halt, err} @@ -23,8 +43,14 @@ defmodule Clacks.ActivityPub.Federator do end) end - @spec federate(activity :: map(), inbox :: String.t()) :: :ok | {:error, any()} - def federate(%{"actor" => actor_id} = activity, inbox) do + @spec federate(Activity.t(), String.t()) :: :ok | {:error, any()} + def federate(activity, inbox) do + activity_for_federating = Activity.data_with_object(activity) + do_federate(activity_for_federating, inbox) + end + + @spec do_federate(activity :: map(), inbox :: String.t()) :: :ok | {:error, any()} + defp do_federate(%{"actor" => actor_id} = activity, inbox) do Logger.debug("Federating #{activity["id"]} to #{inbox}") %{host: inbox_host, path: inbox_path} = URI.parse(inbox) @@ -71,13 +97,13 @@ defmodule Clacks.ActivityPub.Federator do shared_inbox_for(actor) true -> - actor["inbox"] + actor.inbox end end @spec shared_inbox_for(actor :: map()) :: String.t() - defp shared_inbox_for(%{"endpoints" => %{"sharedInbox" => shared}}), do: shared - defp shared_inbox_for(%{"inbox" => inbox}), do: inbox + defp shared_inbox_for(%{shared_inbox: shared}) when not is_nil(shared), do: shared + defp shared_inbox_for(%{inbox: inbox}), do: inbox @spec signature_timestamp() :: String.t() defp signature_timestamp(date \\ NaiveDateTime.utc_now()) do diff --git a/lib/clacks/activitypub/fetcher.ex b/lib/clacks/activitypub/fetcher.ex index fbefd86..0ccf273 100644 --- a/lib/clacks/activitypub/fetcher.ex +++ b/lib/clacks/activitypub/fetcher.ex @@ -44,7 +44,7 @@ defmodule Clacks.ActivityPub.Fetcher do data else {:error, reason} -> - Logger.warn("Couldn't fetch AP object at #{uri}: #{inspect(reason)}") + Logger.warn("Couldn't fetch AP object at #{uri}: #{reason}") nil end end diff --git a/lib/clacks/actor.ex b/lib/clacks/actor.ex index f2a0b32..f735490 100644 --- a/lib/clacks/actor.ex +++ b/lib/clacks/actor.ex @@ -5,7 +5,14 @@ defmodule Clacks.Actor do import Ecto.Query alias Clacks.Repo - @type t() :: %__MODULE__{} + @type t() :: %__MODULE__{ + ap_id: String.t(), + nickname: String.t(), + local: boolean(), + data: map(), + followers: [String.t()], + user: Clacks.User.t() + } @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} diff --git a/lib/clacks/inbox.ex b/lib/clacks/inbox.ex index a014af8..dbcb3e6 100644 --- a/lib/clacks/inbox.ex +++ b/lib/clacks/inbox.ex @@ -4,9 +4,18 @@ defmodule Clacks.Inbox do defp store_activity(%{"actor" => actor, "id" => ap_id} = activity, local \\ false) when is_binary(actor) do + activity_without_embedded_object = + case Map.get(activity, "object") do + %{"id" => object_id} -> + Map.put(activity, "object", object_id) + + _ -> + activity + end + changeset = Activity.changeset(Activity.get_cached_by_ap_id(ap_id) || %Activity{}, %{ - data: activity, + data: activity_without_embedded_object, local: local, actor: actor }) @@ -61,7 +70,7 @@ defmodule Clacks.Inbox do Logger.error("Couldn't store Accept activity: #{inspect(changeset)}") {:error, "Couldn't store Accept activity"} - {:ok, _accept} -> + {:ok, accept} -> ActivityPub.Federator.federate(accept, follower.data["inbox"]) end end diff --git a/lib/clacks/object.ex b/lib/clacks/object.ex index 7e452f2..1d34a1a 100644 --- a/lib/clacks/object.ex +++ b/lib/clacks/object.ex @@ -53,15 +53,15 @@ defmodule Clacks.Object do Repo.one(from o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id) end - @spec fetch(ap_id :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) :: + @spec fetch(url :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) :: t() | Activity.t() | nil - def fetch(ap_id, synthesize_create \\ true, return \\ :object) do - case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do + def fetch(url, synthesize_create \\ true, return \\ :object) do + case Clacks.ActivityPub.Fetcher.fetch_object(url) do nil -> nil - data -> - existing = get_cached_by_ap_id(data["id"]) + %{"id" => ap_id} = data -> + existing = get_cached_by_ap_id(ap_id) changeset = changeset(existing || %__MODULE__{}, %{ @@ -87,7 +87,7 @@ defmodule Clacks.Object do }) {:ok, create} = Repo.insert_or_update(changeset) - create + %Clacks.Activity{create | object: object} else nil end diff --git a/lib/clacks/timeline.ex b/lib/clacks/timeline.ex index c94855d..9313bc5 100644 --- a/lib/clacks/timeline.ex +++ b/lib/clacks/timeline.ex @@ -21,6 +21,7 @@ defmodule Clacks.Timeline do |> restrict_to_public(only_public) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) + |> Activity.preload_object() |> join_with_announced_or_liked() |> select( [activity, announced, announced_actor], @@ -52,6 +53,7 @@ defmodule Clacks.Timeline do |> restrict_to_types(@timeline_types) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) + |> Activity.preload_object() |> join_with_announced_or_liked() |> select( [activity, actor, announced, announced_actor], @@ -70,6 +72,7 @@ defmodule Clacks.Timeline do |> restrict_to_types(@timeline_types) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) + |> Activity.preload_object() |> join_with_actors() |> join_with_announced_or_liked() |> select( diff --git a/lib/clacks/user.ex b/lib/clacks/user.ex index 73b75c7..727a889 100644 --- a/lib/clacks/user.ex +++ b/lib/clacks/user.ex @@ -3,7 +3,12 @@ defmodule Clacks.User do import Ecto.Changeset alias Clacks.Repo - @type t() :: %__MODULE__{} + @type t() :: %__MODULE__{ + username: String.t(), + private_key: String.t(), + password_hash: String.t(), + actor: Clacks.Actor.t() + } schema "users" do field :username, :string diff --git a/lib/clacks/user_actions_helper.ex b/lib/clacks/user_actions_helper.ex index 91a3b7b..653462d 100644 --- a/lib/clacks/user_actions_helper.ex +++ b/lib/clacks/user_actions_helper.ex @@ -17,7 +17,9 @@ defmodule Clacks.UserActionsHelper do def post_status(author, content, content_type, in_reply_to_ap_id) when is_binary(in_reply_to_ap_id) do - case Activity.get_by_ap_id(in_reply_to_ap_id) do + activity = Activity.get_by_ap_id(in_reply_to_ap_id) |> Activity.preload_object() + + case activity do nil -> {:error, "Could find post to reply to with AP ID '#{in_reply_to_ap_id}'"} @@ -33,7 +35,7 @@ defmodule Clacks.UserActionsHelper do note_changeset = Object.changeset_for_creating(note) {:ok, _object} = Repo.insert(note_changeset) - %{"id" => ap_id} = create = ActivityPub.create(note) + %{"id" => create_ap_id} = create = ActivityPub.internal_create(note, author.actor.ap_id) case ActivityPub.Helper.save_and_federate(create, author.actor) do {:ok, activity} -> @@ -42,7 +44,7 @@ defmodule Clacks.UserActionsHelper do {:ok, activity} :error -> - {:error, "Unable to save and federate activity with ID '#{ap_id}'"} + {:error, "Unable to save and federate activity with ID '#{create_ap_id}'"} end end @@ -75,7 +77,9 @@ defmodule Clacks.UserActionsHelper do {nil, nil, nil} %Activity{ - data: %{"id" => in_reply_to_ap_id, "context" => context, "actor" => in_reply_to_actor} + object: %Object{ + data: %{"id" => in_reply_to_ap_id, "context" => context, "actor" => in_reply_to_actor} + } } -> {context, in_reply_to_ap_id, in_reply_to_actor} end diff --git a/lib/clacks/worker/federate.ex b/lib/clacks/worker/federate.ex index 008af08..1d19e0d 100644 --- a/lib/clacks/worker/federate.ex +++ b/lib/clacks/worker/federate.ex @@ -7,9 +7,9 @@ defmodule Clacks.Worker.Federate do require Logger def perform(%{"id" => activity_id, "actor_id" => actor_id}, _job) do - %Activity{data: activity_data} = Repo.get(Activity, activity_id) + activity = Repo.get(Activity, activity_id) actor = Repo.get(Actor, actor_id) - :ok = ActivityPub.Federator.federate_to_followers(activity_data, actor) + :ok = ActivityPub.Federator.federate_to_involved(activity, actor) end end diff --git a/lib/clacks/worker/send_webmention.ex b/lib/clacks/worker/send_webmention.ex index 4d77149..2839879 100644 --- a/lib/clacks/worker/send_webmention.ex +++ b/lib/clacks/worker/send_webmention.ex @@ -6,10 +6,7 @@ defmodule Clacks.Worker.SendWebmention do require Logger @spec enqueue_for_activity(Activity.t()) :: :ok - def enqueue_for_activity( - %Activity{data: %{"type" => "Create", "object" => %{"content" => content} = note}} = - activity - ) do + def enqueue_for_activity(%Activity{object: %{"content" => content} = note} = activity) do tags = Map.get(note, "tag", []) tag_hrefs = Enum.map(tags, fn %{"href" => href} -> href end) diff --git a/lib/clacks_web/controllers/frontend_controller.ex b/lib/clacks_web/controllers/frontend_controller.ex index 7e59561..d5d7292 100644 --- a/lib/clacks_web/controllers/frontend_controller.ex +++ b/lib/clacks_web/controllers/frontend_controller.ex @@ -57,16 +57,12 @@ defmodule ClacksWeb.FrontendController do with %Activity{ local: true, - data: - %{ - "type" => "Create", - "object" => %{"type" => "Note", "attributedTo" => author_id} - } = data - } = activity <- Activity.get(id), + object: %Object{data: %{"type" => "Note", "attributedTo" => author_id}} + } = activity <- Activity.get(id, with_object: true), %Actor{} = author <- Actor.get_by_ap_id(author_id) do case conn.assigns[:format] do "activity+json" -> - json(conn, data) + json(conn, Activity.data_with_object(activity)) "html" -> render(conn, "status.html", %{ @@ -177,7 +173,7 @@ defmodule ClacksWeb.FrontendController do redirect(conn, to: ClacksWeb.FrontendView.local_actor_link(followee)) else follow = ActivityPub.follow(current_user.actor.ap_id, followee.ap_id) - ActivityPub.Helper.save_and_federate(follow, current_user.actor) + {:ok, _activity} = ActivityPub.Helper.save_and_federate(follow, current_user.actor) conn |> put_flash(:info, "Follow request sent") @@ -235,7 +231,7 @@ defmodule ClacksWeb.FrontendController do status_results = with %Activity{ actor: actor_id, - data: %{"type" => "Create", "object" => %{"type" => "Note"}} + object: %Object{} } = activity <- Object.fetch(q, true, :activity), actor <- Actor.get_by_ap_id(actor_id) do [{activity, actor}] diff --git a/lib/clacks_web/templates/frontend/_timeline.html.eex b/lib/clacks_web/templates/frontend/_timeline.html.eex index 672afe2..cf112ce 100644 --- a/lib/clacks_web/templates/frontend/_timeline.html.eex +++ b/lib/clacks_web/templates/frontend/_timeline.html.eex @@ -10,9 +10,9 @@
  • <%= if status.data["type"] == "Announce" do %> <% {announced_status, announced_actor} = announced %> - <%= render "_action_status.html", conn: @conn, action: :announce, action_activity: status, action_actor: author, original_activity: announced_status, original_note: announced_status.data["object"], original_actor: announced_actor %> + <%= render "_action_status.html", conn: @conn, action: :announce, action_activity: status, action_actor: author, original_activity: announced_status, original_note: announced_status.object.data, original_actor: announced_actor %> <% else %> - <%= render "_status.html", conn: @conn, author: author, status: status, note: status.data["object"] %> + <%= render "_status.html", conn: @conn, author: author, status: status, note: status.object.data %> <% end %>
  • <% end %> diff --git a/lib/clacks_web/templates/frontend/search.html.eex b/lib/clacks_web/templates/frontend/search.html.eex index d3e8099..93859d7 100644 --- a/lib/clacks_web/templates/frontend/search.html.eex +++ b/lib/clacks_web/templates/frontend/search.html.eex @@ -26,6 +26,6 @@
    <%= for {status, author} <- @status_results do %> - <%= render "_status.html", conn: @conn, author: author, status: status, note: status.data["object"] %> + <%= render "_status.html", conn: @conn, author: author, status: status, note: status.object.data %> <% end %> <% end %> diff --git a/lib/clacks_web/templates/frontend/status.html.eex b/lib/clacks_web/templates/frontend/status.html.eex index c0efd2b..1ec6399 100644 --- a/lib/clacks_web/templates/frontend/status.html.eex +++ b/lib/clacks_web/templates/frontend/status.html.eex @@ -1,8 +1,8 @@ -<%= render "_status.html", conn: @conn, author: @author, status: @status, note: @status.data["object"] %> +<%= render "_status.html", conn: @conn, author: @author, status: @status, note: @status.object.data %> <%= unless is_nil(@current_user) do %>
    - <%= render "_post_form.html", conn: @conn, in_reply_to: @status.data["object"]["id"], placeholder: "Reply", content: mentions_for_replying_to(@conn, @status) %> + <%= render "_post_form.html", conn: @conn, in_reply_to: @status.object.data["id"], placeholder: "Reply", content: mentions_for_replying_to(@conn, @status) %>
    <% end %> diff --git a/lib/clacks_web/views/frontend_view.ex b/lib/clacks_web/views/frontend_view.ex index 62c266b..48b9acc 100644 --- a/lib/clacks_web/views/frontend_view.ex +++ b/lib/clacks_web/views/frontend_view.ex @@ -1,6 +1,6 @@ defmodule ClacksWeb.FrontendView do use ClacksWeb, :view - alias Clacks.{Actor, Activity, Repo, Notification} + alias Clacks.{Actor, Activity, Repo, Notification, Object} alias ClacksWeb.Router.Helpers, as: Routes alias ClacksWeb.Endpoint require Logger @@ -98,7 +98,7 @@ defmodule ClacksWeb.FrontendView do @spec mentions_for_replying_to(Activity.t()) :: String.t() defp mentions_for_replying_to(conn, %Activity{ - data: %{"object" => %{"actor" => actor, "tag" => tags}} + object: %Object{data: %{"actor" => actor, "tag" => tags}} }) do current_user = conn.assigns[:user] |> Repo.preload(:actor) @@ -135,10 +135,7 @@ defmodule ClacksWeb.FrontendView do @spec render_status_content(activity :: Activity.t()) :: String.t() defp render_status_content(%Activity{ - data: %{ - "type" => "Create", - "object" => %{"type" => "Note", "content" => content} = note - } + object: %Object{data: %{"type" => "Note", "content" => content} = note} }) do with %{"tag" => tags} <- note, {:ok, tree} <- Floki.parse_fragment(content) do