Some things that I've long since forgotten. Let's hope they work

This commit is contained in:
Shadowfacts 2021-08-25 16:11:08 -04:00
parent de495059d5
commit b6d7f45c60
17 changed files with 149 additions and 65 deletions

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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}]

View File

@ -10,9 +10,9 @@
<li>
<%= 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 %>
</li>
<% end %>

View File

@ -26,6 +26,6 @@
<hr>
<%= 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 %>

View File

@ -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 %>
<hr>
<%= 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) %>
<hr>
<% end %>

View File

@ -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