Channel Adapters
LibreFang connects to messaging platforms through 40 channel adapters, allowing users to interact with their agents across every major communication platform. Adapters span consumer messaging, enterprise collaboration, social media, community platforms, privacy-focused protocols, and generic webhooks.
All adapters share a common foundation: graceful shutdown via watch::channel, exponential backoff on connection failures, Zeroizing<String> for secrets, automatic message splitting for platform limits, per-channel model/prompt overrides, DM/group policy enforcement, per-user rate limiting, and output formatting (Markdown, TelegramHTML, SlackMrkdwn, PlainText).
Table of Contents
- All 40 Channels
- Channel Configuration
- Channel Overrides
- Formatter, Rate Limiter, and Policies
- Telegram
- Discord
- Slack
- Signal
- Matrix
- WebChat (Built-in)
- Agent Routing
- Writing Custom Adapters
All 40 Channels
Core (7)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| Telegram | Bot API long-polling | TELEGRAM_BOT_TOKEN | Telegram |
| Discord | Gateway WebSocket v10 | DISCORD_BOT_TOKEN | Discord |
| Slack | Socket Mode WebSocket | SLACK_BOT_TOKEN, SLACK_APP_TOKEN | Slack |
| Cloud API webhook | WA_ACCESS_TOKEN, WA_PHONE_ID, WA_VERIFY_TOKEN | WhatsApp | |
| Signal | signal-cli REST/JSON-RPC | (system service) | Signal |
| Matrix | Client-Server API /sync | MATRIX_TOKEN | Matrix |
| IMAP + SMTP | EMAIL_PASSWORD | Email |
Enterprise (8)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| Microsoft Teams | Bot Framework v3 webhook + OAuth2 | TEAMS_APP_ID, TEAMS_APP_SECRET | Teams |
| Mattermost | WebSocket + REST v4 | MATTERMOST_TOKEN, MATTERMOST_URL | Mattermost |
| Google Chat | Service account webhook | GOOGLE_CHAT_SA_KEY, GOOGLE_CHAT_SPACE | Custom("google_chat") |
| Webex | Bot SDK WebSocket | WEBEX_BOT_TOKEN | Custom("webex") |
| Feishu / Lark | Open Platform webhook | FEISHU_APP_ID, FEISHU_APP_SECRET | Custom("feishu") |
| Rocket.Chat | REST polling | ROCKETCHAT_TOKEN, ROCKETCHAT_URL | Custom("rocketchat") |
| Zulip | Event queue long-polling | ZULIP_EMAIL, ZULIP_API_KEY, ZULIP_URL | Custom("zulip") |
| XMPP | XMPP protocol (stub) | XMPP_JID, XMPP_PASSWORD, XMPP_SERVER | Custom("xmpp") |
Social (8)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| LINE | Messaging API webhook | LINE_CHANNEL_SECRET, LINE_CHANNEL_TOKEN | Custom("line") |
| Viber | Bot API webhook | VIBER_AUTH_TOKEN | Custom("viber") |
| Facebook Messenger | Platform API webhook | MESSENGER_PAGE_TOKEN, MESSENGER_VERIFY_TOKEN | Custom("messenger") |
| Mastodon | Streaming API WebSocket | MASTODON_TOKEN, MASTODON_INSTANCE | Custom("mastodon") |
| Bluesky | AT Protocol WebSocket | BLUESKY_HANDLE, BLUESKY_APP_PASSWORD | Custom("bluesky") |
| OAuth2 polling | REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USERNAME, REDDIT_PASSWORD | Custom("reddit") | |
| Messaging API polling | LINKEDIN_ACCESS_TOKEN | Custom("linkedin") | |
| Twitch | IRC gateway | TWITCH_TOKEN, TWITCH_CHANNEL | Custom("twitch") |
Community (6)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| IRC | Raw TCP PRIVMSG | IRC_SERVER, IRC_NICK, IRC_PASSWORD | Custom("irc") |
| Guilded | WebSocket | GUILDED_BOT_TOKEN | Custom("guilded") |
| Revolt | WebSocket | REVOLT_BOT_TOKEN | Custom("revolt") |
| Keybase | Bot API polling | KEYBASE_USERNAME, KEYBASE_PAPERKEY | Custom("keybase") |
| Discourse | REST polling | DISCOURSE_API_KEY, DISCOURSE_URL | Custom("discourse") |
| Gitter | Streaming API | GITTER_TOKEN | Custom("gitter") |
Self-hosted (1)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| Nextcloud Talk | REST polling | NEXTCLOUD_TOKEN, NEXTCLOUD_URL | Custom("nextcloud") |
Privacy (3)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| Threema | Gateway API webhook | THREEMA_ID, THREEMA_SECRET | Custom("threema") |
| Nostr | NIP-01 relay WebSocket | NOSTR_PRIVATE_KEY, NOSTR_RELAY | Custom("nostr") |
| Mumble | TCP text protocol | MUMBLE_SERVER, MUMBLE_USERNAME, MUMBLE_PASSWORD | Custom("mumble") |
Workplace (4)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| Pumble | Webhook | PUMBLE_WEBHOOK_URL, PUMBLE_TOKEN | Custom("pumble") |
| Flock | Webhook | FLOCK_TOKEN | Custom("flock") |
| Twist | API v3 polling | TWIST_TOKEN | Custom("twist") |
| DingTalk | Robot API webhook | DINGTALK_TOKEN, DINGTALK_SECRET | Custom("dingtalk") |
Notification (2)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| ntfy | SSE pub/sub | NTFY_TOPIC, NTFY_SERVER | Custom("ntfy") |
| Gotify | WebSocket | GOTIFY_TOKEN, GOTIFY_URL | Custom("gotify") |
Integration (1)
| Channel | Protocol | Env Vars | ChannelType Variant |
|---|---|---|---|
| Webhook | Generic HTTP with HMAC-SHA256 | WEBHOOK_URL, WEBHOOK_SECRET | Custom("webhook") |
Channel Configuration
All channel configurations live in ~/.librefang/config.toml under the [channels] section. Each channel is a subsection:
[channels.telegram]
bot_token_env = "TELEGRAM_BOT_TOKEN"
default_agent = "assistant"
allowed_users = ["123456789"]
[channels.discord]
bot_token_env = "DISCORD_BOT_TOKEN"
default_agent = "coder"
[channels.slack]
bot_token_env = "SLACK_BOT_TOKEN"
app_token_env = "SLACK_APP_TOKEN"
default_agent = "ops"
# Enterprise example
[channels.teams]
app_id_env = "TEAMS_APP_ID"
app_secret_env = "TEAMS_APP_SECRET"
default_agent = "ops"
# Social example
[channels.mastodon]
token_env = "MASTODON_TOKEN"
instance = "https://mastodon.social"
default_agent = "social-media"
Common Fields
bot_token_env/token_env-- The environment variable holding the bot/access token. LibreFang reads the token from this env var at startup. All secrets are stored asZeroizing<String>and wiped from memory on drop.default_agent-- The agent name (or ID) that receives messages when no specific routing applies.allowed_users-- Optional list of platform user IDs allowed to interact. Empty means allow all.overrides-- Optional per-channel behavior overrides (see Channel Overrides below).
Environment Variables Reference (Core Channels)
| Channel | Required Env Vars |
|---|---|
| Telegram | TELEGRAM_BOT_TOKEN |
| Discord | DISCORD_BOT_TOKEN |
| Slack | SLACK_BOT_TOKEN, SLACK_APP_TOKEN |
WA_ACCESS_TOKEN, WA_PHONE_ID, WA_VERIFY_TOKEN | |
| Matrix | MATRIX_TOKEN |
EMAIL_PASSWORD |
Env vars for all other channels are listed in the All 40 Channels tables above.
Channel Overrides
Every channel adapter supports ChannelOverrides, which let you customize behavior per channel without modifying the agent manifest. Add an [channels.<name>.overrides] section in config.toml:
[channels.telegram.overrides]
model = "gemini-2.5-flash"
system_prompt = "You are a concise Telegram assistant. Keep replies under 200 words."
dm_policy = "respond"
group_policy = "mention_only"
rate_limit_per_user = 10
threading = true
output_format = "telegram_html"
usage_footer = "compact"
Override Fields
| Field | Type | Default | Description |
|---|---|---|---|
model | Option<String> | Agent default | Override the LLM model for this channel. |
system_prompt | Option<String> | Agent default | Override the system prompt for this channel. |
dm_policy | DmPolicy | Respond | How to handle direct messages. |
group_policy | GroupPolicy | MentionOnly | How to handle group/channel messages. |
rate_limit_per_user | u32 | 0 (unlimited) | Max messages per minute per user. |
threading | bool | false | Send replies as thread responses (platforms that support it). |
output_format | Option<OutputFormat> | Markdown | Output format for this channel. |
usage_footer | Option<UsageFooterMode> | None | Whether to append token usage to responses. |
Formatter, Rate Limiter, and Policies
Output Formatter
The formatter module (librefang-channels/src/formatter.rs) converts Markdown output from the LLM into platform-native formats:
| OutputFormat | Target | Notes |
|---|---|---|
Markdown | Standard Markdown | Default; passed through as-is. |
TelegramHtml | Telegram HTML subset | Converts **bold** to <b>, `code` to <code>, etc. |
SlackMrkdwn | Slack mrkdwn | Converts **bold** to *bold*, links to <url|text>, etc. |
PlainText | Plain text | Strips all formatting. |
Per-User Rate Limiter
The ChannelRateLimiter (librefang-channels/src/rate_limiter.rs) uses a DashMap to track per-user message counts. When rate_limit_per_user is set on a channel's overrides, the limiter enforces a sliding-window cap of N messages per minute. Excess messages receive a polite rejection.
DM Policy
Controls how the adapter handles direct messages:
| DmPolicy | Behavior |
|---|---|
Respond | Respond to all DMs (default). |
AllowedOnly | Only respond to DMs from users in allowed_users. |
Ignore | Silently drop all DMs. |
Group Policy
Controls how the adapter handles messages in group chats, channels, and rooms:
| GroupPolicy | Behavior |
|---|---|
All | Respond to every message in the group. |
MentionOnly | Only respond when the bot is @mentioned (default). |
CommandsOnly | Only respond to /command messages. |
Ignore | Silently ignore all group messages. |
Policy enforcement happens in dispatch_message() before the message reaches the agent loop. This means ignored messages consume zero LLM tokens.
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=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
- Add to config:
[channels.telegram]
bot_token_env = "TELEGRAM_BOT_TOKEN"
default_agent = "assistant"
# Optional: restrict to specific Telegram user IDs
# allowed_users = ["123456789"]
[channels.telegram.overrides]
# Optional: Telegram-native HTML formatting
# output_format = "telegram_html"
# group_policy = "mention_only"
- Restart the daemon:
librefang start
How It Works
The Telegram adapter uses long-polling via the getUpdates API. It polls every few seconds with a 30-second long-poll timeout. On API failures, it applies exponential backoff (starting at 1 second, up to 60 seconds). Shutdown is coordinated via a watch::channel.
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 into multiple messages to respect Telegram's 4096-character limit using the shared split_message() utility.
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=MTIzNDU2Nzg5.ABCDEF.ghijklmnop
- Add to config:
[channels.discord]
bot_token_env = "DISCORD_BOT_TOKEN"
default_agent = "coder"
- Restart the daemon.
How It Works
The Discord adapter connects to the Discord Gateway via WebSocket (v10). It listens for MESSAGE_CREATE events and routes messages to the configured agent. Responses are sent via the REST API's channels/{id}/messages endpoint.
The adapter handles Gateway reconnection, heartbeating, and session resumption automatically.
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-1-...
export SLACK_BOT_TOKEN=xoxb-...
- Add to config:
[channels.slack]
bot_token_env = "SLACK_BOT_TOKEN"
app_token_env = "SLACK_APP_TOKEN"
default_agent = "ops"
[channels.slack.overrides]
# Optional: Slack-native mrkdwn formatting
# output_format = "slack_mrkdwn"
# threading = true
- 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.
Prerequisites
- A Meta Business account with WhatsApp Cloud API access
Setup
- Go to Meta for Developers.
- Create a Business App.
- Add the WhatsApp product.
- Set up a test phone number (or use a production one).
- Copy:
- Phone Number ID
- Permanent Access Token
- Choose a Verify Token (any string you choose)
- Set environment variables:
export WA_PHONE_ID=123456789012345
export WA_ACCESS_TOKEN=EAABs...
export WA_VERIFY_TOKEN=my-secret-verify-token
- Add to config:
[channels.whatsapp]
mode = "cloud_api"
phone_number_id_env = "WA_PHONE_ID"
access_token_env = "WA_ACCESS_TOKEN"
verify_token_env = "WA_VERIFY_TOKEN"
webhook_port = 8443
default_agent = "assistant"
-
Set up a webhook in the Meta dashboard pointing to your server's public URL:
- URL:
https://your-domain.com:8443/webhook/whatsapp - Verify Token: the value you chose above
- Subscribe to:
messages
- URL:
-
Restart the daemon.
How It Works
The WhatsApp adapter runs an HTTP server (on the configured webhook_port) that receives incoming webhooks from the WhatsApp Cloud API. It handles webhook verification (GET) and message reception (POST). Responses are sent via the Cloud API's messages endpoint.
Signal
Prerequisites
- Signal CLI installed and linked to a phone number
Setup
- Install signal-cli.
- Register or link a phone number.
- Add to config:
[channels.signal]
signal_cli_path = "/usr/local/bin/signal-cli"
phone_number = "+1234567890"
default_agent = "assistant"
- Restart the daemon.
How It Works
The Signal adapter spawns signal-cli as a subprocess in daemon mode and communicates via JSON-RPC. Incoming messages are read from the signal-cli output stream and routed to the configured agent.
Matrix
Prerequisites
- A Matrix homeserver account and access token
Setup
- Create a bot account on your Matrix homeserver.
- Generate an access token.
- Set the environment variable:
export MATRIX_TOKEN=syt_...
- Add to config:
[channels.matrix]
homeserver_url = "https://matrix.org"
access_token_env = "MATRIX_TOKEN"
user_id = "@librefang-bot:matrix.org"
default_agent = "assistant"
- Invite the bot to the rooms you want it to monitor.
- Restart the daemon.
How It Works
The Matrix adapter uses the Matrix Client-Server API. It syncs with the homeserver using long-polling (/sync with a timeout) and processes new messages from joined rooms. Responses are sent via the /rooms/{roomId}/send endpoint.
Prerequisites
- An email account with IMAP and SMTP access
Setup
- For Gmail, create an App Password.
- Set the environment variable:
export EMAIL_PASSWORD=abcd-efgh-ijkl-mnop
- Add to config:
[channels.email]
imap_host = "imap.gmail.com"
imap_port = 993
smtp_host = "smtp.gmail.com"
smtp_port = 587
username = "you@gmail.com"
password_env = "EMAIL_PASSWORD"
poll_interval = 30
default_agent = "email-assistant"
- 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:4200/
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)
Agent Routing
The AgentRouter determines which agent receives an incoming message. The routing logic is:
- Per-channel default: Each channel config has a
default_agentfield. Messages from that channel go to that agent. - User-agent binding: If a user has previously been associated with a specific agent (via commands or configuration), messages from that user route to that agent.
- Command prefix: Users can switch agents by sending a command like
/agent coderin the chat. Subsequent messages will be routed to the "coder" agent. - Fallback: If no routing applies, messages go to the first available agent.
Writing Custom Adapters
To add support for a new messaging platform, implement the ChannelAdapter trait. The trait is defined in crates/librefang-channels/src/types.rs.
The ChannelAdapter Trait
pub trait ChannelAdapter: Send + Sync {
/// Human-readable name of this adapter.
fn name(&self) -> &str;
/// The channel type this adapter handles.
fn channel_type(&self) -> ChannelType;
/// Start receiving messages. Returns a stream of incoming messages.
async fn start(
&self,
) -> Result<Pin<Box<dyn Stream<Item = ChannelMessage> + Send>>, Box<dyn std::error::Error>>;
/// Send a response back to a user on this channel.
async fn send(
&self,
user: &ChannelUser,
content: ChannelContent,
) -> Result<(), Box<dyn std::error::Error>>;
/// Send a typing indicator (optional -- default no-op).
async fn send_typing(&self, _user: &ChannelUser) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
/// Stop the adapter and clean up resources.
async fn stop(&self) -> Result<(), Box<dyn std::error::Error>>;
/// Get the current health status of this adapter (optional -- default returns disconnected).
fn status(&self) -> ChannelStatus {
ChannelStatus::default()
}
/// Send a response as a thread reply (optional -- default falls back to `send()`).
async fn send_in_thread(
&self,
user: &ChannelUser,
content: ChannelContent,
_thread_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.send(user, content).await
}
}
1. Define Your Adapter
Create crates/librefang-channels/src/myplatform.rs:
use crate::types::{
ChannelAdapter, ChannelContent, ChannelMessage, ChannelStatus, ChannelType, ChannelUser,
};
use futures::stream::{self, Stream};
use std::pin::Pin;
use tokio::sync::watch;
use zeroize::Zeroizing;
pub struct MyPlatformAdapter {
token: Zeroizing<String>,
client: reqwest::Client,
shutdown: watch::Receiver<bool>,
}
impl MyPlatformAdapter {
pub fn new(token: String, shutdown: watch::Receiver<bool>) -> Self {
Self {
token: Zeroizing::new(token),
client: reqwest::Client::new(),
shutdown,
}
}
}
impl ChannelAdapter for MyPlatformAdapter {
fn name(&self) -> &str {
"MyPlatform"
}
fn channel_type(&self) -> ChannelType {
ChannelType::Custom("myplatform".to_string())
}
async fn start(
&self,
) -> Result<Pin<Box<dyn Stream<Item = ChannelMessage> + Send>>, Box<dyn std::error::Error>> {
// Return a stream that yields ChannelMessage items.
// Use self.shutdown to detect when the daemon is stopping.
// Apply exponential backoff on connection failures.
let stream = stream::empty(); // Replace with your polling/WebSocket logic
Ok(Box::pin(stream))
}
async fn send(
&self,
user: &ChannelUser,
content: ChannelContent,
) -> Result<(), Box<dyn std::error::Error>> {
// Send the response back to the platform.
// Use split_message() if the platform has message length limits.
// Use self.client and self.token to call the platform's API.
Ok(())
}
async fn stop(&self) -> Result<(), Box<dyn std::error::Error>> {
// Clean shutdown: close connections, stop polling.
Ok(())
}
fn status(&self) -> ChannelStatus {
ChannelStatus::default()
}
}
Key points for new adapters:
- Use
ChannelType::Custom("myplatform".to_string())for the channel type. Only the 9 most common channels have namedChannelTypevariants (Telegram,WhatsApp,Slack,Discord,Signal,Matrix,Email,Teams,Mattermost). All others useCustom(String). - Wrap secrets in
Zeroizing<String>so they are wiped from memory on drop. - Accept a
watch::Receiver<bool>for coordinated shutdown with the daemon. - Use exponential backoff for resilience on connection failures.
- Use the shared
split_message(text, max_len)utility for platforms with message length limits.
2. Register the Module
In crates/librefang-channels/src/lib.rs:
pub mod myplatform;
3. Wire It Into the Bridge
In crates/librefang-api/src/channel_bridge.rs, add initialization logic for your adapter alongside the existing adapters.
4. Add Config Support
In librefang-types, add a config struct:
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MyPlatformConfig {
pub token_env: String,
pub default_agent: Option<String>,
#[serde(default)]
pub overrides: ChannelOverrides,
}
Add it to the ChannelsConfig struct and config.toml parsing. The overrides field gives your channel automatic support for model/prompt overrides, DM/group policies, rate limiting, threading, and output format selection.
5. Add CLI Setup Wizard
In crates/librefang-cli/src/main.rs, add a case to cmd_channel_setup with step-by-step instructions for your platform.
6. Test
Write integration tests. Use the ChannelMessage type to simulate incoming messages without connecting to the real platform.
7. Add Feature Flag
Since PR #223, each channel adapter is gated behind a Cargo feature flag for smaller binary sizes.
In crates/librefang-channels/Cargo.toml:
[features]
channel-myplatform = [] # Add your channel feature
Gate your module in lib.rs:
#[cfg(feature = "channel-myplatform")]
pub mod myplatform;
Forward the feature through crates/librefang-api/Cargo.toml and crates/librefang-cli/Cargo.toml:
[features]
channel-myplatform = ["librefang-channels/channel-myplatform"]
If your adapter needs platform-specific dependencies (e.g., a client library), make them optional and tie them to your feature flag:
[dependencies]
myplatform-sdk = { version = "1.0", optional = true }
[features]
channel-myplatform = ["myplatform-sdk"]
When a user configures a channel whose feature is disabled, LibreFang emits a tracing::warn at startup instead of failing silently.
Example: Contributing a New Channel Adapter
Here's a step-by-step checklist for contributing a new channel adapter:
- Create
crates/librefang-channels/src/myplatform.rsimplementingChannelAdapter - Register the module in
lib.rs(with#[cfg(feature = "channel-myplatform")]) - Add feature flag in
Cargo.toml(channels → api → cli chain) - Add config struct in
librefang-types - Wire into
channel_bridge.rs - Add CLI setup wizard step
- Write integration tests
- Update the channel count in README.md and docs
See the examples/custom-channel/ directory in the main repo for a reference template.