Files
OpenJarvis/docs/user-guide/channels.md
T
2026-03-12 17:29:39 +00:00

471 lines
15 KiB
Markdown

# Channels
The channels module lets OpenJarvis send and receive messages through external messaging platforms. Each platform has a dedicated channel implementation that connects directly to the platform's API -- there is no intermediate gateway.
!!! note "Channels are disabled by default"
The `[channel]` config section defaults to `enabled = false`. You must set `enabled = true` and configure platform-specific credentials before channel features become active.
---
## Overview
Channel messaging is built around the `BaseChannel` ABC. Each platform (Telegram, Discord, Slack, WhatsApp, etc.) has its own implementation registered via `@ChannelRegistry.register("name")`. Channels connect directly to their platform APIs, register handlers for incoming messages, and send outgoing messages.
```mermaid
graph LR
A[Your Code] -->|send| B[TelegramChannel / DiscordChannel / SlackChannel / ...]
B -->|Platform API| C[Telegram / Discord / Slack / ...]
C -->|incoming messages| B
B -->|on_message handlers| D[Your Handlers]
```
---
## Supported Channels
| Channel | Registry Key | Platform | Pip Extra | Auth |
|---------|-------------|----------|-----------|------|
| `TelegramChannel` | `telegram` | Telegram Bot API | `channel-telegram` | Bot token |
| `DiscordChannel` | `discord` | Discord Bot API | `channel-discord` | Bot token |
| `SlackChannel` | `slack` | Slack Web API | `channel-slack` | Bot + App tokens |
| `WhatsAppChannel` | `whatsapp` | WhatsApp Business API | — | API token |
| `WhatsAppBaileysChannel` | `whatsapp_baileys` | WhatsApp (Baileys) | — | QR code auth |
| `WebhookChannel` | `webhook` | Generic HTTP webhook | — | URL + optional secret |
| `EmailChannel` | `email` | SMTP/IMAP email | — | Email credentials |
| `SignalChannel` | `signal` | Signal Messenger | — | Signal CLI |
| `GoogleChatChannel` | `google_chat` | Google Chat | — | Service account |
| `IRCChannel` | `irc` | IRC | — | Server credentials |
| `WebChatChannel` | `webchat` | Browser-based chat | — | None |
| `TeamsChannel` | `teams` | Microsoft Teams | — | Bot credentials |
| `MatrixChannel` | `matrix` | Matrix protocol | — | Homeserver + token |
| `MattermostChannel` | `mattermost` | Mattermost | — | Bot token |
| `FeishuChannel` | `feishu` | Feishu/Lark | — | App credentials |
| `BlueBubblesChannel` | `bluebubbles` | iMessage (BlueBubbles) | — | BlueBubbles server |
| `LineChannel` | `line` | LINE Messaging API | `channel-line` | Channel access token |
| `ViberChannel` | `viber` | Viber Bot API | `channel-viber` | Auth token |
| `MessengerChannel` | `messenger` | Facebook Messenger | `channel-messenger` | Page access token |
| `RedditChannel` | `reddit` | Reddit API | `channel-reddit` | OAuth credentials |
| `MastodonChannel` | `mastodon` | Mastodon API | `channel-mastodon` | Access token |
| `XMPPChannel` | `xmpp` | XMPP/Jabber | `channel-xmpp` | JID + password |
| `RocketChatChannel` | `rocketchat` | Rocket.Chat API | `channel-rocketchat` | User credentials |
| `ZulipChannel` | `zulip` | Zulip API | `channel-zulip` | Bot email + API key |
| `TwitchChannel` | `twitch` | Twitch IRC/API | `channel-twitch` | OAuth token |
| `NostrChannel` | `nostr` | Nostr protocol | `channel-nostr` | Private key (nsec) |
---
## Using a Channel
### Connecting
```python title="connect.py"
from openjarvis.channels.telegram import TelegramChannel
channel = TelegramChannel(
bot_token="YOUR_BOT_TOKEN", # (1)!
)
channel.connect()
print(channel.status()) # ChannelStatus.CONNECTED
```
1. Falls back to the `TELEGRAM_BOT_TOKEN` environment variable if not provided.
### Sending Messages
```python title="send_message.py"
from openjarvis.channels.telegram import TelegramChannel
channel = TelegramChannel()
channel.connect()
# Send to a chat by ID
ok = channel.send(
"123456789",
"Analysis complete. Results are ready.",
conversation_id="thread-abc123", # optional, for threading
)
if ok:
print("Message delivered")
else:
print("Delivery failed")
channel.disconnect()
```
### Receiving Messages
Register handler callbacks before calling `connect()`. Each handler receives a `ChannelMessage` and can optionally return a reply string.
```python title="receive_messages.py"
from openjarvis.channels._stubs import ChannelMessage
from openjarvis.channels.discord_channel import DiscordChannel
channel = DiscordChannel()
def handle_incoming(msg: ChannelMessage) -> None:
print(f"[{msg.channel}] {msg.sender}: {msg.content}")
print(f" conversation_id={msg.conversation_id}")
print(f" message_id={msg.message_id}")
channel.on_message(handle_incoming) # (1)!
channel.connect() # (2)!
# Messages now arrive asynchronously via the background listener thread
# Your main thread can continue doing other work
```
1. Register one or more handlers. All registered handlers are called for every incoming message.
2. `connect()` starts the background listener thread after establishing the platform connection.
### Listing Available Channels
```python title="list_channels.py"
from openjarvis.channels.slack import SlackChannel
channel = SlackChannel()
channel.connect()
channels = channel.list_channels()
print(channels) # ["general", "random", "dev"]
```
### Disconnecting
```python title="disconnect.py"
channel.disconnect()
# Stops the listener thread and closes the platform connection
# Status becomes ChannelStatus.DISCONNECTED
```
---
## ChannelMessage Fields
Every incoming message is delivered to handlers as a `ChannelMessage` dataclass.
| Field | Type | Description |
|-------|------|-------------|
| `channel` | `str` | Name of the channel the message arrived on |
| `sender` | `str` | Identifier of the message sender |
| `content` | `str` | Message text |
| `message_id` | `str` | Unique message identifier (may be empty) |
| `conversation_id` | `str` | Thread/conversation identifier (may be empty) |
| `session_id` | `str` | Session identifier (may be empty) |
| `metadata` | `dict[str, Any]` | Additional platform-specific metadata |
---
## Event Bus Integration
Pass an `EventBus` to publish channel events to the rest of the system:
```python title="channel_events.py"
from openjarvis.core.events import EventBus, EventType
from openjarvis.channels.telegram import TelegramChannel
bus = EventBus()
def on_received(event):
print(f"Message received on {event.data['channel']}: {event.data['content']}")
def on_sent(event):
print(f"Message sent to {event.data['channel']}")
bus.subscribe(EventType.CHANNEL_MESSAGE_RECEIVED, on_received)
bus.subscribe(EventType.CHANNEL_MESSAGE_SENT, on_sent)
channel = TelegramChannel(bus=bus)
channel.connect()
```
| Event | Published When | Data Keys |
|-------|----------------|-----------|
| `CHANNEL_MESSAGE_RECEIVED` | A message arrives from the platform | `channel`, `sender`, `content`, `message_id` |
| `CHANNEL_MESSAGE_SENT` | A message is successfully sent | `channel`, `content`, `conversation_id` |
---
## CLI Commands
The `jarvis channel` subcommand group provides quick access to channel operations.
### List Channels
```bash
jarvis channel list
```
### Send a Message
```bash
# Send to a channel by name
jarvis channel send telegram "Build completed successfully"
```
### Show Status
```bash
jarvis channel status
```
---
## API Server Endpoints
When `jarvis serve` is running, three channel endpoints are available. Channels must be configured and enabled in `[channel]` for these endpoints to return data.
### `GET /v1/channels`
Returns the list of registered channels and their status.
```bash
curl http://localhost:8000/v1/channels
```
```json
{
"channels": ["telegram", "discord", "slack"],
"status": "connected"
}
```
If no channels are configured:
```json
{"channels": [], "message": "No channels configured"}
```
### `POST /v1/channels/send`
Send a message to a channel.
```bash
curl -X POST http://localhost:8000/v1/channels/send \
-H "Content-Type: application/json" \
-d '{"channel": "telegram", "content": "Hello!", "conversation_id": "conv-1"}'
```
```json
{"status": "sent", "channel": "telegram"}
```
Required fields: `channel`, `content`. `conversation_id` is optional.
### `GET /v1/channels/status`
Returns the connection status for each configured channel.
```bash
curl http://localhost:8000/v1/channels/status
```
```json
{"status": "connected"}
```
Possible values: `connected`, `disconnected`, `connecting`, `error`, `not_configured`.
---
## Configuration
Channel settings live in the `[channel]` section of `~/.openjarvis/config.toml`. Each platform has its own nested sub-section.
```toml title="~/.openjarvis/config.toml"
[channel]
enabled = true
default_channel = ""
default_agent = "simple"
[channel.telegram]
bot_token = "YOUR_TELEGRAM_BOT_TOKEN"
[channel.discord]
bot_token = "YOUR_DISCORD_BOT_TOKEN"
[channel.slack]
bot_token = "YOUR_SLACK_BOT_TOKEN"
app_token = "YOUR_SLACK_APP_TOKEN"
```
### Configuration Reference
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | `bool` | `false` | Enable channel messaging |
| `default_channel` | `str` | `""` | Default channel to use when not specified |
| `default_agent` | `str` | `simple` | Agent to use for handling inbound messages |
Platform-specific settings are configured in nested sub-sections (e.g., `[channel.telegram]`, `[channel.discord]`).
---
## Complete Example
This example connects a Telegram channel, registers a handler that echoes messages back, sends a test message, and then disconnects after a short wait.
```python title="full_example.py"
import time
from openjarvis.channels._stubs import ChannelMessage
from openjarvis.channels.telegram import TelegramChannel
from openjarvis.core.events import EventBus
bus = EventBus()
channel = TelegramChannel(
bot_token="YOUR_BOT_TOKEN",
bus=bus,
)
received_messages = []
def on_message(msg: ChannelMessage) -> None:
received_messages.append(msg)
print(f"Received from {msg.sender} on #{msg.channel}: {msg.content}")
channel.on_message(on_message)
channel.connect()
# List available channels
channels = channel.list_channels()
print(f"Available channels: {channels}")
# Send a message
if channels:
channel.send(channels[0], "Hello from OpenJarvis!")
# Wait for incoming messages
time.sleep(10)
channel.disconnect()
print(f"Total messages received: {len(received_messages)}")
```
---
## WhatsAppBaileysChannel
`WhatsAppBaileysChannel` is registered as `"whatsapp_baileys"` in `ChannelRegistry` and provides **bidirectional WhatsApp messaging** using the Baileys protocol. It spawns a Node.js bridge subprocess that handles QR-code authentication, incoming message forwarding, and outbound message delivery.
!!! warning "Node.js 22+ required"
The Baileys bridge is a compiled Node.js application bundled inside the package. It is auto-installed to `~/.openjarvis/whatsapp_baileys_bridge/` on first `connect()` call. If `node` is not found on `PATH`, `connect()` logs an error and sets the channel to `ChannelStatus.ERROR`.
!!! note "WhatsApp account required"
WhatsApp does not offer an official API for personal accounts. Baileys operates on the WhatsApp Web protocol. You must scan a QR code with your WhatsApp mobile app to authenticate on first use.
### Connecting
```python title="whatsapp_connect.py"
from openjarvis.channels.whatsapp_baileys import WhatsAppBaileysChannel
channel = WhatsAppBaileysChannel(
assistant_name="Jarvis", # (1)!
assistant_has_own_number=False, # (2)!
)
channel.connect() # spawns the Node.js bridge subprocess
```
1. Display name used in conversation context.
2. Set `True` if the assistant has a dedicated WhatsApp number and should not filter its own messages.
On first connection, the bridge will print a QR code to the terminal. Scan it with the WhatsApp app on your phone to authenticate. Authentication state is saved to `~/.openjarvis/whatsapp_baileys_bridge/auth/` and reused on subsequent connections.
### Receiving Messages
```python title="whatsapp_receive.py"
from openjarvis.channels._stubs import ChannelMessage
from openjarvis.channels.whatsapp_baileys import WhatsAppBaileysChannel
channel = WhatsAppBaileysChannel()
def on_message(msg: ChannelMessage) -> None:
print(f"[{msg.sender}] {msg.content}")
# msg.conversation_id is the WhatsApp JID (e.g. "15551234567@s.whatsapp.net")
channel.on_message(on_message)
channel.connect()
# Background reader thread is running; your code continues here
```
### Sending Messages
Messages are addressed by WhatsApp **JID** (Jabber ID) -- the canonical identifier for a WhatsApp contact or group.
```python title="whatsapp_send.py"
# Individual contact JID format: <country-code><number>@s.whatsapp.net
# Group JID format: <group-id>@g.us
ok = channel.send(
"15551234567@s.whatsapp.net", # JID of the recipient
"Hello from OpenJarvis!",
)
if not ok:
print("Send failed -- check that the bridge is connected")
```
### Disconnecting
```python title="whatsapp_disconnect.py"
channel.disconnect()
# Sends disconnect command to bridge, terminates subprocess, stops reader thread
```
### Constructor Parameters
| Parameter | Type | Default | Description |
|----------------------------|------------|-------------|--------------------------------------------------------|
| `auth_dir` | `str` | `~/.openjarvis/whatsapp_baileys_bridge/auth` | Baileys auth state directory |
| `assistant_name` | `str` | `"Jarvis"` | Display name for the assistant |
| `assistant_has_own_number` | `bool` | `False` | Whether the assistant has a dedicated WhatsApp number |
| `bus` | `EventBus` | `None` | Event bus for publishing channel events |
### Bridge Events
The Node.js bridge communicates with Python via JSON lines on stdio. Python interprets the following event types:
| Bridge event type | Effect |
|-------------------|-------------------------------------------------------------|
| `status` | Updates `ChannelStatus` (`connected` / `disconnected`) |
| `qr` | Logs "QR code received -- scan to authenticate" |
| `message` | Dispatches to all registered `on_message` handlers |
| `error` | Logs the error and sets status to `ChannelStatus.ERROR` |
### Event Bus Integration
When a `bus` is provided, `WhatsAppBaileysChannel` publishes the same events as other channels:
| Event | Published When | Data Keys |
|-------|----------------|-----------|
| `CHANNEL_MESSAGE_RECEIVED` | An inbound WhatsApp message arrives | `channel`, `sender`, `content`, `message_id` |
| `CHANNEL_MESSAGE_SENT` | A message is successfully sent | `channel`, `content`, `conversation_id` |
### Configuration
WhatsApp Baileys channel settings live in the `[channel.whatsapp_baileys]` subsection:
```toml title="~/.openjarvis/config.toml"
[channel.whatsapp_baileys]
auth_dir = "/home/user/.openjarvis/whatsapp_baileys_bridge/auth"
assistant_name = "Jarvis"
assistant_has_own_number = false
```
---
## See Also
- [Architecture: Channels](../architecture/channels.md) -- listener loop internals and channel design
- [API Reference: Channels](../api-reference/openjarvis/channels/index.md) -- full class and type signatures
- [Getting Started: Configuration](../getting-started/configuration.md) -- full config reference
- [User Guide: Agents](agents.md) -- agent system documentation