Compare commits

...

3 Commits

Author SHA1 Message Date
giles
9a8b556c13 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>
2026-02-22 10:14:40 +00:00
giles
a626dd849d Fix AP Delete: Tombstone id must match original Create object id
Mastodon ignored Delete activities because the Tombstone id was the
post URL, not the object id from the original Create activity. Now
looks up the existing Create activity and uses its object id.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 09:25:30 +00:00
giles
d0b1edea7a Add container_nav widget rendering to day and entry nav templates
Events app day view and entry detail nav now render registered
container_nav widgets (e.g. market links) alongside existing entries/posts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 09:13:44 +00:00
4 changed files with 32 additions and 3 deletions

View File

@@ -25,6 +25,15 @@
{% endcall %}
</div>
{# Container nav widgets (market links, etc.) #}
{% if container_nav_widgets %}
{% for wdata in container_nav_widgets %}
{% with ctx=wdata.ctx %}
{% include wdata.widget.template with context %}
{% endwith %}
{% endfor %}
{% endif %}
{# Admin link #}
{% if g.rights.admin %}
{% from 'macros/admin_nav.html' import admin_nav_item %}

View File

@@ -22,6 +22,14 @@
{% endcall %}
</div>
{% if container_nav_widgets %}
{% for wdata in container_nav_widgets %}
{% with ctx=wdata.ctx %}
{% include wdata.widget.template with context %}
{% endwith %}
{% endfor %}
{% endif %}
{# Admin link #}
{% if g.rights.admin %}

View File

@@ -38,7 +38,8 @@ def _build_activity_json(activity: APActivity, actor: ActorProfile, domain: str)
obj.setdefault("type", "Tombstone")
else:
# 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("attributedTo", actor_url)
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
import logging
import os
from sqlalchemy.ext.asyncio import AsyncSession
@@ -48,10 +49,20 @@ async def try_publish(
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 == "Delete":
return # never published, nothing to delete
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(