Fix AP re-publish: use versioned object IDs after Delete
After Delete + re-Create, Mastodon tombstones the old object ID and ignores new Creates with the same ID. Now appends /v2, /v3 etc. so remote servers treat re-publishes as fresh posts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,11 @@ class FederationService(Protocol):
|
|||||||
self, session: AsyncSession, source_type: str, source_id: int,
|
self, session: AsyncSession, source_type: str, source_id: int,
|
||||||
) -> APActivityDTO | None: ...
|
) -> APActivityDTO | None: ...
|
||||||
|
|
||||||
|
async def count_activities_for_source(
|
||||||
|
self, session: AsyncSession, source_type: str, source_id: int,
|
||||||
|
*, activity_type: str,
|
||||||
|
) -> int: ...
|
||||||
|
|
||||||
# -- Followers ------------------------------------------------------------
|
# -- Followers ------------------------------------------------------------
|
||||||
async def get_followers(
|
async def get_followers(
|
||||||
self, session: AsyncSession, username: str,
|
self, session: AsyncSession, username: str,
|
||||||
|
|||||||
@@ -288,6 +288,20 @@ class SqlFederationService:
|
|||||||
).scalars().first()
|
).scalars().first()
|
||||||
return _activity_to_dto(a) if a else None
|
return _activity_to_dto(a) if a else None
|
||||||
|
|
||||||
|
async def count_activities_for_source(
|
||||||
|
self, session: AsyncSession, source_type: str, source_id: int,
|
||||||
|
*, activity_type: str,
|
||||||
|
) -> int:
|
||||||
|
from sqlalchemy import func
|
||||||
|
result = await session.execute(
|
||||||
|
select(func.count()).select_from(APActivity).where(
|
||||||
|
APActivity.source_type == source_type,
|
||||||
|
APActivity.source_id == source_id,
|
||||||
|
APActivity.activity_type == activity_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one()
|
||||||
|
|
||||||
# -- Followers ------------------------------------------------------------
|
# -- Followers ------------------------------------------------------------
|
||||||
|
|
||||||
async def get_followers(
|
async def get_followers(
|
||||||
|
|||||||
@@ -56,13 +56,25 @@ async def try_publish(
|
|||||||
elif activity_type in ("Delete", "Update"):
|
elif activity_type in ("Delete", "Update"):
|
||||||
return # never published, nothing to delete/update
|
return # never published, nothing to delete/update
|
||||||
|
|
||||||
# Stable object ID: same source always gets the same object id so
|
# Stable object ID within a publish cycle. After Delete + re-Create
|
||||||
# Mastodon treats Create/Update/Delete as the same post.
|
# we append a version suffix so remote servers (Mastodon) treat it as
|
||||||
|
# a brand-new post rather than ignoring the tombstoned ID.
|
||||||
domain = os.getenv("AP_DOMAIN", "rose-ash.com")
|
domain = os.getenv("AP_DOMAIN", "rose-ash.com")
|
||||||
object_data["id"] = (
|
base_object_id = (
|
||||||
f"https://{domain}/users/{actor.preferred_username}"
|
f"https://{domain}/users/{actor.preferred_username}"
|
||||||
f"/objects/{source_type.lower()}/{source_id}"
|
f"/objects/{source_type.lower()}/{source_id}"
|
||||||
)
|
)
|
||||||
|
if activity_type == "Create" and existing and existing.activity_type == "Delete":
|
||||||
|
# Count prior Creates to derive a version number
|
||||||
|
create_count = await services.federation.count_activities_for_source(
|
||||||
|
session, source_type, source_id, activity_type="Create",
|
||||||
|
)
|
||||||
|
object_data["id"] = f"{base_object_id}/v{create_count + 1}"
|
||||||
|
elif activity_type in ("Update", "Delete") and existing and existing.object_data:
|
||||||
|
# Use the same object ID as the most recent activity
|
||||||
|
object_data["id"] = existing.object_data.get("id", base_object_id)
|
||||||
|
else:
|
||||||
|
object_data["id"] = base_object_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await services.federation.publish_activity(
|
await services.federation.publish_activity(
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ class StubFederationService:
|
|||||||
async def get_activity_for_source(self, session, source_type, source_id):
|
async def get_activity_for_source(self, session, source_type, source_id):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def count_activities_for_source(self, session, source_type, source_id, *, activity_type):
|
||||||
|
return 0
|
||||||
|
|
||||||
async def get_followers(self, session, username):
|
async def get_followers(self, session, username):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user