Events integration with ActivityPub #56

Open
opened 2025-06-29 17:14:05 +00:00 by iannuzzelli · 0 comments
iannuzzelli commented 2025-06-29 17:14:05 +00:00 (Migrated from gitlab.com)

Native ActivityPub federation for the PeaceLink calendar

Goal

Make the PeaceLink calendar a fediverse actor (e.g. @calendario@peacelink.it) that people on Mastodon, Gancio, Mobilizon and other ActivityPub servers can follow. Every approved event is published as an ActivityPub Event; edits, cancellations and removals are propagated to followers.

Events stay hosted on peacelink.it: no second platform, no sync with an external service.

Current state

  • Public JSON endpoint: https://www.peacelink.it/js/events.php (approved events only, CET/CEST, Italian labels, 5 min cache)
  • Public MCP server: mcp-server.peacelink.it (get_upcoming_events)
  • Event page per event (url field), iCal/webcal/Google export links
  • Fields available: title, HTML description, start/end, all_day, is_online, type, link, contact_name, email, address, place, place_details, province/region, latitude/longitude, images (thumb/image/large)

Scope

In scope (v1)

  1. WebFinger discovery
  2. Actor document with RSA keypair
  3. Outbox with Create{Event} activities
  4. Inbox handling Follow, Undo{Follow}, Delete (of remote actors)
  5. Follower storage
  6. Signed delivery (HTTP Signatures) via an async queue with retries
  7. Outgoing Create, Update, Delete triggered by the event lifecycle in PhPeace
  8. Content negotiation on event pages (return the Event object for Accept: application/activity+json)
  9. Signature verification on incoming inbox requests

Out of scope (v1, possible later)

  • Replies/comments, likes, boosts handling (accept and ignore with 202)
  • RSVP / Join activities
  • Per-region actors (see open questions)
  • Importing remote events into the PeaceLink calendar
  • Following other accounts

1. WebFinger

GET /.well-known/webfinger?resource=acct:calendario@peacelink.it

{
  "subject": "acct:calendario@peacelink.it",
  "aliases": ["https://www.peacelink.it/calendario/ap/actor"],
  "links": [
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://www.peacelink.it/calendario/ap/actor"
    },
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://www.peacelink.it/calendario/events.php"
    }
  ]
}
  • Content-Type: application/jrd+json
  • Return 404 for unknown resources
  • Must be served on peacelink.it itself (the domain in the handle). If peacelink.it (without www) redirects to www, check that the redirect preserves path and query, or serve WebFinger on both hosts.
  • Access-Control-Allow-Origin: * is useful for web clients.

Optional but recommended: NodeInfo (/.well-known/nodeinfo → /nodeinfo/2.1) so servers can identify the software (PhPeace).

2. Actor

GET https://www.peacelink.it/calendario/ap/actor (Content-Type application/activity+json)

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1"
  ],
  "id": "https://www.peacelink.it/calendario/ap/actor",
  "type": "Service",
  "preferredUsername": "calendario",
  "name": "Calendario PeaceLink",
  "summary": "<p>Eventi per la pace, il disarmo e la cittadinanza attiva in Italia.</p>",
  "url": "https://www.peacelink.it/calendario/events.php",
  "inbox": "https://www.peacelink.it/calendario/ap/inbox",
  "outbox": "https://www.peacelink.it/calendario/ap/outbox",
  "followers": "https://www.peacelink.it/calendario/ap/followers",
  "endpoints": { "sharedInbox": "https://www.peacelink.it/ap/inbox" },
  "icon": { "type": "Image", "mediaType": "image/png", "url": "https://www.peacelink.it/..." },
  "manuallyApprovesFollowers": false,
  "discoverable": true,
  "published": "2026-10-01T00:00:00Z",
  "publicKey": {
    "id": "https://www.peacelink.it/calendario/ap/actor#main-key",
    "owner": "https://www.peacelink.it/calendario/ap/actor",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
  }
}
  • Generate an RSA 2048 keypair once; store the private key outside the web root / in a secret (k8s Secret if PhPeace runs on k3s). Never rotate it casually: remote servers cache it.
  • type: Service marks it as a bot account on Mastodon.
  • Use https://www.peacelink.it/... consistently for all IDs. IDs must never change once published.

3. Event object

Each approved event gets a stable ActivityPub ID. Suggested: reuse the event page URL as id, so content negotiation (section 8) resolves it.

Mapping from the PhPeace event to Event:

AP property Source Notes
id event page URL stable, never changes
url event page URL
type "Event"
name title plain text
content description sanitized HTML (see below)
summary first ~300 chars of description as text optional, used by some clients
startTime start ISO 8601 with offset (2026-10-12T18:00:00+02:00); compute CET/CEST via Europe/Rome
endTime end same
published approval date
updated last modification date needed for Update
attributedTo actor ID
to ["https://www.w3.org/ns/activitystreams#Public"]
cc [followers collection]
location Place (see below) omit or use virtual location if is_online = 1
attachment Image from image/large include mediaType, name (alt text = title if nothing better)
tag Hashtags e.g. #pace, #disarmo, region hashtag, event type
category type Mobilizon extension, optional
timezone "Europe/Rome" Mobilizon/Gancio extension, optional
ical:status CONFIRMED / CANCELLED for cancellations
isOnline is_online optional extension

Location:

"location": {
  "type": "Place",
  "name": "Sala Consiliare, Taranto",
  "address": {
    "type": "PostalAddress",
    "streetAddress": "Via ...",
    "addressLocality": "Taranto",
    "addressRegion": "Puglia",
    "addressCountry": "IT"
  },
  "latitude": 40.47,
  "longitude": 17.24
}

For online events: omit location or set a Place/VirtualLocation with the stream link; keep link also in content.

All-day events: Mobilizon/Gancio do not have a uniform all-day flag; use startTime at 00:00 and endTime at 23:59 local, and say it in content.

Read FEP-8a8e ("A common approach to using the Event object type") before implementing; it documents what Mobilizon, Gancio, Friendica etc. actually emit and consume. Test against at least Mastodon, Gancio and Mobilizon.

Content sanitization

  • Strip scripts, styles, iframes, inline event handlers
  • Keep p, br, a, strong, em, ul, ol, li, blockquote
  • Make all links absolute
  • Append a footer line in content with date/time, place and the event URL in human-readable Italian, because Mastodon does not render Event fields: it converts the Event into a status showing essentially name + url. A meaningful content/summary is what Mastodon users will actually read.

Privacy

  • Do not federate email (organizer email). Once delivered it is copied to many servers and cannot be reliably retracted; it also feeds spam harvesters.
  • contact_name is fine to include in content.
  • Consider whether place_details may contain personal info (phone numbers etc.).

4. Outbox

GET /calendario/ap/outbox → OrderedCollection with totalItems and paginated first/next (OrderedCollectionPage), newest first, containing Create activities.

Activity example:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://www.peacelink.it/calendario/ap/activity/create/12345",
  "type": "Create",
  "actor": "https://www.peacelink.it/calendario/ap/actor",
  "published": "2026-10-05T14:00:00Z",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://www.peacelink.it/calendario/ap/followers"],
  "object": { "...": "Event object" }
}

Note: Mastodon does not backfill from the outbox when someone follows. New followers only see events published after they follow. Acceptable for v1.

5. Inbox

Endpoints:

  • POST /calendario/ap/inbox (actor inbox)
  • POST /ap/inbox (shared inbox, same handler)

For every request:

  1. Verify HTTP Signature (section 7). Reject with 401 if invalid.
  2. Verify Digest header matches the body.
  3. Parse JSON-LD as plain JSON (no full JSON-LD processing needed).
  4. Process asynchronously if possible; respond 202 Accepted quickly.

Handled activities:

Incoming Action
Follow (object = our actor) store follower (actor ID, inbox, sharedInbox), reply with signed Accept{Follow} to follower's inbox
Undo{Follow} remove follower
Delete (actor deleted itself) remove follower if present; these arrive unsigned-verifiable often (key gone), accept silently
anything else (Like, Announce, Create{Note} replies...) 202, ignore (optionally log)

The Accept must embed or reference the original Follow activity (embed the full object; Mastodon expects it).

Followers collection GET /calendario/ap/followers: return OrderedCollection with totalItems only (don't expose the list).

6. Event lifecycle hooks in PhPeace

PhPeace event Outgoing activity
Event approved (first time) Create{Event}
Approved event edited Update{Event} with new updated
Event cancelled (if PhPeace has the state) Update{Event} with ical:status: CANCELLED and a "ANNULLATO" prefix in name/content
Event deleted / unapproved Delete{Tombstone} with the event ID; event URL returns 410 for AP requests
Event ends nothing (past events stay)

Debounce updates: editors often save several times in a row. Wait e.g. 10 minutes after the last edit before sending Update, and skip it if the federated representation hash is unchanged.

Store per event: ap_published_at, ap_last_hash, ap_deleted.

7. HTTP Signatures

Outgoing (all POSTs to remote inboxes, and GETs of remote actors when verifying):

  • Headers: Host, Date (RFC 7231 format), Digest: SHA-256=<base64>, Content-Type: application/activity+json
  • Signature: keyId="https://www.peacelink.it/calendario/ap/actor#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="<base64>"
  • Signing string: lines (request-target): post /inbox, host: ..., date: ..., digest: ... joined by \n
  • User-Agent identifying PhPeace and the site URL

Incoming verification:

  • Parse Signature header, fetch keyId (the remote actor, cached), verify with the public key
  • Check Date within ±12 hours
  • Cache remote actor documents/keys (e.g. 24h); on verification failure refetch once (key rotation)

Some servers (secure/authorized fetch mode) require signed GETs too: sign GET requests for remote actors with headers (request-target) host date.

Use an existing PHP library if one fits PhPeace's dependencies, otherwise this is ~150 lines with openssl_sign/openssl_verify.

8. Content negotiation on event pages

Event page URL with Accept: application/activity+json or application/ld+json; profile="https://www.w3.org/ns/activitystreams" → return the Event JSON instead of HTML. Also add to the HTML head:

<link rel="alternate" type="application/activity+json" href="(event URL)">

This lets users paste an event URL into Mastodon/Mobilizon search and get the event. Check the caching layer (Varnish/CDN) varies on Accept, or serve the JSON from a separate path and return a redirect/Link header.

9. Delivery queue

  • DB table ap_delivery: id, activity_id, target_inbox, payload, attempts, next_attempt_at, last_error, status
  • On each outgoing activity: resolve followers, deduplicate by sharedInbox (one delivery per server, not per follower)
  • Worker (cron every minute or a k8s CronJob / long-running worker): POST signed payload
  • Success: 2xx. Retry on 5xx/timeouts/429 with exponential backoff (1m, 5m, 30m, 2h, 12h, 24h...), give up after ~3 days
  • 410 Gone from an inbox: remove followers on that inbox
  • Timeouts: 10s connect, 20s total
  • Concurrency limit per host

10. Data model (new tables)

  • ap_keys (or config/secret): private/public key
  • ap_followers: actor_id (unique), inbox, shared_inbox, followed_at
  • ap_remote_actors: actor_id, public_key_pem, fetched_at (cache)
  • ap_delivery: as above
  • events table: add ap_published_at, ap_last_hash, ap_deleted, plus a reliable updated_at if missing

11. Small improvements to /js/events.php (useful anyway)

  • Add a stable id and updated field per event
  • Add timezone offset to start/end (or an explicit timezone field)
  • Reconsider exposing email publicly

12. Testing

  • Local: test WebFinger and actor with curl -H 'Accept: application/activity+json'
  • Validate JSON with a fediverse debugging tool (e.g. browser.pub or similar AP inspectors)
  • Follow from a test Mastodon account; check Accept arrives and the follow is confirmed
  • Publish a test event (on a staging instance or with a test region), verify it appears on:
    • Mastodon (as a post with title + link + content)
    • Gancio instance (as an event, with date and place)
    • Mobilizon instance (as an event, with map)
  • Edit → Update reaches remote servers; delete → removed remotely
  • Unfollow → follower removed
  • Signature failure path: send an unsigned POST, expect 401
  • Delivery failure: point a follower at a dead inbox, check backoff and give-up

Acceptance criteria

  • @calendario@peacelink.it is searchable and followable from Mastodon
  • Newly approved events appear in followers' timelines within a few minutes
  • Events appear as proper events (date, place, map) on Gancio and Mobilizon
  • Edits and deletions propagate
  • Organizer email is never federated
  • Incoming requests without valid signatures are rejected
  • Delivery failures are retried and eventually dropped without blocking the queue
  • Event URLs resolve as AP objects via content negotiation

Open questions

  • Handle name: calendario, eventi, or calendar?
  • One actor for everything, or also per-region actors (@puglia@peacelink.it)? The region filter makes it cheap; could be v2.
  • Hashtags: which set? (fixed #pace #disarmo + region + event type?)
  • Federate only future events approved after go-live, or also currently upcoming events at launch? (Recommended: publish upcoming events once at launch into the outbox, but deliver only new ones.)
  • Moderation: what if remote users reply? v1 ignores replies; maybe forward mentions to an editor mailbox later.
  • Does PhPeace have a cancelled state, or only delete?
  • Hosting of the worker: PhPeace cron vs. k3s CronJob.
# Native ActivityPub federation for the PeaceLink calendar ## Goal Make the PeaceLink calendar a fediverse actor (e.g. `@calendario@peacelink.it`) that people on Mastodon, Gancio, Mobilizon and other ActivityPub servers can follow. Every approved event is published as an ActivityPub `Event`; edits, cancellations and removals are propagated to followers. Events stay hosted on `peacelink.it`: no second platform, no sync with an external service. ## Current state - Public JSON endpoint: `https://www.peacelink.it/js/events.php` (approved events only, CET/CEST, Italian labels, 5 min cache) - Public MCP server: `mcp-server.peacelink.it` (`get_upcoming_events`) - Event page per event (`url` field), iCal/webcal/Google export links - Fields available: title, HTML description, start/end, all_day, is_online, type, link, contact_name, email, address, place, place_details, province/region, latitude/longitude, images (thumb/image/large) ## Scope ### In scope (v1) 1. WebFinger discovery 2. Actor document with RSA keypair 3. Outbox with `Create{Event}` activities 4. Inbox handling `Follow`, `Undo{Follow}`, `Delete` (of remote actors) 5. Follower storage 6. Signed delivery (HTTP Signatures) via an async queue with retries 7. Outgoing `Create`, `Update`, `Delete` triggered by the event lifecycle in PhPeace 8. Content negotiation on event pages (return the `Event` object for `Accept: application/activity+json`) 9. Signature verification on incoming inbox requests ### Out of scope (v1, possible later) - Replies/comments, likes, boosts handling (accept and ignore with 202) - RSVP / `Join` activities - Per-region actors (see open questions) - Importing remote events into the PeaceLink calendar - Following other accounts ## 1. WebFinger `GET /.well-known/webfinger?resource=acct:calendario@peacelink.it` ```json { "subject": "acct:calendario@peacelink.it", "aliases": ["https://www.peacelink.it/calendario/ap/actor"], "links": [ { "rel": "self", "type": "application/activity+json", "href": "https://www.peacelink.it/calendario/ap/actor" }, { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://www.peacelink.it/calendario/events.php" } ] } ``` - Content-Type: `application/jrd+json` - Return 404 for unknown resources - Must be served on `peacelink.it` itself (the domain in the handle). If `peacelink.it` (without `www`) redirects to `www`, check that the redirect preserves path and query, or serve WebFinger on both hosts. - `Access-Control-Allow-Origin: *` is useful for web clients. Optional but recommended: NodeInfo (`/.well-known/nodeinfo` → `/nodeinfo/2.1`) so servers can identify the software (PhPeace). ## 2. Actor `GET https://www.peacelink.it/calendario/ap/actor` (Content-Type `application/activity+json`) ```json { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" ], "id": "https://www.peacelink.it/calendario/ap/actor", "type": "Service", "preferredUsername": "calendario", "name": "Calendario PeaceLink", "summary": "<p>Eventi per la pace, il disarmo e la cittadinanza attiva in Italia.</p>", "url": "https://www.peacelink.it/calendario/events.php", "inbox": "https://www.peacelink.it/calendario/ap/inbox", "outbox": "https://www.peacelink.it/calendario/ap/outbox", "followers": "https://www.peacelink.it/calendario/ap/followers", "endpoints": { "sharedInbox": "https://www.peacelink.it/ap/inbox" }, "icon": { "type": "Image", "mediaType": "image/png", "url": "https://www.peacelink.it/..." }, "manuallyApprovesFollowers": false, "discoverable": true, "published": "2026-10-01T00:00:00Z", "publicKey": { "id": "https://www.peacelink.it/calendario/ap/actor#main-key", "owner": "https://www.peacelink.it/calendario/ap/actor", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" } } ``` - Generate an RSA 2048 keypair once; store the private key outside the web root / in a secret (k8s Secret if PhPeace runs on k3s). Never rotate it casually: remote servers cache it. - `type: Service` marks it as a bot account on Mastodon. - Use `https://www.peacelink.it/...` consistently for all IDs. IDs must never change once published. ## 3. Event object Each approved event gets a stable ActivityPub ID. Suggested: reuse the event page URL as `id`, so content negotiation (section 8) resolves it. Mapping from the PhPeace event to `Event`: | AP property | Source | Notes | |---|---|---| | `id` | event page URL | stable, never changes | | `url` | event page URL | | | `type` | `"Event"` | | | `name` | `title` | plain text | | `content` | `description` | sanitized HTML (see below) | | `summary` | first ~300 chars of description as text | optional, used by some clients | | `startTime` | `start` | ISO 8601 **with offset** (`2026-10-12T18:00:00+02:00`); compute CET/CEST via `Europe/Rome` | | `endTime` | `end` | same | | `published` | approval date | | | `updated` | last modification date | needed for `Update` | | `attributedTo` | actor ID | | | `to` | `["https://www.w3.org/ns/activitystreams#Public"]` | | | `cc` | `[followers collection]` | | | `location` | `Place` (see below) | omit or use virtual location if `is_online = 1` | | `attachment` | `Image` from `image`/`large` | include `mediaType`, `name` (alt text = title if nothing better) | | `tag` | `Hashtag`s | e.g. `#pace`, `#disarmo`, region hashtag, event `type` | | `category` | `type` | Mobilizon extension, optional | | `timezone` | `"Europe/Rome"` | Mobilizon/Gancio extension, optional | | `ical:status` | `CONFIRMED` / `CANCELLED` | for cancellations | | `isOnline` | `is_online` | optional extension | Location: ```json "location": { "type": "Place", "name": "Sala Consiliare, Taranto", "address": { "type": "PostalAddress", "streetAddress": "Via ...", "addressLocality": "Taranto", "addressRegion": "Puglia", "addressCountry": "IT" }, "latitude": 40.47, "longitude": 17.24 } ``` For online events: omit `location` or set a `Place`/`VirtualLocation` with the stream `link`; keep `link` also in `content`. All-day events: Mobilizon/Gancio do not have a uniform all-day flag; use `startTime` at 00:00 and `endTime` at 23:59 local, and say it in `content`. Read FEP-8a8e ("A common approach to using the Event object type") before implementing; it documents what Mobilizon, Gancio, Friendica etc. actually emit and consume. Test against at least Mastodon, Gancio and Mobilizon. ### Content sanitization - Strip scripts, styles, iframes, inline event handlers - Keep `p, br, a, strong, em, ul, ol, li, blockquote` - Make all links absolute - Append a footer line in `content` with date/time, place and the event URL in human-readable Italian, because **Mastodon does not render Event fields**: it converts the `Event` into a status showing essentially `name` + `url`. A meaningful `content`/`summary` is what Mastodon users will actually read. ### Privacy - **Do not federate `email`** (organizer email). Once delivered it is copied to many servers and cannot be reliably retracted; it also feeds spam harvesters. - `contact_name` is fine to include in `content`. - Consider whether `place_details` may contain personal info (phone numbers etc.). ## 4. Outbox `GET /calendario/ap/outbox` → `OrderedCollection` with `totalItems` and paginated `first`/`next` (`OrderedCollectionPage`), newest first, containing `Create` activities. Activity example: ```json { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://www.peacelink.it/calendario/ap/activity/create/12345", "type": "Create", "actor": "https://www.peacelink.it/calendario/ap/actor", "published": "2026-10-05T14:00:00Z", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://www.peacelink.it/calendario/ap/followers"], "object": { "...": "Event object" } } ``` Note: Mastodon does not backfill from the outbox when someone follows. New followers only see events published after they follow. Acceptable for v1. ## 5. Inbox Endpoints: - `POST /calendario/ap/inbox` (actor inbox) - `POST /ap/inbox` (shared inbox, same handler) For every request: 1. Verify HTTP Signature (section 7). Reject with 401 if invalid. 2. Verify `Digest` header matches the body. 3. Parse JSON-LD as plain JSON (no full JSON-LD processing needed). 4. Process asynchronously if possible; respond `202 Accepted` quickly. Handled activities: | Incoming | Action | |---|---| | `Follow` (object = our actor) | store follower (actor ID, inbox, sharedInbox), reply with signed `Accept{Follow}` to follower's inbox | | `Undo{Follow}` | remove follower | | `Delete` (actor deleted itself) | remove follower if present; these arrive unsigned-verifiable often (key gone), accept silently | | anything else (`Like`, `Announce`, `Create{Note}` replies...) | 202, ignore (optionally log) | The `Accept` must embed or reference the original `Follow` activity (embed the full object; Mastodon expects it). Followers collection `GET /calendario/ap/followers`: return `OrderedCollection` with `totalItems` only (don't expose the list). ## 6. Event lifecycle hooks in PhPeace | PhPeace event | Outgoing activity | |---|---| | Event approved (first time) | `Create{Event}` | | Approved event edited | `Update{Event}` with new `updated` | | Event cancelled (if PhPeace has the state) | `Update{Event}` with `ical:status: CANCELLED` and a "ANNULLATO" prefix in `name`/`content` | | Event deleted / unapproved | `Delete{Tombstone}` with the event ID; event URL returns 410 for AP requests | | Event ends | nothing (past events stay) | Debounce updates: editors often save several times in a row. Wait e.g. 10 minutes after the last edit before sending `Update`, and skip it if the federated representation hash is unchanged. Store per event: `ap_published_at`, `ap_last_hash`, `ap_deleted`. ## 7. HTTP Signatures Outgoing (all POSTs to remote inboxes, and GETs of remote actors when verifying): - Headers: `Host`, `Date` (RFC 7231 format), `Digest: SHA-256=<base64>`, `Content-Type: application/activity+json` - `Signature: keyId="https://www.peacelink.it/calendario/ap/actor#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="<base64>"` - Signing string: lines `(request-target): post /inbox`, `host: ...`, `date: ...`, `digest: ...` joined by `\n` - `User-Agent` identifying PhPeace and the site URL Incoming verification: - Parse `Signature` header, fetch `keyId` (the remote actor, cached), verify with the public key - Check `Date` within ±12 hours - Cache remote actor documents/keys (e.g. 24h); on verification failure refetch once (key rotation) Some servers (secure/authorized fetch mode) require signed GETs too: sign GET requests for remote actors with headers `(request-target) host date`. Use an existing PHP library if one fits PhPeace's dependencies, otherwise this is ~150 lines with `openssl_sign`/`openssl_verify`. ## 8. Content negotiation on event pages Event page URL with `Accept: application/activity+json` or `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` → return the `Event` JSON instead of HTML. Also add to the HTML head: ```html <link rel="alternate" type="application/activity+json" href="(event URL)"> ``` This lets users paste an event URL into Mastodon/Mobilizon search and get the event. Check the caching layer (Varnish/CDN) varies on `Accept`, or serve the JSON from a separate path and return a redirect/`Link` header. ## 9. Delivery queue - DB table `ap_delivery`: id, activity_id, target_inbox, payload, attempts, next_attempt_at, last_error, status - On each outgoing activity: resolve followers, **deduplicate by sharedInbox** (one delivery per server, not per follower) - Worker (cron every minute or a k8s CronJob / long-running worker): POST signed payload - Success: 2xx. Retry on 5xx/timeouts/429 with exponential backoff (1m, 5m, 30m, 2h, 12h, 24h...), give up after ~3 days - 410 Gone from an inbox: remove followers on that inbox - Timeouts: 10s connect, 20s total - Concurrency limit per host ## 10. Data model (new tables) - `ap_keys` (or config/secret): private/public key - `ap_followers`: actor_id (unique), inbox, shared_inbox, followed_at - `ap_remote_actors`: actor_id, public_key_pem, fetched_at (cache) - `ap_delivery`: as above - events table: add `ap_published_at`, `ap_last_hash`, `ap_deleted`, plus a reliable `updated_at` if missing ## 11. Small improvements to `/js/events.php` (useful anyway) - Add a stable `id` and `updated` field per event - Add timezone offset to `start`/`end` (or an explicit `timezone` field) - Reconsider exposing `email` publicly ## 12. Testing - Local: test WebFinger and actor with `curl -H 'Accept: application/activity+json'` - Validate JSON with a fediverse debugging tool (e.g. browser.pub or similar AP inspectors) - Follow from a test Mastodon account; check `Accept` arrives and the follow is confirmed - Publish a test event (on a staging instance or with a test region), verify it appears on: - Mastodon (as a post with title + link + content) - Gancio instance (as an event, with date and place) - Mobilizon instance (as an event, with map) - Edit → `Update` reaches remote servers; delete → removed remotely - Unfollow → follower removed - Signature failure path: send an unsigned POST, expect 401 - Delivery failure: point a follower at a dead inbox, check backoff and give-up ## Acceptance criteria - [ ] `@calendario@peacelink.it` is searchable and followable from Mastodon - [ ] Newly approved events appear in followers' timelines within a few minutes - [ ] Events appear as proper events (date, place, map) on Gancio and Mobilizon - [ ] Edits and deletions propagate - [ ] Organizer email is never federated - [ ] Incoming requests without valid signatures are rejected - [ ] Delivery failures are retried and eventually dropped without blocking the queue - [ ] Event URLs resolve as AP objects via content negotiation ## Open questions - Handle name: `calendario`, `eventi`, or `calendar`? - One actor for everything, or also per-region actors (`@puglia@peacelink.it`)? The `region` filter makes it cheap; could be v2. - Hashtags: which set? (fixed `#pace #disarmo` + region + event type?) - Federate only future events approved after go-live, or also currently upcoming events at launch? (Recommended: publish upcoming events once at launch into the outbox, but deliver only new ones.) - Moderation: what if remote users reply? v1 ignores replies; maybe forward mentions to an editor mailbox later. - Does PhPeace have a cancelled state, or only delete? - Hosting of the worker: PhPeace cron vs. k3s CronJob.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
peacelink/phpeace#56
No description provided.