# How the Hermes Agent Messaging Gateway Handles Platform-Specific Adapters for Telegram, Discord, Slack, and WhatsApp

> Discover how the Hermes Agent messaging gateway uses platform-specific adapters for Telegram, Discord, Slack, and WhatsApp. Run unified conversational logic across all platforms efficiently.

- Repository: [Nous Research/hermes-agent](https://github.com/NousResearch/hermes-agent)
- Tags: how-to-guide
- Published: 2026-03-09

---

**The Hermes Agent messaging gateway abstracts Telegram, Discord, Slack, and WhatsApp behind a unified `BasePlatformAdapter` interface, allowing the same conversational logic to run across all platforms while respecting each service's unique capabilities and constraints.**

The NousResearch Hermes Agent project provides a multi-platform messaging gateway that enables AI agents to communicate seamlessly across different chat services. By implementing platform-specific adapters that inherit from a common base class, the gateway normalizes message formats, media handling, and connection management. This architecture ensures that whether users interact via Telegram bots, Discord servers, Slack workspaces, or WhatsApp chats, the underlying agent logic remains identical.

## Base Adapter Interface

All platform adapters inherit from `BasePlatformAdapter` defined in [`gateway/platforms/base.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/base.py). This abstract class establishes the core contract that every platform must satisfy, ensuring consistent behavior across the messaging gateway.

The base adapter declares the following abstract methods:

- `connect()` – Open a connection to the service and start receiving events
- `disconnect()` – Gracefully shut down the connection
- `send()` – Deliver a text message with optional reply threading
- `edit_message()` – Edit a previously sent message if the platform supports it
- `send_image()` / `send_image_file()` – Send images by URL or from local files
- `send_voice()` – Send audio or voice files
- `send_video()` – Send video files
- `send_document()` – Send generic file attachments
- `send_typing()` – Emit a typing indicator while the agent processes
- `get_chat_info()` – Query chat metadata including name and type
- `format_message()` – Convert platform-specific markdown
- `truncate_message()` – Split oversized messages respecting platform limits

The base class also defines common data structures including `MessageEvent`, `MessageType`, and `SendResult`. It provides helper utilities for caching media files locally via `cache_image_from_*`, `cache_audio_from_*`, and `cache_document_from_*` methods. These ensure incoming media payloads are stored in a stable short-lived cache at `~/.hermes/*_cache`, allowing vision and transcription tools to access files even after platform URLs expire.

## Telegram Adapter

The Telegram implementation in [`gateway/platforms/telegram.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/telegram.py) uses the `python-telegram-bot` library (v20+) to handle the Bot API.

### Connection and Event Loop

The `connect()` method builds an `Application` instance using the bot token from `self.config.token`. It registers three `MessageHandler` instances for text, commands, and media, then starts polling via `updater.start_polling()`.

### Message Normalisation

The adapter converts incoming `telegram.Update` objects into `MessageEvent` instances through `_handle_text_message`, `_handle_command`, and `_handle_media_message`. Media types including photos, videos, audio, voice, documents, and stickers are downloaded into local caches using `cache_image_from_bytes`, `cache_audio_from_bytes`, and `cache_document_from_bytes`. Sticker handling includes optional vision analysis with cached descriptions to avoid repeated API calls.

### Sending Messages

The `send()` method formats text using **MarkdownV2**, truncates content to `self.MAX_MESSAGE_LENGTH` (4096 characters), and falls back to plain text if the Markdown parser rejects the content. Native media methods including `send_image`, `send_image_file`, `send_voice`, `send_video`, and `send_document` use the Telegram Bot API's file upload endpoints. URL-based images are sent directly when possible; otherwise, they are downloaded and uploaded.

### Additional Features

The adapter registers a rich set of bot commands (`/new`, `/reset`, `/model`, etc.) during `connect()` to populate Telegram's built-in command menu. The `format_message()` method escapes MarkdownV2-specific characters, while `extract_images()` converts Markdown image syntax into native attachments.

## Discord Adapter

The Discord implementation in [`gateway/platforms/discord.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/discord.py) relies on [`discord.py`](https://github.com/NousResearch/hermes-agent/blob/main/discord.py) to manage the Gateway API and slash commands.

### Connection and Event Loop

The `connect()` method creates a `commands.Bot` instance with required intents including message content, DMs, guild messages, and members. Slash commands are registered via `tree.command` and synced on startup using `tree.sync()`.

### Message Normalisation

The `on_message` listener discards bot messages and supports mention-gated replies unless the channel appears in `DISCORD_FREE_RESPONSE_CHANNELS`. The adapter handles text, commands (starting with `/`), and attachments. Images, audio, and other files are cached using `cache_image_from_url` and `cache_audio_from_url`.

### Sending Messages

The `send()` method resolves the Discord channel, formats text without special escaping, and respects Discord's 2000-character limit by splitting content using `truncate_message`. Threaded replies use the `reference` field on the first chunk. Media methods including `send_image`, `send_image_file`, `send_voice`, `send_video`, and `send_document` upload files using `discord.File`. For URLs, the adapter downloads content via **aiohttp** then uploads as a file because Discord does not render external URLs as inline media.

### Approval UI

Dangerous commands are gated by `send_exec_approval()`, which posts an embed with three buttons: **Allow Once**, **Always Allow**, and **Deny**. The `ExecApprovalView` verifies the clicking user against `DISCORD_ALLOWED_USERS` and records permanent approvals using the `approval` tool.

### Additional Features

The adapter automatically resolves usernames provided in `DISCORD_ALLOWED_USERS` to numeric IDs on startup via `_resolve_allowed_usernames`.

## Slack Adapter

The Slack implementation in [`gateway/platforms/slack.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/slack.py) uses **slack-bolt** with **Socket Mode** to handle real-time messaging without public HTTP endpoints.

### Connection and Event Loop

The `connect()` method creates an `AsyncApp` with the bot token, fetches the bot's own user ID via `auth_test` for mention detection, and registers two handlers: `handle_message_event` for regular messages and `handle_hermes_command` for the `/hermes` slash command.

### Message Normalisation

The `_handle_slack_message` method filters out bot messages, edits, and deletions. In channels, the bot only responds when its user ID is mentioned (`<@UXXXX>`). Attachments are downloaded via `_download_slack_file` using the bot token for authentication; images and audio are cached locally.

### Sending Messages

The `send()` method posts plain text via `chat_postMessage`, optionally threading using `thread_ts`. The `send_image` and `send_image_file` methods download images via **httpx** and upload them with `files_upload_v2`. If the download fails, the adapter falls back to sending the URL as plain text. The `send_voice` method uploads audio as a file since Slack has no native voice message type.

### Typing Indicator

Slack does not expose a typing API for bots, so `send_typing()` is a no-op.

### Additional Features

The `/hermes` slash command maps sub-commands (`new`, `reset`, `status`, etc.) to Hermes internal commands, providing a familiar command surface inside Slack.

## WhatsApp Adapter

The WhatsApp implementation in [`gateway/platforms/whatsapp.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/whatsapp.py) follows a **bridge pattern** because WhatsApp lacks an official bot API for personal accounts.

### Architecture Overview

A Node.js process (the "bridge") runs a WhatsApp Web client (using **whatsapp-web.js** or **Baileys**) and exposes a tiny HTTP JSON API. The Python adapter communicates with this bridge over `localhost:<port>`.

### Connection and Process Management

The `connect()` method checks for Node.js, ensures bridge dependencies via `npm install`, kills any orphaned processes bound to the configured port, then spawns the bridge with `subprocess.Popen`. The bridge health endpoint (`/health`) is polled until it reports `"status":"connected"` (maximum 30 seconds). A background task `_poll_messages()` repeatedly calls `/messages` to retrieve inbound WhatsApp messages and converts each into a `MessageEvent`.

### Message Normalisation

The `_build_message_event()` method interprets bridge payloads, distinguishes media types (`PHOTO`, `VIDEO`, `VOICE`, `DOCUMENT`), caches remote media using `cache_image_from_url` and `cache_audio_from_url`, and builds the appropriate `MessageEvent`.

### Sending Messages

The `send()` method posts to `/send` with JSON containing `chatId`, `message`, and `replyTo`. Media methods (`send_image`, `send_image_file`, `send_video`, `send_document`) delegate to `_send_media_to_bridge`, which posts to `/send-media` with the local file path and media type. The bridge handles the actual upload to WhatsApp, including inline playback for videos.

### Typing Indicator

Implemented by posting to `/typing`; the bridge forwards the "typing..." state to the WhatsApp client.

### Chat Info

The `get_chat_info()` method queries `/chat/<chatId>` on the bridge, returning the chat name, type (`group` or `dm`), and participants if available.

### Resilience

The adapter aggressively cleans up the bridge process on `disconnect()`, kills any lingering processes on the same port, and logs bridge output to `bridge.log` for troubleshooting.

## Code Examples

### Initialising the Gateway

```python
from gateway.config import load_config
from gateway.platforms.telegram import TelegramAdapter
from gateway.platforms.discord import DiscordAdapter
from gateway.platforms.slack import SlackAdapter
from gateway.platforms.whatsapp import WhatsAppAdapter

async def start_all():
    cfg = load_config()                     # reads ~/.hermes/config.yaml

    adapters = [
        TelegramAdapter(cfg.platforms.telegram),
        DiscordAdapter(cfg.platforms.discord),
        SlackAdapter(cfg.platforms.slack),
        WhatsAppAdapter(cfg.platforms.whatsapp),
    ]

    # Connect every enabled platform concurrently

    await asyncio.gather(*(a.connect() for a in adapters))

    # The gateway now routes incoming events to the central AIAgent

    # (see hermes_cli/gateway.py for the dispatcher).

# In the real Hermes CLI this is called from hermes_cli/gateway.py

```

### Sending a Message From a Tool

A tool (e.g., a `/reply` command) can address a platform-agnostic `send` method:

```python
async def reply_to_user(adapter, chat_id, text):
    # `adapter` is any subclass of BasePlatformAdapter

    result = await adapter.send(chat_id, text)
    if not result.success:
        logger.error("Failed to send reply: %s", result.error)

```

### Using the Exec-Approval Flow (Discord)

```python

# In a tool that runs a shell command:

approval_id = await request_approval(
    platform_adapter=discord_adapter,
    chat_id=channel_id,
    command="rm -rf /tmp/*"
)

if approval_id:
    # The user clicked "Allow Once" or "Always Allow"

    await run_dangerous_command()

```

The `DiscordAdapter.send_exec_approval()` automatically builds an embed with three buttons and records permanent approvals when the "Always Allow" button is pressed.

### WhatsApp Bridge Startup (Manual)

```bash

# Ensure Node.js is installed

node --version

# Install bridge dependencies (run once)

cd $(python -c "import pathlib, hermes_agent; print(pathlib.Path(hermes_agent.__file__).parents[2] / 'scripts' / 'whatsapp-bridge')")
npm install

# Start Hermes; the adapter will launch the bridge automatically:

hermes gateway

```

The bridge logs (`bridge.log`) contain QR-code prompts for initial pairing and connection status.

## Summary

- The **Hermes Agent messaging gateway** uses a unified `BasePlatformAdapter` interface defined in [`gateway/platforms/base.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/base.py) to abstract platform differences across Telegram, Discord, Slack, and WhatsApp.
- Each concrete adapter implements core lifecycle methods (`connect()`, `disconnect()`, `send()`) and media handling (`send_image()`, `send_voice()`, `send_document()`) while respecting platform-specific limits and formatting rules.
- **Telegram** uses `python-telegram-bot` with MarkdownV2 formatting and native media upload endpoints.
- **Discord** leverages [`discord.py`](https://github.com/NousResearch/hermes-agent/blob/main/discord.py) with slash commands, button-based approval UIs, and mention-gating for channel responses.
- **Slack** integrates via `slack-bolt` using Socket Mode, with file uploads via `files_upload_v2` and slash command mapping.
- **WhatsApp** employs a bridge pattern with a Node.js subprocess exposing an HTTP API, handling the lack of an official bot API through [`whatsapp-web.js`](https://github.com/NousResearch/hermes-agent/blob/main/whatsapp-web.js) or Baileys.

## Frequently Asked Questions

### How does the messaging gateway handle different message length limits across platforms?

Each adapter implements the `truncate_message()` method to split oversized content into chunks that respect platform-specific constraints. For example, the Discord adapter enforces a 2000-character limit per message, while the Telegram adapter supports up to 4096 characters. The base class provides common splitting logic, but individual adapters can override this to handle platform-specific formatting requirements like threaded replies or multi-part message chains.

### Can the Hermes Agent send media files like images and voice messages on all platforms?

Yes, all four platform adapters implement the full media suite: `send_image()`, `send_voice()`, `send_video()`, and `send_document()`. However, implementation details vary by platform. Telegram and Discord support native inline media rendering, while Slack requires file uploads via `files_upload_v2`. WhatsApp routes media through the Node.js bridge which handles the actual upload to WhatsApp Web. Each adapter also caches remote media locally using `cache_image_from_*` and `cache_audio_from_*` helpers to ensure availability after platform URLs expire.

### How does the gateway manage authentication and connection lifecycles?

Each adapter manages its own connection state through the standardized `connect()` and `disconnect()` methods. Telegram uses polling via `python-telegram-bot`, Discord uses the Gateway API via [`discord.py`](https://github.com/NousResearch/hermes-agent/blob/main/discord.py), Slack uses Socket Mode with `slack-bolt`, and WhatsApp spawns a Node.js bridge process that communicates over localhost HTTP. The base class defines these as abstract methods, ensuring every adapter implements proper lifecycle management including graceful shutdowns and connection health monitoring.

### Is there a way to restrict which users can execute dangerous commands on Discord?

Yes, the Discord adapter implements an execution approval system via `send_exec_approval()`. When a tool attempts to run a dangerous command, the adapter posts an embed with three buttons: **Allow Once**, **Always Allow**, and **Deny**. The `ExecApprovalView` class verifies the clicking user against the `DISCORD_ALLOWED_USERS` configuration list. Permanent approvals are recorded using the `approval` tool, while the adapter automatically resolves usernames to numeric IDs on startup via `_resolve_allowed_usernames()`.