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:
@@ -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
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
Reference in New Issue
Block a user