Files
rose-ash/shared/models/sx_pub.py
giles 7b3d763291 sx-pub Phase 1: DB schema, IPFS wiring, actor + webfinger + collections + status endpoints
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>
2026-03-25 01:17:27 +00:00

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}>"