- Stable object ID per source (Post#123 always gets the same id) instead of deriving from activity UUID - Dedup Update activities (Ghost fires duplicate webhooks) - Use setdefault for object id in delivery handler Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
"""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)
|