Some things that I've long since forgotten. Let's hope they work
This commit is contained in:
parent
de495059d5
commit
b6d7f45c60
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}]
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue