Add federation/ActivityPub models, contracts, and services

Phase 0+1 of ActivityPub integration:
- 6 ORM models (ActorProfile, APActivity, APFollower, APInboxItem, APAnchor, IPFSPin)
- FederationService protocol + SqlFederationService implementation + stub
- 4 DTOs (ActorProfileDTO, APActivityDTO, APFollowerDTO, APAnchorDTO)
- Registry slot for federation service
- Alembic migration for federation tables
- IPFS async client (httpx-based)
- HTTP Signatures (RSA-2048 sign/verify)
- login_url() now uses AUTH_APP env var for flexible auth routing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-21 15:10:08 +00:00
parent 7abef48cf2
commit 8850a0106a
14 changed files with 1158 additions and 4 deletions

View File

@@ -134,3 +134,56 @@ class CartSummaryDTO:
items: list[CartItemDTO] = field(default_factory=list)
ticket_count: int = 0
ticket_total: Decimal = Decimal("0")
# ---------------------------------------------------------------------------
# Federation / ActivityPub domain
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class ActorProfileDTO:
id: int
user_id: int
preferred_username: str
public_key_pem: str
display_name: str | None = None
summary: str | None = None
inbox_url: str | None = None
outbox_url: str | None = None
created_at: datetime | None = None
@dataclass(frozen=True, slots=True)
class APActivityDTO:
id: int
activity_id: str
activity_type: str
actor_profile_id: int
object_type: str | None = None
object_data: dict | None = None
published: datetime | None = None
is_local: bool = True
source_type: str | None = None
source_id: int | None = None
ipfs_cid: str | None = None
@dataclass(frozen=True, slots=True)
class APFollowerDTO:
id: int
actor_profile_id: int
follower_acct: str
follower_inbox: str
follower_actor_url: str
created_at: datetime | None = None
@dataclass(frozen=True, slots=True)
class APAnchorDTO:
id: int
merkle_root: str
activity_count: int = 0
tree_ipfs_cid: str | None = None
ots_proof_cid: str | None = None
confirmed_at: datetime | None = None
bitcoin_txid: str | None = None

View File

@@ -19,6 +19,9 @@ from .dtos import (
ProductDTO,
CartItemDTO,
CartSummaryDTO,
ActorProfileDTO,
APActivityDTO,
APFollowerDTO,
)
@@ -162,3 +165,67 @@ class CartService(Protocol):
async def adopt_cart_for_user(
self, session: AsyncSession, user_id: int, session_id: str,
) -> None: ...
@runtime_checkable
class FederationService(Protocol):
# -- Actor management -----------------------------------------------------
async def get_actor_by_username(
self, session: AsyncSession, username: str,
) -> ActorProfileDTO | None: ...
async def get_actor_by_user_id(
self, session: AsyncSession, user_id: int,
) -> ActorProfileDTO | None: ...
async def create_actor(
self, session: AsyncSession, user_id: int, preferred_username: str,
display_name: str | None = None, summary: str | None = None,
) -> ActorProfileDTO: ...
async def username_available(
self, session: AsyncSession, username: str,
) -> bool: ...
# -- Publishing (core cross-domain API) -----------------------------------
async def publish_activity(
self, session: AsyncSession, *,
actor_user_id: int,
activity_type: str,
object_type: str,
object_data: dict,
source_type: str | None = None,
source_id: int | None = None,
) -> APActivityDTO: ...
# -- Queries --------------------------------------------------------------
async def get_activity(
self, session: AsyncSession, activity_id: str,
) -> APActivityDTO | None: ...
async def get_outbox(
self, session: AsyncSession, username: str,
page: int = 1, per_page: int = 20,
) -> tuple[list[APActivityDTO], int]: ...
async def get_activity_for_source(
self, session: AsyncSession, source_type: str, source_id: int,
) -> APActivityDTO | None: ...
# -- Followers ------------------------------------------------------------
async def get_followers(
self, session: AsyncSession, username: str,
) -> list[APFollowerDTO]: ...
async def add_follower(
self, session: AsyncSession, username: str,
follower_acct: str, follower_inbox: str, follower_actor_url: str,
follower_public_key: str | None = None,
) -> APFollowerDTO: ...
async def remove_follower(
self, session: AsyncSession, username: str, follower_acct: str,
) -> bool: ...
# -- Stats ----------------------------------------------------------------
async def get_stats(self, session: AsyncSession) -> dict: ...