8.2 KiB · text 5af55d9
defmodule GitGud.Repo.Migrations.CreateFederation do
use Ecto.Migration
def change do
# ── actors ────────────────────────────────────────────────────────
#
# Single table for both local and remote actors.
#
# * Local: exactly one of local_user_id / local_organization_id /
# local_repository_id is set. private_key_pem is present.
# * Remote: all local_*_id are null. remote_doc holds the last
# fetched ActivityStreams document. private_key_pem is null.
create table(:actors) do
add :kind, :string, null: false
add :actor_type, :string, null: false
add :actor_url, :string, null: false, size: 500
add :inbox_url, :string, size: 500
add :outbox_url, :string, size: 500
add :followers_url, :string, size: 500
add :following_url, :string, size: 500
add :shared_inbox_url, :string, size: 500
add :preferred_username, :string, size: 100
add :name, :string, size: 200
add :summary, :text
# Crypto. Public is mandatory; private is only present for local
# actors.
add :public_key_pem, :text, null: false
add :public_key_id, :string, null: false, size: 500
add :private_key_pem, :text
# Local back-references — exactly one is set for kind="local".
add :local_user_id, references(:users, on_delete: :delete_all)
add :local_organization_id, references(:organizations, on_delete: :delete_all)
add :local_repository_id, references(:repositories, on_delete: :delete_all)
# Remote cache.
add :remote_doc, :map
add :last_fetched_at, :utc_datetime
add :fetch_error, :text
# Moderation state (per-actor).
# status: active | suspended | shadow_banned
add :status, :string, null: false, default: "active"
timestamps(type: :utc_datetime)
end
create unique_index(:actors, [:actor_url])
create unique_index(:actors, [:local_user_id])
create unique_index(:actors, [:local_organization_id])
create unique_index(:actors, [:local_repository_id])
create index(:actors, [:kind])
create index(:actors, [:actor_type])
create index(:actors, [:status])
# Partition invariant: local rows have exactly one local FK set
# AND have a private key; remote rows have no FKs.
execute(
"""
ALTER TABLE actors ADD CONSTRAINT actors_kind_local_invariant CHECK (
(kind = 'local' AND (
(local_user_id IS NOT NULL)::int +
(local_organization_id IS NOT NULL)::int +
(local_repository_id IS NOT NULL)::int
) = 1
AND private_key_pem IS NOT NULL
)
OR (kind = 'remote' AND
local_user_id IS NULL AND
local_organization_id IS NULL AND
local_repository_id IS NULL AND
private_key_pem IS NULL)
)
""",
"ALTER TABLE actors DROP CONSTRAINT actors_kind_local_invariant"
)
# ── activities ────────────────────────────────────────────────────
create table(:fed_activities) do
# The AP `id` field: a URL globally unique to the activity.
add :activity_url, :string, null: false, size: 500
# ActivityStreams type: "Follow", "Accept", "Create", "Update",
# "Delete", "Like", "Announce", "Undo", or ForgeFed extensions
# like "Push", "BranchOpen", etc.
add :type, :string, null: false, size: 80
add :actor_id, references(:actors, on_delete: :delete_all), null: false
# Either an actor or an object (note, ticket, …). We don't model
# arbitrary objects yet — store the raw and read fields as needed.
add :target_actor_id, references(:actors, on_delete: :nilify_all)
add :object_actor_id, references(:actors, on_delete: :nilify_all)
add :raw, :map, null: false
add :published_at, :utc_datetime
add :is_local, :boolean, null: false, default: false
timestamps(type: :utc_datetime)
end
create unique_index(:fed_activities, [:activity_url])
create index(:fed_activities, [:actor_id])
create index(:fed_activities, [:type])
create index(:fed_activities, [:is_local, :inserted_at])
# ── follows ───────────────────────────────────────────────────────
create table(:fed_follows) do
add :follower_actor_id, references(:actors, on_delete: :delete_all), null: false
add :target_actor_id, references(:actors, on_delete: :delete_all), null: false
# NULL until the target sends Accept.
add :accepted_at, :utc_datetime
add :follow_activity_id, references(:fed_activities, on_delete: :nilify_all)
timestamps(type: :utc_datetime)
end
create unique_index(:fed_follows, [:follower_actor_id, :target_actor_id])
create index(:fed_follows, [:target_actor_id])
create index(:fed_follows, [:accepted_at])
# ── inbound deliveries ────────────────────────────────────────────
#
# Raw record of every activity that hit one of our inboxes, before
# processing. Used for moderation review and replay.
create table(:fed_inbox_deliveries) do
add :target_actor_id, references(:actors, on_delete: :delete_all), null: false
add :source_actor_url, :string, size: 500
add :source_host, :string, size: 250
add :raw_body, :text, null: false
add :headers, :map, null: false, default: %{}
add :signature_verified, :boolean, null: false, default: false
# pending | accepted | rejected | failed | dropped_moderation
add :status, :string, null: false, default: "pending"
add :status_reason, :string, size: 500
add :processed_at, :utc_datetime
timestamps(type: :utc_datetime, updated_at: false)
end
create index(:fed_inbox_deliveries, [:target_actor_id, :inserted_at])
create index(:fed_inbox_deliveries, [:status])
create index(:fed_inbox_deliveries, [:source_host])
# ── outbound deliveries ───────────────────────────────────────────
create table(:fed_outbound_deliveries) do
add :activity_id, references(:fed_activities, on_delete: :delete_all), null: false
add :target_inbox_url, :string, null: false, size: 500
# pending | success | failure | dropped
add :status, :string, null: false, default: "pending"
add :status_code, :integer
add :attempts, :integer, null: false, default: 0
add :last_error, :text
add :delivered_at, :utc_datetime
timestamps(type: :utc_datetime)
end
create index(:fed_outbound_deliveries, [:activity_id])
create index(:fed_outbound_deliveries, [:status])
# ── moderation ────────────────────────────────────────────────────
#
# Allowlist / blocklist for whole instances, plus per-actor blocks.
create table(:fed_instance_blocks) do
add :host, :string, null: false, size: 250
# silence | suspend
# silence: drop inbound but allow outbound discovery
# suspend: hard block — also refuse to fetch their actors
add :severity, :string, null: false, default: "suspend"
add :reason, :text
timestamps(type: :utc_datetime)
end
create unique_index(:fed_instance_blocks, [:host])
create table(:fed_actor_blocks) do
add :actor_url, :string, null: false, size: 500
add :reason, :text
timestamps(type: :utc_datetime)
end
create unique_index(:fed_actor_blocks, [:actor_url])
# Optional global allowlist mode. When a row exists, only listed
# hosts may deliver to our inboxes.
create table(:fed_instance_allowlist) do
add :host, :string, null: false, size: 250
add :note, :string, size: 500
timestamps(type: :utc_datetime, updated_at: false)
end
create unique_index(:fed_instance_allowlist, [:host])
end
end