Skip to content

Social publishing plugins

cb-social-plugin packages three independent Call Baxter plugin entry points:

Plugin Native actions Telegram mirror
bluesky create/update/delete post, upload blob bluesky.mirror_telegram_message
mastodon create/update/delete post, upload media mastodon.mirror_telegram_message
x create/update/delete post, upload image x.mirror_telegram_message

Each provider also exposes <provider>.flush_due_media_groups for Telegram albums staged through media_group_wait_seconds.

Shared mirror contract

The mirror layer deliberately follows the existing WordPress operator shape without forcing the three remote protocols into identical semantics:

  • update or message accepts Telegram raw update/message objects.
  • required_hashtag optionally gates publication.
  • dry_run renders the source decision without making a remote request.
  • state_db_path persists (provider, Telegram source) mappings and source versions.
  • media_group_wait_seconds stages Telegram albums before publication.
  • telegram_gateway_factory or telegram_bot_token supplies Telegram file access for images.
  • media_required: true turns otherwise reported media omissions into hard failures.

The initial media contract is image-only. This keeps the mirror deterministic while provider video and GIF APIs have materially different asynchronous/chunked processing models.

Provider-specific behavior

Bluesky

Configuration accepts either access_token + repo, or identifier + app_password. The latter creates an AT Protocol session and uses the returned DID as the repository.

Mirrors derive a stable record key from the Telegram source key and publish with putRecord. Telegram edits therefore update the same AT Protocol record. URL facets are generated with UTF-8 byte offsets, and up to four downloaded images are embedded. The plugin currently enforces a 1,000,000-byte per-image mirror limit before upload.

Mastodon

Configuration requires base_url and a user access_token. Creates use POST /api/v1/statuses with a deterministic Idempotency-Key; Telegram edits use the mapped status ID with the status edit endpoint. Visibility, sensitivity, content warning, and language can be supplied in config or action arguments. Images upload through /api/v2/media.

X

Configuration requires a user access_token; base_url defaults to https://api.x.com. Posts use the current v2 create/edit/delete contract and images use the v2 media upload endpoint.

The current X API edits through POST /2/tweets with edit_options.previous_post_id; each successful edit creates a new Post ID while retaining one edit history. X remains authoritative for edit eligibility and limits. For Telegram edits:

  • edit_policy: ignore remains the compatibility default and keeps the existing X Post unchanged while advancing the local source version so the same Telegram edit is not retried forever.
  • edit_policy: native opts into X's native edit chain.
  • edit_policy: replace preserves the older delete-then-repost behavior for existing configurations; it is non-atomic and should only be used when that failure mode is acceptable.

Native editing is recommended for new edit-propagation rules because it avoids the destructive delete/recreate window without changing existing rule semantics.

Idempotency boundary

Use state_db_path in production. Bluesky has an additional remote safety property because the mirror targets a deterministic record key. Mastodon sends an idempotency key on creates, but remote retention is bounded by the server implementation.

X crash-window warning and recovery

X does not document a create idempotency key for POST /2/tweets, so Call Baxter cannot make the remote commit and its SQLite mapping one atomic transaction. When state_db_path is configured, X mirrors use a small durable write-ahead protocol:

  1. Immediately before create/edit, persist an in-flight intent keyed by provider, Telegram source, source version, and payload fingerprint.
  2. Send exactly one X mutation.
  3. As soon as X returns a Post ID, persist that ID as a durable receipt.
  4. Promote the receipt into the ordinary source-to-Post mapping; that commit removes the intent.

If the process dies between steps 2 and 3, the next delivery does not issue another create. It queries the authenticated user's recent X posts from just before the attempt and only adopts a remote Post when exactly one post has the expected text. For an edit, the candidate must also carry the prior mapped Post ID in its edit history. A durable receipt from step 3 is recovered without any X mutation at all.

If read access is unavailable, X has not made the post visible yet, or reconciliation finds zero or multiple candidates, the mirror raises XMirrorRecoveryRequired and leaves the intent intact. This is deliberate fail-closed behavior: a temporarily blocked mirror is preferable to blindly creating a duplicate. A later replay can reconcile the same intent once the timeline is visible. Removing the intent by hand should only be done after independently proving that X did not commit the post.

For compatibility, X mirroring without state_db_path is still accepted. That path has no durable mapping or recovery receipt and therefore retains the narrow crash window where X may create a post before Call Baxter can persist proof of it. Production X mirroring should configure durable state.