Core Messaging Channels
These are the most commonly used channel adapters, covering the major consumer messaging platforms and built-in communication options.
Telegram
Prerequisites
- A Telegram bot token (from @BotFather)
Setup
- Open Telegram and message
@BotFather. - Send
/newbotand follow the prompts to create a new bot. - Copy the bot token.
- Set the environment variable:
export TELEGRAM_BOT_TOKEN=your-token # Environment variable
librefang vault set TELEGRAM_BOT_TOKEN # Encrypted vault (recommended)
librefang config set-key telegram # .env file
# Or set via dashboard "Set API Key" button # secrets.env
- Add to config:
[[sidecar_channels]]
name = "telegram"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.telegram"]
channel_type = "telegram"
[sidecar_channels.env]
TELEGRAM_BOT_TOKEN = "..."
# ALLOWED_USERS = "123456789,@alice"
The in-process [channels.telegram] block is no longer accepted. Declare Telegram as a [[sidecar_channels]] entry as shown above.
Rust alternative (since #5831). A first-party Rust Telegram sidecar binary at sdk/rust/librefang-sidecar-telegram/ is wire-equivalent to the Python adapter — same TELEGRAM_BOT_TOKEN / ALLOWED_USERS env, same Markdown → Telegram-HTML rendering, same allowed-update set. Pick it when you do not want a Python runtime on the host, when per-respawn startup latency matters, or when the ~3 MB stripped binary beats a Python image. Build with cargo build --release -p librefang-sidecar-telegram and swap the command line above for /abs/path/to/target/release/librefang-sidecar-telegram. Full reference: Rust Telegram sidecar adapter.
- Restart the daemon:
librefang start
How It Works
The Telegram sidecar adapter (librefang.sidecar.adapters.telegram, shipped in librefang-sdk) uses long-polling via the getUpdates API with a 30-second server timeout. On API failures it applies supervised exponential backoff. The daemon restarts the subprocess on crash.
Messages from authorized users are converted to ChannelMessage events and routed to the configured agent. Responses are sent back via the sendMessage API. Long responses are automatically split to respect Telegram's 4096-character limit.
File Attachments
Files attached to Telegram messages arrive as temporary authenticated URLs that the LLM cannot access directly. The bridge downloads them to a configurable directory and rewrites the message into local-path content blocks — non-image files become a ContentBlock::Text with the saved path (the agent then calls file_read); images become ContentBlock::ImageFile. See Channel File Downloads on the overview for file_download_dir / file_download_max_bytes and the stale-file sweeper.
Interactive Setup
librefang channel setup telegram
This walks you through the setup interactively.
Discord
Prerequisites
- A Discord application and bot (from the Discord Developer Portal)
Setup
- Go to Discord Developer Portal.
- Click "New Application" and name it.
- Go to the Bot section and click "Add Bot".
- Copy the bot token.
- Under Privileged Gateway Intents, enable:
- Message Content Intent (required to read message content)
- Go to OAuth2 > URL Generator:
- Select scopes:
bot - Select permissions:
Send Messages,Read Message History - Copy the generated URL and open it to invite the bot to your server.
- Select scopes:
- Set the environment variable:
export DISCORD_BOT_TOKEN=your-token # Environment variable
librefang vault set DISCORD_BOT_TOKEN # Encrypted vault (recommended)
librefang config set-key discord # .env file
# Or set via dashboard "Set API Key" button # secrets.env
- Add to config (Discord is an out-of-process sidecar adapter):
[[sidecar_channels]]
name = "discord"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.discord"]
channel_type = "discord"
[sidecar_channels.env]
DISCORD_BOT_TOKEN = "..."
# DISCORD_ALLOWED_GUILDS = "123,456"
# DISCORD_INTENTS = "37376"
- Restart the daemon.
How It Works
The Discord sidecar (librefang.sidecar.adapters.discord in the
Python SDK) connects to the Discord Gateway via WebSocket (v10) in a
supervised subprocess and speaks newline-delimited JSON-RPC over
stdio with the daemon. It listens for MESSAGE_CREATE events and
emits them as message events; agent replies are POSTed via the REST
API's channels/{id}/messages endpoint.
The sidecar handles Gateway reconnection, periodic heartbeats (with
RFC-required jitter on the first beat), session resumption, and
gateway close-code handling automatically. Daemon-side supervision
(restart, exponential backoff, circuit-break) is configured via the
common [[sidecar_channels]] fields documented under
Sidecar channels.
Slack
Prerequisites
- A Slack app with Socket Mode enabled
Setup
- Go to Slack API and click "Create New App" > "From Scratch".
- Enable Socket Mode (Settings > Socket Mode):
- Generate an App-Level Token with scope
connections:write. - Copy the token (
xapp-...).
- Generate an App-Level Token with scope
- Go to OAuth & Permissions and add Bot Token Scopes:
chat:writeapp_mentions:readim:historyim:readim:write
- Install the app to your workspace.
- Copy the Bot User OAuth Token (
xoxb-...). - Set the environment variables:
export SLACK_APP_TOKEN=xapp-... # Environment variable
export SLACK_BOT_TOKEN=xoxb-...
librefang vault set SLACK_APP_TOKEN # Encrypted vault (recommended)
librefang vault set SLACK_BOT_TOKEN
librefang config set-key slack # .env file
# Or set via dashboard "Set API Key" button # secrets.env
- Add to config (Slack is an out-of-process sidecar adapter):
[[sidecar_channels]]
name = "slack"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.slack"]
channel_type = "slack"
[sidecar_channels.env]
SLACK_APP_TOKEN = "xapp-..."
SLACK_BOT_TOKEN = "xoxb-..."
# SLACK_FORCE_FLAT_REPLIES = "false" # set "true" to post replies as top-level messages
- Restart the daemon.
How It Works
The Slack adapter uses Socket Mode, which establishes a WebSocket connection to Slack's servers. This avoids the need for a public webhook URL. The adapter receives events (app mentions, direct messages) and routes them to the configured agent. Responses are posted via the chat.postMessage Web API. When threading = true, replies are sent to the message's thread via thread_ts.
Processing-State Reactions
The Slack adapter shows live "I'm working on it" feedback by adding 👀 to the user's message on receive and replacing it with ✅ when the reply is sent. Toggle with the SLACK_REACTIONS environment variable (default true — set to false to disable). already_reacted and no_reaction errors are silently ignored, so reaction failures never block message processing. See Reactions and Processing State on the channels overview.
WhatsApp (sidecar)
WhatsApp migrated from the in-process Rust adapter to a Python sidecar (librefang.sidecar.adapters.whatsapp, stdlib-only). The in-process [channels.whatsapp] block is no longer recognised. Both modes are preserved: Cloud API (Meta's official Business API) and Web/QR mode (the Node.js @librefang/whatsapp-gateway Baileys process).
Prerequisites
- For Cloud API: a Meta Business account with WhatsApp Cloud API access (phone number ID, access token, app secret)
- For Web/QR mode: Node.js >= 18 on the daemon host and a personal WhatsApp account
python3on the daemon host (no third-party Python packages required)
Cloud API mode setup
- Go to Meta for Developers and create a Business App with the WhatsApp product.
- Set up a phone number; note the Phone Number ID and Permanent Access Token. Pick any string for the Verify Token.
- In the app's Settings, copy the App Secret — that's the HMAC-SHA256 key for
X-Hub-Signature-256. - Add to
config.toml:
[[sidecar_channels]]
name = "whatsapp"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.whatsapp"]
channel_type = "whatsapp"
[sidecar_channels.env]
WHATSAPP_PHONE_NUMBER_ID = "your-phone-id"
WHATSAPP_WEBHOOK_PORT = "8460"
# WHATSAPP_DM_POLICY = "respond"
# WHATSAPP_GROUP_POLICY = "all"
- Put
WHATSAPP_ACCESS_TOKEN,WHATSAPP_VERIFY_TOKEN,WHATSAPP_APP_SECRETin~/.librefang/secrets.env. - In the Meta dashboard, set the Webhook URL to
https://your-domain.com:8460/webhook(or whateverWHATSAPP_WEBHOOK_PORT/WHATSAPP_WEBHOOK_PATHyou chose). Subscribe tomessages. Use the same Verify Token. - Restart the daemon.
The sidecar runs its own webhook server, verifies X-Hub-Signature-256 against the App Secret on every inbound, and sends outbound via https://graph.facebook.com/v17.0/{phone_id}/messages. Unlike the legacy Rust adapter (whose start() was a stub that never parsed inbound), this sidecar implements the full Cloud API webhook handler.
Web/QR mode setup
- Install + run the Baileys gateway separately:
npx @librefang/whatsapp-gateway
# Listens on http://127.0.0.1:3009 by default
- Scan the QR code printed by the gateway with WhatsApp → Linked Devices.
- Add to
config.toml:
[[sidecar_channels]]
name = "whatsapp"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.whatsapp"]
channel_type = "whatsapp"
[sidecar_channels.env]
WHATSAPP_GATEWAY_URL = "http://localhost:3009"
- Restart the daemon.
In Web/QR mode the gateway POSTs inbound directly to LibreFang's REST API (/api/agents/{id}/message), bypassing the sidecar; outbound replies route through the sidecar to {gateway_url}/message/send. The kernel no longer auto-spawns the gateway (the embedded whatsapp_gateway.rs module was removed in this migration) — you must run it as a separate process.
Group chat & DM policy
The sidecar applies the same DM × group policy as the legacy Rust adapter:
WHATSAPP_DM_POLICY | Behaviour |
|---|---|
respond (default) | Respond to all DMs |
allowed_only | Respond only when sender's phone is in WHATSAPP_ALLOWED_USERS |
ignore | Drop all DMs |
WHATSAPP_GROUP_POLICY | Behaviour |
|---|---|
all (default) | Respond to all group messages |
mention_only | Respond only when bot phone or WHATSAPP_BOT_NAME appears in the text |
commands_only | Respond only to /cmd messages |
ignore | Drop all group messages |
For mention_only set WHATSAPP_BOT_PHONE (with +, e.g. +15551234567) and / or WHATSAPP_BOT_NAME (case-insensitive substring).
WeChat (Personal) — sidecar
WeChat is provided by the Python sidecar adapter (librefang.sidecar.adapters.wechat). The in-process [channels.wechat] config block was removed in the sidecar migration. Declare WeChat as a [[sidecar_channels]] entry instead.
Prerequisites
- A personal WeChat account (iOS 8.0.70+ recommended)
- WeChat ClawBot plugin access (currently in gradual rollout)
python3on the daemon host (stdlib-only sidecar — no third-party packages)
Setup
- Add to
config.toml:
[[sidecar_channels]]
name = "wechat"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.wechat"]
channel_type = "wechat"
[sidecar_channels.env]
# WECHAT_BOT_TOKEN = "" # leave blank to trigger QR login
# WECHAT_ALLOWED_USERS = "" # comma-separated hash@im.wechat
- Start (or restart) the LibreFang daemon. The sidecar will log a QR code at INFO — scan it with your WeChat app to confirm. After the first QR login, paste the
bot_tokenintoWECHAT_BOT_TOKEN(in~/.librefang/secrets.env) to skip the QR flow on subsequent restarts.
How It Works
The WeChat adapter uses Tencent's official iLink protocol (ilinkai.weixin.qq.com), the same protocol behind the WeChat ClawBot plugin. No third-party proxies or unofficial APIs are involved.
Connection flow:
- QR Login — calls
GET /ilink/bot/get_bot_qrcodeto generate a QR code, then pollsGET /ilink/bot/get_qrcode_statusuntil the user scans and confirms. Returns abot_tokenfor all subsequent requests. - Long-Polling — calls
POST /ilink/bot/getupdateswith a cursor (get_updates_buf). The server holds the connection for up to 35 seconds until new messages arrive, then returns them along with an updated cursor. - Sending — calls
POST /ilink/bot/sendmessagewith thecontext_tokenfrom the inbound message to associate the reply with the correct conversation. - Typing — calls
POST /ilink/bot/sendtypingwith atyping_ticket(fetched fromPOST /ilink/bot/getconfig) to show a typing indicator.
Supported message types: text, image, voice, file, video (all 5 iLink item types).
Reconnection: If a bot_token is configured, the adapter skips QR login and resumes polling immediately. On network errors, exponential backoff is applied (2s → 60s max).
Limitations
- Media upload is not yet supported (CDN flow with AES-128-ECB encryption). Incoming media is received; outgoing media falls back to text placeholders.
- Group chat detection is not yet implemented.
- Streaming responses are not yet supported (messages are sent as complete text).
- Grayscale rollout — the iLink API may not be available to all WeChat accounts yet.
Signal (sidecar)
Signal migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.signal).
Prerequisites
- A running
signal-cli-rest-apiinstance (Docker image recommended), registered with a phone number. - Python 3 + the
librefangSDK (pip install -e sdk/python).
Setup
- Stand up
signal-cli-rest-api(typically as a container behind an HTTPS reverse proxy) and register the bot's phone number with Signal. - Declare the sidecar in
~/.librefang/config.toml:
[[sidecar_channels]]
name = "signal"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.signal"]
channel_type = "signal"
default_agent = "assistant"
[sidecar_channels.env]
SIGNAL_API_URL = "https://signal-cli.example.com"
SIGNAL_NUMBER = "+15555550100"
# SIGNAL_ALLOWED_USERS = "+15555550199,+15555550200" # optional
# SIGNAL_POLL_INTERVAL_SECS = "2" # optional
# SIGNAL_ALLOW_LOCAL = "1" # only if API runs on localhost
- (Optional) If
signal-cli-rest-apiwas started with--api-key, addSIGNAL_API_KEY=…to~/.librefang/secrets.env. - Restart the daemon.
How It Works
The sidecar polls GET /v1/receive/{phone} every SIGNAL_POLL_INTERVAL_SECS (default 2 s) and emits each new dataMessage.message to the kernel over stdio. Outbound messages use POST /v2/send. The SSRF guard rejects SIGNAL_API_URL values that resolve to private / loopback / CGNAT / link-local addresses unless SIGNAL_ALLOW_LOCAL=1 is set explicitly.
Plain-Text Output by Default
Signal output defaults to OutputFormat::PlainText because signal-cli renders Markdown asterisks and underscores literally. The mapping is keyed off the sidecar entry's channel_type = "signal", so the default still applies after the migration. Per-channel output_format overrides on [[sidecar_channels]] are not currently exposed — flip the format on the agent if you need Markdown — see Signal Plain-Text Default on the channels overview.
Media Attachments
The Rust adapter delivered Image, Voice, Video, Audio, Animation, File, FileData, and MediaGroup content blocks via base64_attachments on /v2/send. The sidecar currently routes anything other than text to the standard (Unsupported content type) placeholder; a follow-up will re-wire base64 attachments end-to-end through the sidecar runtime.
Matrix (sidecar)
Matrix migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.matrix).
Prerequisites
- A Matrix homeserver account and access token (Element-issued token works).
- Python 3 + the
librefangSDK (pip install -e sdk/python).
Setup
- Create a bot account on your Matrix homeserver (or use an existing one).
- Generate an access token (Element settings → Help & About → Access Token, or
POST /_matrix/client/v3/login). - Put the token in
~/.librefang/secrets.env:
MATRIX_ACCESS_TOKEN=syt_...
- Declare the sidecar in
~/.librefang/config.toml:
[[sidecar_channels]]
name = "matrix"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.matrix"]
channel_type = "matrix"
default_agent = "assistant"
[sidecar_channels.env]
MATRIX_HOMESERVER_URL = "https://matrix.org"
MATRIX_USER_ID = "@librefang-bot:matrix.org"
# MATRIX_ALLOWED_ROOMS = "!abc:matrix.org,!def:matrix.org" # optional
# MATRIX_ACCOUNT_ID = "prod-bot" # optional
# MATRIX_MAX_UPLOAD_BYTES = "52428800" # optional, default 50 MiB
- Invite the bot to the rooms you want it to monitor.
- Restart the daemon.
How It Works
The sidecar polls GET /_matrix/client/v3/sync with the bot's access token, 30 s server timeout, since cursor for incremental delivery. Inbound m.room.message events (m.text / m.notice / m.emote / m.image / m.file / m.audio / m.video) are emitted to the kernel; outbound replies use PUT /_matrix/client/v3/rooms/{room}/send/{type}/{txn}. Markdown is rendered to Matrix's HTML subset for formatted_body; m.thread and m.replace (edit) relations are wired through so threaded replies and streaming edits work out of the box.
Prerequisites
- An email account with IMAP and SMTP access
Setup
- For Gmail, create an App Password.
- Set the environment variable:
export EMAIL_PASSWORD=your-password # Environment variable
librefang vault set EMAIL_PASSWORD # Encrypted vault (recommended)
librefang config set-key email # .env file
# Or set via dashboard "Set API Key" button # secrets.env
- Add to config (email runs as an out-of-process sidecar adapter,
librefang.sidecar.adapters.email):
[[sidecar_channels]]
name = "email"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.email"]
channel_type = "email"
[sidecar_channels.env]
EMAIL_IMAP_HOST = "imap.gmail.com"
EMAIL_SMTP_HOST = "smtp.gmail.com"
EMAIL_USERNAME = "you@gmail.com"
# EMAIL_IMAP_PORT = "993"
# EMAIL_SMTP_PORT = "587"
# EMAIL_POLL_INTERVAL_SECS = "30"
EMAIL_PASSWORD belongs in ~/.librefang/secrets.env.
- Hot-reload (
curl -X POST http://127.0.0.1:4545/api/channels/reload) or restart the daemon.
How It Works
The email adapter polls the IMAP inbox at the configured interval. New emails are parsed (subject + body) and routed to the configured agent. Responses are sent as reply emails via SMTP, preserving the subject line threading.
WebChat (Built-in)
The WebChat UI is embedded in the daemon and requires no configuration. When the daemon is running:
http://127.0.0.1:4545/
Features:
- Real-time chat via WebSocket
- Streaming responses (text deltas as they arrive)
- Agent selection (switch between running agents)
- Token usage display
- No authentication required on localhost (protected by CORS)
Message Truncation for Platform Limits
Source: librefang-channels/src/message_truncator.rs
Messaging platforms impose hard character limits on outbound messages, counted in UTF-16 code units rather than bytes or Unicode code points. LibreFang measures message length in UTF-16 units and handles oversized messages automatically.
Platform Limits
| Platform | UTF-16 unit limit | Behavior when exceeded |
|---|---|---|
| Telegram | 4 096 | Message is split into multiple sequential messages |
| Discord | 2 000 | Message is split into multiple sequential messages |
| Other channels | Configurable | Truncated by default unless split is enabled |
How UTF-16 Length Differs from Byte Length
Most ASCII text has the same byte count and UTF-16 unit count. The difference matters for:
- Emoji (
😀): 2 UTF-16 units, but 4 bytes in UTF-8 - Supplementary CJK characters: 2 UTF-16 units, 4 bytes in UTF-8
- Basic CJK / Korean / Arabic: 1 UTF-16 unit, 3 bytes in UTF-8
A string that fits in 2 000 bytes may still exceed 2 000 UTF-16 units, or vice versa. Using byte length as a proxy silently produces truncated or rejected messages.
Truncation
truncate_to_utf16_limit(text, limit) uses binary search over the string's UTF-16
unit positions to find the longest prefix that fits, then cuts at a valid Unicode scalar
boundary. The result is never split mid-codepoint or mid-surrogate-pair.
Splitting
split_to_utf16_chunks(text, limit) divides the message into sequential chunks, each
within limit UTF-16 units. LibreFang sends the chunks as separate messages in order,
preserving all content. The split boundary respects Unicode scalar boundaries — no
chunk ends in the middle of a character.
Splitting and chunking are now sidecar-side
Every channel adapter runs out-of-process as a sidecar now, so each
sidecar owns its own chunking at platform limits — the pre-migration
split_long_messages knob on [channels.<name>] is gone with the
rest of ChannelsConfig. Sidecars cannot be told to truncate
instead of split from the daemon side; if you need that behaviour
for a custom sidecar, implement it inside that sidecar.