1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726# 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](#all-40-channels)
- [Channel Configuration](#channel-configuration)
- [Channel Overrides](#channel-overrides)
- [Formatter, Rate Limiter, and Policies](#formatter-rate-limiter-and-policies)
- [Telegram](#telegram)
- [Discord](#discord)
- [Slack](#slack)
- [WhatsApp](#whatsapp)
- [Signal](#signal)
- [Matrix](#matrix)
- [Email](#email)
- [WebChat (Built-in)](#webchat-built-in)
- [Agent Routing](#agent-routing)
- [Writing Custom Adapters](#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` |
| 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 | `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:
```toml
[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](#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](#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`:
```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
- A Telegram bot token (from [@BotFather](https://t.me/botfather))
### 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:
```bash
export TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
```
5. Add to config:
```toml
[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"
```
6. Restart the daemon:
```bash
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.
### Interactive Setup
```bash
openfang channel setup telegram
```
This walks you through the setup interactively.
---
## Discord
### Prerequisites
- A Discord application and bot (from the [Discord Developer Portal](https://discord.com/developers/applications))
### Setup
1. Go to [Discord Developer Portal](https://discord.com/developers/applications).
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:
```bash
export DISCORD_BOT_TOKEN=MTIzNDU2Nzg5.ABCDEF.ghijklmnop
```
8. Add to config:
```toml
[channels.discord]
bot_token_env = "DISCORD_BOT_TOKEN"
default_agent = "coder"
```
9. 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](https://api.slack.com/apps) 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:
```bash
export SLACK_APP_TOKEN=xapp-1-...
export SLACK_BOT_TOKEN=xoxb-...
```
7. Add to config:
```toml
[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
```
8. 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](https://developers.facebook.com/).
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:
```bash
export WA_PHONE_ID=123456789012345
export WA_ACCESS_TOKEN=EAABs...
export WA_VERIFY_TOKEN=my-secret-verify-token
```
7. Add to config:
```toml
[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"
```
8. 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`
9. 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
1. Install [signal-cli](https://github.com/AsamK/signal-cli).
2. Register or link a phone number.
3. Add to config:
```toml
[channels.signal]
signal_cli_path = "/usr/local/bin/signal-cli"
phone_number = "+1234567890"
default_agent = "assistant"
```
4. 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:
```bash
export MATRIX_TOKEN=syt_...
```
4. Add to config:
```toml
[channels.matrix]
homeserver_url = "https://matrix.org"
access_token_env = "MATRIX_TOKEN"
user_id = "@openfang-bot:matrix.org"
default_agent = "assistant"
```
5. Invite the bot to the rooms you want it to monitor.
6. 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](https://myaccount.google.com/apppasswords).
2. Set the environment variable:
```bash
export EMAIL_PASSWORD=abcd-efgh-ijkl-mnop
```
3. Add to config:
```toml
[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"
```
4. 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. **Per-channel default**: Each channel config has a `default_agent` field. Messages from that channel go to that agent.
2. **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.
3. **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.
4. **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/openfang-channels/src/types.rs`.
### The ChannelAdapter Trait
```rust
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`:
```rust
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`:
```rust
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:
```rust
#[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.