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>
This commit is contained in:
164
shared/models/sx_pub.py
Normal file
164
shared/models/sx_pub.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""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}>"
|
||||
Reference in New Issue
Block a user