Skip to content

Social plugin architecture

cb-social-plugin is an interface package: it supplies action sinks for Bluesky, Mastodon and X. It does not introduce a second event runtime or a special social rule engine.

Layers

ActionInvocation
      |
      +--> provider native action -----------------------> provider API
      |
      `--> <provider>.mirror_telegram_message
                     |
                     v
              shared mirror contract
               /      |       \
              /       |        \
      source key   state DB   Telegram media gateway
              \       |        /
               \      |       /
                provider adapter ------------------------> provider API

The shared layer in cb_social_plugin.common owns source normalization, hashtag filtering, source-version checks, durable publication mappings, Telegram album staging, image acquisition and the common action-result shape. Provider modules own authentication and remote API semantics.

Shared mirror contract

Each provider exposes:

  • <provider>.mirror_telegram_message
  • <provider>.flush_due_media_groups

The mirror accepts either a raw Telegram update or message. It derives a publication key from chat/message identity (or media-group identity), computes a monotonically comparable source version from Telegram timestamps/update ids, and optionally persists the remote mapping.

With state_db_path configured, an already-published source version is skipped before a remote request. A newer Telegram edit is handed to the provider adapter with the existing mapping.

State database

The package uses a small SQLite database with three tables.

social_publications

Primary key: (provider, source_key).

It stores the remote id/URI/CID/record key when applicable, the latest Telegram source_version, provider metadata, and update time. Provider names are part of the key, so the same Telegram message can be mirrored independently to all three networks in one database.

social_media_groups

Primary key: (provider, source_key).

It stores a due timestamp and the normalized Telegram payloads staged for one album. Repeated identical payloads are deduplicated. A successful real flush persists the publication mapping and deletes the staged group; a dry-run flush reports due groups without deleting them.

social_publish_attempts

Primary key: (provider, source_key, source_version).

This is a durable write-ahead receipt for provider operations whose remote create boundary cannot be made atomic with the local mapping. X uses it when durable state is configured. The row records a payload fingerprint, expected text, previous remote post id for native edits, start time, and—once known—the remote post id/URI. Promoting a successful publication into social_publications removes completed attempts up to that source version.

Media acquisition pipeline

Telegram message(s)
  -> select best photo / image document
  -> deduplicate by file_unique_id or file_id
  -> Telegram file gateway get_file
  -> download bytes
  -> provider upload API
  -> attach returned media/blob ids to post

The gateway can be supplied by telegram_gateway_factory or created from a Telegram bot token. When media exists but no gateway is available, the default behavior is to publish text and report a media omission. media_required: true turns acquisition/upload omissions into failures instead.

The current mirror contract is image-only. Video/GIF publishing is intentionally not normalized across providers because those APIs have different asynchronous and chunked processing semantics.

Idempotency boundaries

The local publication mapping is an operational duplicate guard, not a distributed transaction. Provider idempotency or a durable recovery protocol is therefore required to close or deliberately fail closed around the remote-success/local-commit crash window.

Provider Remote behavior Remaining boundary
Bluesky deterministic record key + putRecord retry targets the same AT Protocol record
Mastodon deterministic create Idempotency-Key; mapped edits target the same status server retention of idempotency keys is provider-defined
X native edits can use edit_options.previous_post_id; creates have no documented idempotency key with state_db_path, persists an in-flight attempt before mutation, records returned Post IDs, and reconciles ambiguous commits read-only; stateless compatibility mode retains the narrow duplicate-create crash window

These properties do not automatically make the Call Baxter action replay-eligible. The generic runtime's action replay evidence also requires durable executor idempotency and authorization provenance; the social mirror contracts intentionally remain fail-closed today. See Arbitrary actions and replay safety.

Error boundary

Provider clients call raise_for_status(). HTTP errors, including rate-limit responses, therefore propagate to the Call Baxter action boundary rather than being converted into apparent success. Call Baxter records the executor failure and applies normal on_error / stop_on_error semantics.