Fix duplicate AP posts + stable object IDs

- 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>
This commit is contained in:
giles
2026-02-22 10:14:40 +00:00
parent a626dd849d
commit 9a8b556c13
2 changed files with 14 additions and 6 deletions

View File

@@ -38,7 +38,8 @@ def _build_activity_json(activity: APActivity, actor: ActorProfile, domain: str)
obj.setdefault("type", "Tombstone") obj.setdefault("type", "Tombstone")
else: else:
# Create/Update: full object with attribution # Create/Update: full object with attribution
obj["id"] = object_id # Prefer stable id from object_data (set by try_publish), fall back to activity-derived
obj.setdefault("id", object_id)
obj.setdefault("type", activity.object_type) obj.setdefault("type", activity.object_type)
obj.setdefault("attributedTo", actor_url) obj.setdefault("attributedTo", actor_url)
obj.setdefault("published", activity.published.isoformat() if activity.published else None) obj.setdefault("published", activity.published.isoformat() if activity.published else None)

View File

@@ -8,6 +8,7 @@ which creates the APActivity in the same DB transaction. AP delivery
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -48,14 +49,20 @@ async def try_publish(
if existing: if existing:
if activity_type == "Create" and existing.activity_type != "Delete": if activity_type == "Create" and existing.activity_type != "Delete":
return # already published (allow re-Create after Delete/unpublish) 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": if activity_type == "Delete" and existing.activity_type == "Delete":
return # already deleted return # already deleted
elif activity_type == "Delete": elif activity_type in ("Delete", "Update"):
return # never published, nothing to delete return # never published, nothing to delete/update
# Delete must reference the same object id Mastodon received in Create # Stable object ID: same source always gets the same object id so
if activity_type == "Delete" and existing: # Mastodon treats Create/Update/Delete as the same post.
object_data["id"] = existing.activity_id + "/object" 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: try:
await services.federation.publish_activity( await services.federation.publish_activity(