nulla/lib/nulla/models/actor.ex
2025-06-29 19:29:13 +02:00

164 lines
4.3 KiB
Elixir

defmodule Nulla.Models.Actor do
use Ecto.Schema
import Ecto.Changeset
alias Nulla.Repo
alias Nulla.Snowflake
alias Nulla.Models.Note
@primary_key {:id, :integer, autogenerate: false}
schema "actors" do
field :domain, :string
field :ap_id, :string
field :type, :string
field :following, :string
field :followers, :string
field :inbox, :string
field :outbox, :string
field :featured, :string
field :featuredTags, :string
field :preferredUsername, :string
field :name, :string
field :summary, :string
field :url, :string
field :manuallyApprovesFollowers, :boolean
field :discoverable, :boolean, default: true
field :indexable, :boolean, default: true
field :published, :utc_datetime
field :memorial, :boolean, default: false
field :publicKey, :map
field :tag, {:array, :map}, default: []
field :attachment, {:array, :map}, default: []
field :endpoints, :map
field :icon, :map
field :image, :map
field :vcard_bday, :date
field :vcard_Address, :string
has_many :notes, Note
has_many :media_attachments, through: [:notes, :media_attachments]
end
@doc false
def changeset(actor, attrs) do
actor
|> cast(attrs, [
:id,
:domain,
:ap_id,
:type,
:following,
:followers,
:inbox,
:outbox,
:featured,
:featuredTags,
:preferredUsername,
:name,
:summary,
:url,
:manuallyApprovesFollowers,
:discoverable,
:indexable,
:published,
:memorial,
:publicKey,
:tag,
:attachment,
:endpoints,
:icon,
:image,
:vcard_bday,
:vcard_Address
])
|> validate_required([
:domain,
:ap_id,
:type,
:following,
:followers,
:inbox,
:outbox,
:preferredUsername,
:url,
:publicKey,
:endpoints
])
|> unique_constraint([:preferredUsername, :domain])
|> unique_constraint(:ap_id)
end
def create_actor(attrs) when is_map(attrs) do
id = Map.get(attrs, :id, Snowflake.next_id())
%__MODULE__{}
|> changeset(attrs)
|> put_change(:id, id)
|> Repo.insert()
end
def create_actor_minimal(username, domain, publicKeyPem) do
id = Snowflake.next_id()
attrs = %{
id: id,
domain: domain,
ap_id: "https://#{domain}/users/#{username}",
type: "Person",
following: "https://#{domain}/users/#{username}/following",
followers: "https://#{domain}/users/#{username}/followers",
inbox: "https://#{domain}/users/#{username}/inbox",
outbox: "https://#{domain}/users/#{username}/outbox",
featured: "https://#{domain}/users/#{username}/collections/featured",
featuredTags: "https://#{domain}/users/#{username}/collections/tags",
preferredUsername: username,
url: "https://#{domain}/@#{username}",
manuallyApprovesFollowers: false,
discoverable: true,
indexable: true,
published: DateTime.utc_now(),
memorial: false,
publicKey:
Jason.OrderedObject.new(
id: "https://#{domain}/users/#{username}#main-key",
owner: "https://#{domain}/users/#{username}",
publicKeyPem: publicKeyPem
),
endpoints: Jason.OrderedObject.new(sharedInbox: "https://#{domain}/inbox")
}
%__MODULE__{}
|> changeset(attrs)
|> Repo.insert()
end
def get_actor(by) when is_map(by) or is_list(by) do
Repo.get_by(__MODULE__, by)
end
def get_or_create_actor(%{"id" => ap_id} = actor_json) when is_binary(ap_id) do
case get_actor(ap_id: ap_id) do
nil ->
params =
actor_json
|> Map.put("ap_id", ap_id)
|> Map.delete("id")
|> Map.put("domain", URI.parse(ap_id).host)
case create_actor(params) do
{:ok, actor} -> {:ok, actor}
{:error, changeset} -> {:error, {:actor_creation_failed, changeset}}
end
actor ->
updates =
actor_json
|> Map.delete("id")
|> Map.put("domain", URI.parse(ap_id).host)
case changeset(actor, updates) |> Repo.update() do
{:ok, updated_actor} -> {:ok, updated_actor}
{:error, changeset} -> {:error, {:actor_update_failed, changeset}}
end
end
end
end