> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nullclaw/nullclaw/llms.txt
> Use this file to discover all available pages before exploring further.

# Channels

> Messaging platform integration via vtable interface

## Overview

Channels are NullClaw's abstraction layer for messaging platforms. Every channel implements the `Channel` vtable interface, enabling runtime-swappable transport backends.

## Channel Interface

```zig theme={null}
pub const Channel = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        // Start the channel (connect, begin listening)
        start: *const fn(ptr: *anyopaque) anyerror!void,
        
        // Stop the channel (disconnect, clean up)
        stop: *const fn(ptr: *anyopaque) void,
        
        // Send a message to a target (user, channel, room, etc.)
        send: *const fn(
            ptr: *anyopaque,
            target: []const u8,
            message: []const u8,
            media: []const []const u8,
        ) anyerror!void,
        
        // Return the channel name (e.g. "telegram", "discord")
        name: *const fn(ptr: *anyopaque) []const u8,
        
        // Health check — return true if operational
        healthCheck: *const fn(ptr: *anyopaque) bool,
        
        // Optional: streaming output (chunk/final events)
        sendEvent: ?*const fn(...) anyerror!void = null,
        
        // Optional: typing indicators
        startTyping: *const fn(ptr: *anyopaque, recipient: []const u8) anyerror!void,
        stopTyping: *const fn(ptr: *anyopaque, recipient: []const u8) anyerror!void,
    };
};
```

## Supported Channels (18+)

### Real-Time Channels

| Channel        | Transport           | Features                                   |
| -------------- | ------------------- | ------------------------------------------ |
| **Telegram**   | Long-polling        | Groups, media, inline keyboards, proxies   |
| **Discord**    | WebSocket gateway   | Voice channels, threads, embeds, reactions |
| **Slack**      | Socket mode + HTTP  | Threads, blocks, app mentions, reactions   |
| **Signal**     | signal-cli JSON-RPC | E2E encryption, groups, attachments        |
| **Matrix**     | HTTP /sync          | Federation, E2E encryption, rooms          |
| **Mattermost** | WebSocket + REST    | Teams, channels, threads, integrations     |
| **IRC**        | TCP socket (TLS)    | Classic IRC protocol, SASL auth            |
| **Nostr**      | NIP-17/NIP-04 DMs   | Decentralized, relay-based, gift wraps     |
| **DingTalk**   | WebSocket stream    | Enterprise IM, China-focused               |

### Webhook-Based Channels

| Channel         | Transport          | Features                                  |
| --------------- | ------------------ | ----------------------------------------- |
| **WhatsApp**    | Meta webhook       | Business API, media, templates            |
| **Lark/Feishu** | HTTP callback      | ByteDance suite, cards, bots              |
| **Line**        | Webhook + push API | Japan/SEA market, stickers, flex messages |
| **OneBot**      | HTTP/WebSocket     | QQ protocol adapter, China IM             |
| **QQ**          | Tencent API        | China's largest IM, groups, cards         |
| **Email**       | IMAP/SMTP          | Classic email, attachments, HTML          |

### Local/Direct Channels

| Channel      | Transport               | Features                            |
| ------------ | ----------------------- | ----------------------------------- |
| **CLI**      | stdin/stdout            | Interactive terminal, REPL mode     |
| **iMessage** | AppleScript + SQLite    | macOS-only, SMS/iMessage hybrid     |
| **MaixCam**  | USB serial + JSON       | Embedded AI camera, IoT             |
| **Web**      | WebSocket (local/relay) | Browser UI, E2E encryption, pairing |

## Channel Message Flow

```mermaid theme={null}
flowchart LR
    Platform[Telegram/Discord/etc.] --> Channel[Channel Implementation]
    Channel --> Normalize[Normalize to ChannelMessage]
    Normalize --> Bus[Message Bus]
    Bus --> Agent[Agent Session]
    Agent --> Response[Generate Response]
    Response --> Channel
    Channel --> Send[Platform API]
```

### Inbound Flow

1. **Platform delivers event** (webhook POST, WebSocket frame, long-poll result)
2. **Channel implementation parses** platform-specific format
3. **Normalizes to `ChannelMessage`**:
   ```zig theme={null}
   pub const ChannelMessage = struct {
       id: []const u8,
       sender: []const u8,
       content: []const u8,
       channel: []const u8,
       timestamp: u64,
       reply_target: ?[]const u8 = null,
       message_id: ?i64 = null,
       first_name: ?[]const u8 = null,
       is_group: bool = false,
   };
   ```
4. **Checks allowlist** (sender must be in `allow_from` config)
5. **Routes to agent session** via message bus
6. **Agent processes** and generates response
7. **Outbound delivery** via `Channel.send()`

### Outbound Flow

```zig theme={null}
try channel.send(
    target,        // "@username" or "chat_id" or "#channel"
    message,       // Text content
    media,         // [][]const u8 — URLs or file paths
);
```

Channel implementation:

1. **Splits long messages** if platform has length limits
2. **Uploads media** (if supported)
3. **Formats platform payload** (JSON, multipart, etc.)
4. **Sends via platform API** (HTTP POST, WebSocket send, etc.)

## Configuration

### Basic Setup

Channels are configured in `~/.nullclaw/config.json`:

```json theme={null}
{
  "channels": {
    "telegram": {
      "accounts": {
        "main": {
          "bot_token": "123456:ABC-DEF",
          "allow_from": ["alice", "bob"],
          "reply_in_private": true,
          "proxy": "socks5://127.0.0.1:9050"
        }
      }
    },
    "discord": {
      "accounts": {
        "main": {
          "token": "discord-bot-token",
          "guild_id": "123456789",
          "allow_from": ["user_id_1"],
          "allow_bots": false
        }
      }
    },
    "signal": {
      "accounts": {
        "main": {
          "phone_number": "+1234567890",
          "allow_from": ["+9876543210"],
          "signal_cli_path": "/usr/local/bin/signal-cli"
        }
      }
    }
  }
}
```

### Multi-Account Support

Run multiple accounts per channel:

```json theme={null}
"telegram": {
  "accounts": {
    "personal": {
      "bot_token": "token1",
      "allow_from": ["alice"]
    },
    "work": {
      "bot_token": "token2",
      "allow_from": ["bob", "carol"]
    }
  }
}
```

## Security: Allowlists

Every channel enforces an **allowlist** for inbound messages:

```json theme={null}
"allow_from": ["alice", "bob"]
```

* **Empty allowlist** = deny all inbound messages
* **`"*"`** = allow all (explicit opt-in)
* **Otherwise** = exact-match allowlist (case-insensitive)

### Special Cases

**Nostr**: The `owner_pubkey` is **always** allowed regardless of `dm_allowed_pubkeys`:

```json theme={null}
"nostr": {
  "owner_pubkey": "npub1abc...",
  "dm_allowed_pubkeys": ["*"]
}
```

**Signal**: Supports both phone numbers and UUIDs:

```json theme={null}
"signal": {
  "allow_from": ["+1234567890", "uuid:a1b2c3d4-..."]
}
```

## Channel-Specific Features

### Telegram

* **Long-polling** (no webhook setup required)
* **Group support** with `reply_in_private` option
* **Media attachments** (photos, documents, audio)
* **SOCKS5 proxy** support for restricted regions
* **Inline keyboards** (button responses)

### Discord

* **WebSocket gateway** (real-time events)
* **Thread-aware** (creates threads for long conversations)
* **Embed support** (rich message formatting)
* **Reaction-based** UI interactions
* **Voice channel** presence (status only, no audio)

### Signal

* **E2E encryption** (native Signal protocol)
* **Group chats** with privacy mode
* **Attachments** (images, files)
* **Typing indicators**
* **Requires `signal-cli`** binary in PATH

### Nostr

* **NIP-17 gift-wrapped DMs** (default)
* **NIP-04 legacy DMs** (fallback)
* **Multi-relay** rumor deduplication
* **DM inbox relays** (kind:10050 announcement)
* **Encrypted private keys** (ChaCha20-Poly1305)

### WhatsApp

* **Business API** (Meta webhook)
* **Template messages** (for initial contact)
* **Media support** (images, audio, documents)
* **Read receipts**

### IRC

* **TLS socket** connection
* **SASL authentication**
* **Channel join/part** management
* **PRIVMSG/NOTICE** support
* **DCC send** (file transfers)

## Message Splitting

Channels automatically split messages that exceed platform limits:

```zig theme={null}
pub fn splitMessage(msg: []const u8, max_bytes: usize) SplitIterator
```

* **Respects UTF-8 boundaries** (no broken multibyte chars)
* **Configurable max size** per platform
* **Iterates chunks** for sequential delivery

Default limits:

* Telegram: 4096 bytes
* Discord: 2000 bytes
* IRC: 512 bytes
* Signal: no enforced limit

## Health Checks

```zig theme={null}
if (channel.healthCheck()) {
    // Channel is operational
}
```

Implementations check:

* **Connection state** (WebSocket alive, TCP socket open)
* **Authentication status** (token valid, login successful)
* **Recent activity** (last message sent/received timestamp)

## Typing Indicators

Optional vtable methods for real-time UX:

```zig theme={null}
try channel.startTyping("@alice");
defer channel.stopTyping("@alice") catch {};

// Generate response...
```

Supported by: Telegram, Discord, Slack, Signal, Matrix.

## Streaming Output

Channels can implement `sendEvent` for incremental delivery:

```zig theme={null}
try channel.sendEvent(
    target,
    "chunk of text...",
    &.{},
    .chunk,  // or .final
);
```

Supported by: Telegram (edit message), Discord (edit message), Web (WebSocket frames).

<Info>
  Channels without `sendEvent` fall back to `send()` for `.final` stage and ignore `.chunk`.
</Info>

## Implementation Example

### Minimal Channel

```zig:src/channels/my_channel.zig theme={null}
const std = @import("std");
const Channel = @import("root.zig").Channel;

pub const MyChannel = struct {
    api_token: []const u8,
    allow_from: []const []const u8,

    pub fn start(ptr: *anyopaque) anyerror!void {
        const self: *MyChannel = @ptrCast(@alignCast(ptr));
        // Connect to platform, start listening...
    }

    pub fn stop(ptr: *anyopaque) void {
        const self: *MyChannel = @ptrCast(@alignCast(ptr));
        // Disconnect, cleanup...
    }

    pub fn send(
        ptr: *anyopaque,
        target: []const u8,
        message: []const u8,
        media: []const []const u8,
    ) anyerror!void {
        const self: *MyChannel = @ptrCast(@alignCast(ptr));
        // Format payload, call platform API...
    }

    pub fn name(_: *anyopaque) []const u8 {
        return "my_channel";
    }

    pub fn healthCheck(ptr: *anyopaque) bool {
        const self: *MyChannel = @ptrCast(@alignCast(ptr));
        return self.connected;
    }

    pub fn channel(self: *MyChannel) Channel {
        return .{ .ptr = @ptrCast(self), .vtable = &vtable };
    }

    pub const vtable = Channel.VTable{
        .start = start,
        .stop = stop,
        .send = send,
        .name = name,
        .healthCheck = healthCheck,
    };
};
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Configuration" icon="gear" href="/essentials/configuration">
    Full channel configuration reference
  </Card>

  <Card title="Security" icon="shield" href="/concepts/security">
    Learn about allowlists and pairing
  </Card>
</CardGroup>
