Skip to content

Architecture

Role in the call-baxter world

This package is a call-baxter runtime plugin. It is not a PHP plugin installed inside WordPress.

The intended flow is:

Telegram webhook/poller -> call-baxter telegram plugin -> telegram.* events
                                     -> call-baxter rules
                                     -> wordpress.mirror_telegram_message
                                     -> wordpress.flush_due_media_groups
                                     -> wordpress.cleanup_orphan_media
                                     -> WordPress REST API

Main actions

wordpress.mirror_telegram_message - accepts either a full Telegram update object or a single Telegram message object - parses Telegram text/caption and supported media - applies hashtag filtering - derives title + slug + body HTML - can render basic Telegram entities as HTML for the body (bold, italic, underline, strikethrough, code, pre, url, text_link) - uploads supported media to WordPress - creates or updates a WordPress post - when media_group_id is present and staging is configured, defers publication instead of posting immediately - can route post status and categories from hashtags - can sync WordPress tags from hashtags

wordpress.flush_due_media_groups - loads staged media-group fragments from the plugin state store - assembles all parts for a given chat_id + media_group_id - applies filters once to the combined publication text - creates one WordPress post for the whole group - reuses the same post on later flushes when a media-group key is already mapped - reconciles sufficiently old media from interrupted create attempts before flushing

wordpress.cleanup_orphan_media - inspects the durable publication-media ledger - looks up the deterministic post slug before deleting anything - marks tracked media attached when the post exists - force-deletes only media proven to belong to an interrupted create with no matching post - excludes attempts protected by a live publication lease, claims each candidate before remote work, and renews that claim during cleanup - supports age thresholds, bounded batches, lease duration controls, and dry-run reporting

Routing and taxonomy policy

The plugin keeps publication policy local to the WordPress side: - status_by_hashtag picks a post status from matching hashtags - category_ids_by_hashtag attaches configured category ids from matching hashtags - sync_tags_from_hashtags: true resolves hashtags into WordPress tags through the REST API

Current tag sync behavior: - hashtags are normalized into slugs - existing tags are looked up by slug - missing tags are created - hashtags already consumed by status_by_hashtag are excluded from tag sync

Responsibility split

Telegram plugin responsibility

The Telegram plugin should own transport-native facts and reusable grouping semantics: - identifying update/message kinds - normalizing raw Telegram payloads - recognizing media_group_id - eventually, publishing higher-level aggregate events that are reusable across many sinks

This is the cleaner long-term home for media-group reassembly, because the problem is Telegram-specific rather than WordPress-specific.

WordPress plugin responsibility

The WordPress plugin should own publication policy: - title/body/slug generation - hashtag filtering for publication - media upload strategy - taxonomy/status routing for WordPress posts - post creation and update policy - practical fallback buffering when a downstream publication target needs a whole bundle before it can post

That is why this plugin now includes a small staging/flush mechanism: it solves the immediate WordPress publishing problem without forcing the Telegram plugin to become aggregate-aware first.

Message splits without media_group_id

This is a different class of problem.

If content is split across unrelated Telegram messages with no stable transport-level grouping key, the WordPress plugin should not guess. That belongs either in: - an explicit authoring convention, - a Telegram-side aggregation rule, - or a higher-level editorial workflow.

media_group_id is reliable enough to buffer automatically. Arbitrary multi-message essays are not.

Idempotency and edits

The plugin keeps publication mappings and attempts in the same SQLite state DB used for staged media groups.

Keys: - tg-message:<chat_id>:<message_id> - tg-media-group:<chat_id>:<media_group_id>

Behavior: - repeated delivery of the same Telegram message is skipped once mirrored - edited_message / edited_channel_post updates patch the existing WordPress post - source versions combine Telegram edit_date/date with update_id, so older edits are skipped even when they arrive after a newer one - mapped-post updates use the same publication lease as creates, preventing concurrent edits from racing each other - media-group flush stores both the group key and all participating message keys

Crash-recovery protocol

For each Telegram message or media group, the plugin derives a stable publication key and a deterministic WordPress slug containing a hash of that key.

  1. A worker atomically claims a short SQLite lease in publication_attempts and renews it while Telegram downloads, WordPress uploads, and post requests are active. publication_heartbeat_seconds may shorten the renewal interval; it cannot exceed one third of the lease.
  2. Other workers wait for the mapping or return wordpress.deferred while that lease is active.
  3. The owner creates the WordPress post and records the returned post id as remote_created only while its lease is still live.
  4. Message and media-group mappings are committed together and the attempt becomes mapped; completion is fenced by owner token and lease expiry.
  5. If the process dies after WordPress accepted the post but before local completion, the expired-lease owner queries WordPress by the deterministic slug, recovers the post id, and repairs every mapping without another create.

The remote HTTP call is deliberately outside the SQLite transaction. SQLite protects local ownership and completion; the deterministic WordPress slug provides reconciliation across the system boundary.

Each successful WordPress media upload is written to publication_media before post creation continues. On recovery, the plugin first checks the durable remote_created state or deterministic post slug. remote_created cannot be stolen by an edited-message worker; it is finalized directly from the stored post ID. Existing posts retain their tracked media and repair local mappings; absent posts cause create-attempt media to be deleted before another upload is attempted. The normal media-group flush also runs bounded reconciliation for entries older than orphan_media_cleanup_after_seconds (default: 86,400 seconds, limit 50). Cleanup excludes active leases at query time and claims each candidate again before destructive work, closing the selection-to-deletion race.

state_db_path must be configured and stored on durable media for this protocol. Back up the state DB together with WordPress. The defaults are a 30-second publication lease and a two-second duplicate-worker wait; both can be overridden with publication_lease_seconds and publication_wait_seconds.

Dry-run media-group flushes never consume staged data. Failed or concurrently active publications also remain staged for a later flush.

Staged media-group parts are version-aware as well: a delayed original part cannot replace a newer edited part or extend its flush deadline. If recovery lookup against WordPress fails, the local lease is marked failed and released immediately for retry.

State-store schema initialization is serialized within a process and WAL activation retries lock contention from other processes. Normal store connections do not attempt to change journal mode, so concurrent first-use actions can safely share a newly created database.

There remains a very small external-system ambiguity if the process dies after WordPress returns an upload response but before the ledger insert commits. Update-attempt media is tracked for diagnosis but is not automatically deleted because an existing post may already reference it. These cases are visible in publication_media and should be reviewed before manual deletion.

In the call-baxter runtime, this package should be treated as a sink-side publication plugin:

  • Telegram plugins emit telegram.* events.
  • call-baxter rules decide which events should result in publication work.
  • This plugin exposes WordPress-oriented actions that operate on Telegram payloads through those rules.

That keeps the WordPress side publication-focused instead of turning it into a transport adapter.