Add per-app ActivityPub actors via shared AP blueprint
Each AP-enabled app (blog, market, events, federation) now serves its own webfinger, actor profile, inbox, outbox, and followers endpoints. Per-app actors are virtual projections of the same ActorProfile/keypair, scoped by APFollower.app_domain and APActivity.origin_app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,7 @@ def _follower_to_dto(f: APFollower) -> APFollowerDTO:
|
||||
follower_inbox=f.follower_inbox,
|
||||
follower_actor_url=f.follower_actor_url,
|
||||
created_at=f.created_at,
|
||||
app_domain=f.app_domain,
|
||||
)
|
||||
|
||||
|
||||
@@ -252,6 +253,7 @@ class SqlFederationService:
|
||||
async def get_outbox(
|
||||
self, session: AsyncSession, username: str,
|
||||
page: int = 1, per_page: int = 20,
|
||||
origin_app: str | None = None,
|
||||
) -> tuple[list[APActivityDTO], int]:
|
||||
actor = (
|
||||
await session.execute(
|
||||
@@ -261,22 +263,23 @@ class SqlFederationService:
|
||||
if actor is None:
|
||||
return [], 0
|
||||
|
||||
filters = [
|
||||
APActivity.actor_profile_id == actor.id,
|
||||
APActivity.is_local == True, # noqa: E712
|
||||
]
|
||||
if origin_app is not None:
|
||||
filters.append(APActivity.origin_app == origin_app)
|
||||
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count(APActivity.id)).where(
|
||||
APActivity.actor_profile_id == actor.id,
|
||||
APActivity.is_local == True, # noqa: E712
|
||||
)
|
||||
select(func.count(APActivity.id)).where(*filters)
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
offset = (page - 1) * per_page
|
||||
result = await session.execute(
|
||||
select(APActivity)
|
||||
.where(
|
||||
APActivity.actor_profile_id == actor.id,
|
||||
APActivity.is_local == True, # noqa: E712
|
||||
)
|
||||
.where(*filters)
|
||||
.order_by(APActivity.published.desc())
|
||||
.limit(per_page)
|
||||
.offset(offset)
|
||||
@@ -315,6 +318,7 @@ class SqlFederationService:
|
||||
|
||||
async def get_followers(
|
||||
self, session: AsyncSession, username: str,
|
||||
app_domain: str | None = None,
|
||||
) -> list[APFollowerDTO]:
|
||||
actor = (
|
||||
await session.execute(
|
||||
@@ -324,15 +328,18 @@ class SqlFederationService:
|
||||
if actor is None:
|
||||
return []
|
||||
|
||||
result = await session.execute(
|
||||
select(APFollower).where(APFollower.actor_profile_id == actor.id)
|
||||
)
|
||||
q = select(APFollower).where(APFollower.actor_profile_id == actor.id)
|
||||
if app_domain is not None:
|
||||
q = q.where(APFollower.app_domain == app_domain)
|
||||
|
||||
result = await session.execute(q)
|
||||
return [_follower_to_dto(f) for f in result.scalars().all()]
|
||||
|
||||
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,
|
||||
app_domain: str | None = None,
|
||||
) -> APFollowerDTO:
|
||||
actor = (
|
||||
await session.execute(
|
||||
@@ -342,15 +349,17 @@ class SqlFederationService:
|
||||
if actor is None:
|
||||
raise ValueError(f"Actor not found: {username}")
|
||||
|
||||
# Upsert: update if already following, insert if new
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(APFollower).where(
|
||||
APFollower.actor_profile_id == actor.id,
|
||||
APFollower.follower_acct == follower_acct,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
# Upsert: update if already following this (actor, acct, app_domain)
|
||||
q = select(APFollower).where(
|
||||
APFollower.actor_profile_id == actor.id,
|
||||
APFollower.follower_acct == follower_acct,
|
||||
)
|
||||
if app_domain is not None:
|
||||
q = q.where(APFollower.app_domain == app_domain)
|
||||
else:
|
||||
q = q.where(APFollower.app_domain.is_(None))
|
||||
|
||||
existing = (await session.execute(q)).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
existing.follower_inbox = follower_inbox
|
||||
@@ -365,6 +374,7 @@ class SqlFederationService:
|
||||
follower_inbox=follower_inbox,
|
||||
follower_actor_url=follower_actor_url,
|
||||
follower_public_key=follower_public_key,
|
||||
app_domain=app_domain,
|
||||
)
|
||||
session.add(follower)
|
||||
await session.flush()
|
||||
@@ -372,6 +382,7 @@ class SqlFederationService:
|
||||
|
||||
async def remove_follower(
|
||||
self, session: AsyncSession, username: str, follower_acct: str,
|
||||
app_domain: str | None = None,
|
||||
) -> bool:
|
||||
actor = (
|
||||
await session.execute(
|
||||
@@ -381,12 +392,16 @@ class SqlFederationService:
|
||||
if actor is None:
|
||||
return False
|
||||
|
||||
result = await session.execute(
|
||||
delete(APFollower).where(
|
||||
APFollower.actor_profile_id == actor.id,
|
||||
APFollower.follower_acct == follower_acct,
|
||||
)
|
||||
)
|
||||
filters = [
|
||||
APFollower.actor_profile_id == actor.id,
|
||||
APFollower.follower_acct == follower_acct,
|
||||
]
|
||||
if app_domain is not None:
|
||||
filters.append(APFollower.app_domain == app_domain)
|
||||
else:
|
||||
filters.append(APFollower.app_domain.is_(None))
|
||||
|
||||
result = await session.execute(delete(APFollower).where(*filters))
|
||||
return result.rowcount > 0
|
||||
|
||||
async def get_followers_paginated(
|
||||
|
||||
Reference in New Issue
Block a user