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:
updateormessageaccepts Telegram raw update/message objects.required_hashtagoptionally gates publication.dry_runrenders the source decision without making a remote request.state_db_pathpersists(provider, Telegram source)mappings and source versions.media_group_wait_secondsstages Telegram albums before publication.telegram_gateway_factoryortelegram_bot_tokensupplies Telegram file access for images.media_required: trueturns 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: ignoreremains 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: nativeopts into X's native edit chain.edit_policy: replacepreserves 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:
- Immediately before create/edit, persist an in-flight intent keyed by provider, Telegram source, source version, and payload fingerprint.
- Send exactly one X mutation.
- As soon as X returns a Post ID, persist that ID as a durable receipt.
- 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.