Foundation for the sx-pub federated publishing protocol: - 6 SQLAlchemy models: actor, collections, documents, activities, followers, following - Conditional DB enablement in sx_docs (DATABASE_URL present → enable DB) - Python IO helpers: get_or_create_actor (auto-generates RSA keypair), list_collections, check_status, seed_default_collections - 4 defhandler endpoints returning text/sx (no JSON): /pub/actor, /pub/webfinger, /pub/collections, /pub/status - Alembic migration infrastructure for sx service - Docker compose: DB + pgbouncer + IPFS + env vars Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
6.7 KiB
Python
165 lines
6.7 KiB
Python
"""sx-pub ORM models — federated SX publishing protocol.
|
|
|
|
Tables for the sx-pub actor, content collections, published documents,
|
|
outbox activities, and federation relationships (followers/following).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
String, Integer, DateTime, Text,
|
|
ForeignKey, UniqueConstraint, Index, func,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from shared.db.base import Base
|
|
|
|
|
|
class SxPubActor(Base):
|
|
"""Singleton actor for this sx-pub instance."""
|
|
__tablename__ = "sx_pub_actor"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
preferred_username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
|
display_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
public_key_pem: Mapped[str] = mapped_column(Text, nullable=False)
|
|
private_key_pem: Mapped[str] = mapped_column(Text, nullable=False)
|
|
domain: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SxPubActor @{self.preferred_username}>"
|
|
|
|
|
|
class SxPubCollection(Base):
|
|
"""Named grouping of published documents (e.g. core-specs, platforms)."""
|
|
__tablename__ = "sx_pub_collections"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
slug: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
|
|
documents = relationship("SxPubDocument", back_populates="collection", lazy="selectin")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SxPubCollection {self.slug}>"
|
|
|
|
|
|
class SxPubDocument(Base):
|
|
"""Published content — path→CID index entry."""
|
|
__tablename__ = "sx_pub_documents"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
collection_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("sx_pub_collections.id", ondelete="CASCADE"), nullable=False,
|
|
)
|
|
slug: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
title: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
content_hash: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
ipfs_cid: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
requires: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
status: Mapped[str] = mapped_column(
|
|
String(20), nullable=False, default="draft", server_default="draft",
|
|
)
|
|
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
|
|
collection = relationship("SxPubCollection", back_populates="documents")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("collection_id", "slug", name="uq_pub_doc_collection_slug"),
|
|
Index("ix_pub_doc_collection", "collection_id"),
|
|
Index("ix_pub_doc_status", "status"),
|
|
Index("ix_pub_doc_cid", "ipfs_cid"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SxPubDocument {self.slug} cid={self.ipfs_cid}>"
|
|
|
|
|
|
class SxPubActivity(Base):
|
|
"""Outbox activity (Publish, Follow, Announce, Anchor)."""
|
|
__tablename__ = "sx_pub_activities"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
activity_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
object_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
object_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
ipfs_cid: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
published: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
|
|
__table_args__ = (
|
|
Index("ix_pub_activity_type", "activity_type"),
|
|
Index("ix_pub_activity_published", "published"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SxPubActivity {self.activity_type}>"
|
|
|
|
|
|
class SxPubFollower(Base):
|
|
"""Remote server that follows us."""
|
|
__tablename__ = "sx_pub_followers"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
follower_acct: Mapped[str] = mapped_column(String(512), nullable=False)
|
|
follower_inbox: Mapped[str] = mapped_column(String(512), nullable=False)
|
|
follower_actor_url: Mapped[str] = mapped_column(String(512), nullable=False)
|
|
follower_public_key: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
state: Mapped[str] = mapped_column(
|
|
String(20), nullable=False, default="accepted", server_default="accepted",
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("follower_acct", name="uq_pub_follower_acct"),
|
|
Index("ix_pub_follower_state", "state"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SxPubFollower {self.follower_acct}>"
|
|
|
|
|
|
class SxPubFollowing(Base):
|
|
"""Remote server we follow."""
|
|
__tablename__ = "sx_pub_following"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
remote_actor_url: Mapped[str] = mapped_column(String(512), unique=True, nullable=False)
|
|
remote_inbox: Mapped[str] = mapped_column(String(512), nullable=False)
|
|
state: Mapped[str] = mapped_column(
|
|
String(20), nullable=False, default="pending", server_default="pending",
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
|
)
|
|
accepted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index("ix_pub_following_state", "state"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SxPubFollowing {self.remote_actor_url}>"
|