clacks/lib/clacks/activity.ex

143 lines
3.8 KiB
Elixir

defmodule Clacks.Activity do
require Logger
use Ecto.Schema
import Ecto.Changeset
alias Clacks.{Repo, Object}
import Ecto.Query
@type t() :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
schema "activities" do
field :data, :map
field :local, :boolean
belongs_to :actor, Clacks.Actor, foreign_key: :actor_ap_id, references: :ap_id, type: :string
has_one :object, Clacks.Object, on_delete: :nothing, foreign_key: :id
timestamps()
end
def changeset(%__MODULE__{} = schema, attrs) do
schema
|> cast(attrs, [:data, :local, :actor_ap_id])
|> validate_required([:data, :local, :actor_ap_id])
end
@spec changeset_for_creating(activity :: map(), local :: boolean()) :: Ecto.Changeset.t()
def changeset_for_creating(activity, local \\ false) do
changeset(%__MODULE__{}, %{
data: activity,
local: local,
actor_ap_id: activity["actor"]
})
end
@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
def get_by_ap_id(ap_id, force_refetch \\ false) do
if force_refetch do
fetch(ap_id)
else
get_cached_by_ap_id(ap_id) || fetch(ap_id)
end
end
@spec get_cached_by_ap_id(ap_id :: String.t()) :: t() | nil
def get_cached_by_ap_id(ap_id) do
Repo.one(from a in __MODULE__, where: fragment("?->>'id'", a.data) == ^ap_id)
end
@spec get_by_object_ap_id(object_id :: String.t(), type :: String.t()) :: t() | nil
def get_by_object_ap_id(object_id, type) do
__MODULE__
|> where(
[a],
fragment("?->>'type'", a.data) == ^type and
fragment("COALESCE(?->'object'->>'id', ?->>'object')", a.data, a.data) == ^object_id
)
|> preload_object()
# todo: may not need to preload actor by default
|> preload_actor()
|> 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 preload_actor(Ecto.Queryable.t()) :: Ecto.Query.t()
def preload_actor(query) do
preload(query, :actor)
end
@spec fetch(ap_id :: String.t()) :: t() | nil
def fetch(ap_id) do
case Clacks.ActivityPub.Fetcher.fetch_activity(ap_id) do
nil ->
nil
data ->
actor = data["actor"] || data["attributedTo"]
existing = get_cached_by_ap_id(data["id"])
changeset =
changeset(existing || %__MODULE__{}, %{
data: data,
local: false,
actor: actor
})
case Repo.insert_or_update(changeset) do
{:ok, activity} ->
_ = Clacks.Actor.get_by_ap_id(actor)
activity
{:error, changeset} ->
Logger.error("Couldn't store remote activity #{ap_id}: #{inspect(changeset)}")
nil
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