defmodule Clacks.Object do require Logger use Ecto.Schema import Ecto.Changeset alias Clacks.Repo import Ecto.Query @type t() :: %__MODULE__{} schema "objects" do field :data, :map timestamps() end def changeset(%__MODULE__{} = schema, attrs) do schema |> cast(attrs, [:data]) |> validate_required([:data]) end @spec changeset_for_creating(data :: map()) :: Ecto.Changeset.t() def changeset_for_creating(data) do changeset(%__MODULE__{}, %{data: data}) end @spec get_actor(object :: t()) :: Clacks.Actor.t() | nil def get_actor(object) do case object.data["actor"] || object.data["attributedTo"] do nil -> nil id when is_binary(id) -> Clacks.Actor.get_by_ap_id(id) end end @spec get_by_ap_id( ap_id :: String.t(), force_refetch :: boolean(), synthesize_create :: boolean() ) :: t() | nil def get_by_ap_id(ap_id, force_refetch \\ false, synthesize_create \\ true) do if force_refetch do fetch(ap_id, synthesize_create) else get_cached_by_ap_id(ap_id) || fetch(ap_id, synthesize_create) 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 o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id) end @spec fetch(url :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) :: t() | Activity.t() | nil def fetch(url, synthesize_create \\ true, return \\ :object) when is_binary(url) do case Clacks.ActivityPub.Fetcher.fetch_object(url) do nil -> nil %{"id" => ap_id} = data -> existing = get_cached_by_ap_id(ap_id) changeset = changeset(existing || %__MODULE__{}, %{ data: data }) case Repo.insert_or_update(changeset) do {:ok, object} -> actor = data["actor"] || data["attributedTo"] _ = Clacks.Actor.get_by_ap_id(actor) activity = case Clacks.Activity.get_by_object_ap_id(ap_id, "Create") do nil -> if synthesize_create do create = Clacks.ActivityPub.synthesized_create(data) changeset = Clacks.Activity.changeset(%Clacks.Activity{}, %{ data: create, local: false, actor: actor }) {:ok, create} = Repo.insert_or_update(changeset) %Clacks.Activity{create | object: object} else nil end %Clacks.Activity{} = activity -> activity end case return do :object -> object :activity -> activity end {:error, changeset} -> Logger.error("Couldn't store remote object #{ap_id}: #{inspect(changeset)}") nil end end end end