Files
wehub-resource-sync d93385b373
CI / Check / macos-latest (push) Waiting to run
CI / Check / ubuntu-latest (push) Waiting to run
CI / Check / windows-latest (push) Waiting to run
CI / Test / macos-latest (push) Waiting to run
CI / Test / ubuntu-latest (push) Waiting to run
CI / Test / windows-latest (push) Waiting to run
CI / Clippy (push) Waiting to run
CI / Format (push) Waiting to run
CI / Security Audit (push) Waiting to run
CI / Secrets Scan (push) Waiting to run
CI / Install Script Smoke Test (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:43:09 +08:00

30 KiB

Channel Adapters

OpenFang 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

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
WhatsApp 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
Email 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 / WebSocket 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")
Reddit OAuth2 polling REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USERNAME, REDDIT_PASSWORD Custom("reddit")
LinkedIn 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 ~/.openfang/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. OpenFang reads the token from this env var at startup. All secrets are stored as Zeroizing<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
WhatsApp WA_ACCESS_TOKEN, WA_PHONE_ID, WA_VERIFY_TOKEN
Matrix MATRIX_TOKEN
Email 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 (openfang-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 (openfang-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

Setup

  1. Open Telegram and message @BotFather.
  2. Send /newbot and follow the prompts to create a new bot.
  3. Copy the bot token.
  4. Set the environment variable:
export TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
  1. 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"
  1. Restart the daemon:
openfang 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.

Per-User Identity (telegram_user_id)

Every inbound Telegram message exposes the sender's numeric Telegram user_id under message.metadata["telegram_user_id"] as a string. This is the stable, permanent identifier from message.from.id (or message.sender_chat.id for channel/group posts).

Display names are not unique and can change, so agents that need deterministic per-user behavior — RBAC, per-user workspaces, family-assistant style memory keyed by person — should key on telegram_user_id, not sender.display_name.

The bridge also injects the id into the prompt prefix when no sender_email is set:

[From: Alena (tg_id:554772934)] Hello!

Agents can read the raw metadata field via tool calls that expose ChannelMessage.metadata (the prompt prefix is a convenience for plain LLM context). sender.platform_id continues to hold the chat_id (not user_id), since replies are addressed to the chat, not the user.

Interactive Setup

openfang channel setup telegram

This walks you through the setup interactively.


Discord

Prerequisites

Setup

  1. Go to Discord Developer Portal.
  2. Click "New Application" and name it.
  3. Go to the Bot section and click "Add Bot".
  4. Copy the bot token.
  5. Under Privileged Gateway Intents, enable:
    • Message Content Intent (required to read message content)
  6. 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.
  7. Set the environment variable:
export DISCORD_BOT_TOKEN=MTIzNDU2Nzg5.ABCDEF.ghijklmnop
  1. Add to config:
[channels.discord]
bot_token_env = "DISCORD_BOT_TOKEN"
default_agent = "coder"
  1. 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

  1. Go to Slack API and click "Create New App" > "From Scratch".
  2. Enable Socket Mode (Settings > Socket Mode):
    • Generate an App-Level Token with scope connections:write.
    • Copy the token (xapp-...).
  3. Go to OAuth & Permissions and add Bot Token Scopes:
    • chat:write
    • app_mentions:read
    • im:history
    • im:read
    • im:write
  4. Install the app to your workspace.
  5. Copy the Bot User OAuth Token (xoxb-...).
  6. Set the environment variables:
export SLACK_APP_TOKEN=xapp-1-...
export SLACK_BOT_TOKEN=xoxb-...
  1. 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
  1. 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.


WhatsApp

Prerequisites

  • A Meta Business account with WhatsApp Cloud API access

Setup

  1. Go to Meta for Developers.
  2. Create a Business App.
  3. Add the WhatsApp product.
  4. Set up a test phone number (or use a production one).
  5. Copy:
    • Phone Number ID
    • Permanent Access Token
    • Choose a Verify Token (any string you choose)
  6. Set environment variables:
export WA_PHONE_ID=123456789012345
export WA_ACCESS_TOKEN=EAABs...
export WA_VERIFY_TOKEN=my-secret-verify-token
  1. 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"
  1. 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
  2. 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.


Feishu / Lark

Prerequisites

Setup

  1. Create a custom app in Feishu Open Platform.
  2. Enable the IM message event subscription for your app.
  3. Set environment variable:
export FEISHU_APP_SECRET=cli_xxx_secret
  1. Add to config (default: websocket mode):
[channels.feishu]
app_id = "cli_xxx"
app_secret_env = "FEISHU_APP_SECRET"
mode = "websocket"
default_agent = "assistant"
  1. Restart the daemon.

Webhook Compatibility Mode

If you need the legacy callback flow, switch to webhook and expose a public callback URL:

[channels.feishu]
app_id = "cli_xxx"
app_secret_env = "FEISHU_APP_SECRET"
mode = "webhook"
webhook_port = 8453
default_agent = "assistant"

Then configure Feishu event callback to:

https://<your-domain>:8453/feishu/webhook

How It Works

  • websocket mode: OpenFang obtains endpoint from Feishu and receives events via long connection (no public inbound webhook needed).
  • webhook mode: OpenFang starts an HTTP callback server and receives Feishu push events.
  • send path (both modes): outbound messages still go through Feishu OpenAPI HTTP im/v1/messages.

Signal

Prerequisites

  • Signal CLI installed and linked to a phone number

Setup

  1. Install signal-cli.
  2. Register or link a phone number.
  3. Add to config:
[channels.signal]
signal_cli_path = "/usr/local/bin/signal-cli"
phone_number = "+1234567890"
default_agent = "assistant"
  1. 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

  1. Create a bot account on your Matrix homeserver.
  2. Generate an access token.
  3. Set the environment variable:
export MATRIX_TOKEN=syt_...
  1. Add to config:
[channels.matrix]
homeserver_url = "https://matrix.org"
access_token_env = "MATRIX_TOKEN"
user_id = "@openfang-bot:matrix.org"
default_agent = "assistant"
  1. Invite the bot to the rooms you want it to monitor.
  2. 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.


Email

Prerequisites

  • An email account with IMAP and SMTP access

Setup

  1. For Gmail, create an App Password.
  2. Set the environment variable:
export EMAIL_PASSWORD=abcd-efgh-ijkl-mnop
  1. 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"
  1. 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:

  1. Bindings (most specific first). Declarative [[bindings]] rules in config.toml map message attributes (channel, channel_id, peer_id, guild_id, account_id, roles) to agents. The router scores each rule by specificity and picks the highest-scoring match.
  2. Per-channel default: Each channel config has a default_agent field. Messages from that channel go to that agent.
  3. 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.
  4. Command prefix: Users can switch agents by sending a command like /agent coder in the chat. Subsequent messages will be routed to the "coder" agent.
  5. Fallback: If no routing applies, messages go to the first available agent.

Bindings

A binding has an agent (the target) and a match_rule (the criteria). All non-empty fields in the rule must match.

# Route a specific Discord channel to a dedicated agent.
[[bindings]]
agent = "researcher-medical"
match_rule = { channel = "discord", channel_id = "1234567890" }

[[bindings]]
agent = "researcher-business"
match_rule = { channel = "discord", channel_id = "9876543210" }

# Catch-all for the same user on any other channel.
[[bindings]]
agent = "assistant"
match_rule = { channel = "discord", peer_id = "user_discord_id" }

peer_id vs channel_id — these are easy to confuse and the difference matters:

  • peer_id matches the user (Discord user ID, Slack user ID, etc.).
  • channel_id matches the channel/conversation (Discord text channel, Slack conversation, Telegram chat).

Use peer_id for "messages from this person." Use channel_id for "messages in this room."

Specificity scores (higher wins):

Field Score
peer_id 8
channel_id 8
guild_id 4
roles 2
account_id 2
channel 1

A binding's score is the sum of its set fields. peer_id and channel_id are equally specific, so a rule with both (16) beats either alone (8). Ties are broken by declaration order in the config.

Adapter coverage for channel_id — the following adapters populate ctx.channel_id directly from sender.platform_id (their "user" field is overloaded as a channel/conversation/room/space ID because that field doubles as the send target):

discord, slack, telegram, matrix, mattermost, teams, webex, rocketchat, nextcloud, pumble, revolt, guilded, feishu, lark, keybase, google_chat, line, twist, flock, twitch.

(Feishu Intl region emits Custom("lark") rather than Custom("feishu"); both spellings are recognized.)

Adapters not on this list (Reddit, Bluesky, Mastodon, Signal, Email, ntfy, Discourse, etc.) carry a user ID in platform_id and have no per-conversation concept, or use a hybrid scheme (IRC, Zulip flip between channel and user based on is_group). Bindings targeting channel_id on those platforms will only match if the adapter writes a channel_id key into message metadata.

The kernel emits a startup warning when a binding sets channel_id for a non-supporting adapter, so misconfigurations surface early instead of silently routing nowhere. The single source of truth for this list is CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL in openfang-types::config, consumed by both routing (ChannelMessage::channel_id()) and config validation.

Strict parsingAgentBinding and BindingMatchRule use #[serde(deny_unknown_fields)]. Typos at the binding level (e.g. match_rules for match_rule, channnel_id for channel_id) fail config load with a clear error rather than parsing into a no-op rule that silently matches every message. Existing configs that work today are unaffected; only configs with stray/misspelled fields inside a [[bindings]] block need a fix. The top-level KernelConfig deliberately stays permissive so unrecognized top-level keys (forward-compat, downstream forks) don't break startup.


Writing Custom Adapters

To add support for a new messaging platform, implement the ChannelAdapter trait. The trait is defined in crates/openfang-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/openfang-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 named ChannelType variants (Telegram, WhatsApp, Slack, Discord, Signal, Matrix, Email, Teams, Mattermost). All others use Custom(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/openfang-channels/src/lib.rs:

pub mod myplatform;

3. Wire It Into the Bridge

In crates/openfang-api/src/channel_bridge.rs, add initialization logic for your adapter alongside the existing adapters.

4. Add Config Support

In openfang-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/openfang-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.