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:
giles
2026-02-23 19:02:30 +00:00
parent 001cbffd74
commit f2262f702b
10 changed files with 1087 additions and 35 deletions

View File

@@ -131,10 +131,30 @@ async def on_any_activity(activity: APActivity, session: AsyncSession) -> None:
log.warning("Actor not found or missing key for activity %s", activity.activity_id)
return
# Load followers
# Load followers: aggregate (app_domain IS NULL) always get everything.
# If the activity has an origin_app, also include app-specific followers.
from sqlalchemy import or_
follower_filters = [
APFollower.actor_profile_id == actor.id,
]
origin_app = activity.origin_app
if origin_app and origin_app != "federation":
# Aggregate followers (NULL) + followers of this specific app
follower_filters.append(
or_(
APFollower.app_domain.is_(None),
APFollower.app_domain == origin_app,
)
)
else:
# Federation / no origin_app: deliver to all followers
pass
followers = (
await session.execute(
select(APFollower).where(APFollower.actor_profile_id == actor.id)
select(APFollower).where(*follower_filters)
)
).scalars().all()
@@ -142,7 +162,7 @@ async def on_any_activity(activity: APActivity, session: AsyncSession) -> None:
log.debug("No followers to deliver to for %s", activity.activity_id)
return
# Deduplicate inboxes
# Deduplicate inboxes (same remote actor may follow both aggregate + app)
all_inboxes = {f.follower_inbox for f in followers if f.follower_inbox}
# Check delivery log — skip inboxes we already delivered to (idempotency)