"""Inline federation publication — called at write time, not via async handler. Replaces the old pattern where emit_event("post.published") → async handler → publish_activity(). Now the originating service calls try_publish() directly, which creates the APActivity in the same DB transaction. AP delivery (federation.activity_created → inbox POST) stays async. """ from __future__ import annotations import logging import os from sqlalchemy.ext.asyncio import AsyncSession from shared.services.registry import services log = logging.getLogger(__name__) async def try_publish( session: AsyncSession, *, user_id: int | None, activity_type: str, object_type: str, object_data: dict, source_type: str, source_id: int, ) -> None: """Publish an AP activity if federation is available and user has a profile. Safe to call from any app — returns silently if federation isn't wired or the user has no actor profile. """ if not services.has("federation"): return if not user_id: return actor = await services.federation.get_actor_by_user_id(session, user_id) if not actor: return # Dedup: don't re-Create if already published, don't re-Delete if already deleted existing = await services.federation.get_activity_for_source( session, source_type, source_id, ) if existing: if activity_type == "Create" and existing.activity_type != "Delete": return # already published (allow re-Create after Delete/unpublish) if activity_type == "Update" and existing.activity_type == "Update": return # already updated (Ghost fires duplicate webhooks) if activity_type == "Delete" and existing.activity_type == "Delete": return # already deleted elif activity_type in ("Delete", "Update"): return # never published, nothing to delete/update # Stable object ID: same source always gets the same object id so # Mastodon treats Create/Update/Delete as the same post. domain = os.getenv("AP_DOMAIN", "rose-ash.com") object_data["id"] = ( f"https://{domain}/users/{actor.preferred_username}" f"/objects/{source_type.lower()}/{source_id}" ) try: await services.federation.publish_activity( session, actor_user_id=user_id, activity_type=activity_type, object_type=object_type, object_data=object_data, source_type=source_type, source_id=source_id, ) log.info( "Published %s/%s for %s#%d by user %d", activity_type, object_type, source_type, source_id, user_id, ) except Exception: log.exception("Failed to publish activity for %s#%d", source_type, source_id)