Hermes-style agent gateway in MoonBit: bridges 14 messaging platforms (Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant, Mattermost, Matrix, DingTalk, Feishu/Lark, WeChat, WeCom, iMessage via BlueBubbles) into a single agent callback, with retries, chunking, typing indicators, per-chat sessions and slash commands. TOML configuration with Hermes-compatible environment-variable fallback.
| Platform | value | Inbound | Sends |
|---|---|---|---|
| Telegram | telegram | webhook + long-poll | text / media / edit / delete / typing / reactions |
| Discord | discord | gateway events + interactions | text / embeds / edit / delete / typing / reactions |
| Slack | slack | events API (signature-checked) | text / edit / delete / typing / reactions |
| WhatsApp (Cloud/Meta API) | webhook + hub verification | text / media / read receipts | |
| Signal (signal-cli HTTP) | signal | receive RPC polling | text via JSON-RPC send |
| Email (SMTP/IMAP) | parsed raw messages | RFC 5322 messages over SMTP (STARTTLS/SSL) | |
| Mattermost | mattermost | outgoing webhook | posts via v4 API |
| Matrix | matrix | /sync polling | text / edit / reactions / typing (m.* events) |
| DingTalk | dingtalk | robot callback | interactive-card / webhook / app messages |
| Feishu / Lark | feishu | event webhook + challenge | app messages |
| WeChat (iLink Bot API) | weixin | long-poll | iLink bot messages |
| WeCom (Enterprise WeChat) | wecom | callback (XML + JSON) | app messages / group-robot webhook, markdown |
| BlueBubbles (iMessage) | bluebubbles | new-message webhook | text via AppleScript, attachments |
| Home Assistant | homeassistant | automation webhook | notify / any notify.* service |
moon check --target native
moon build --target native
moon test --target native # 131 testsIMRELAY_CONFIG=gateway.toml moon run example/main --target nativeGET / landing page / health check
GET /health {"ok":true,"platforms":N}
POST /<platform> inbound webhook for that platform (per table above)[gateway]
name = "my-gateway" # optional
host = "0.0.0.0" # listen address
port = 8080 # listen port
public_url = "https://chat.example.com/" # optional, used for webhook hints
allowed_users = ["alice"] # optional global allow-list
allow_all_users = true # optional; default false
[platforms.telegram]
token = "123:abc"
home_channel = "-100123"
home_channel_name = "Ops"
home_channel_thread = "42"
allowed_users = ["alice"]
[platforms.telegram.extra] # platform-specific options (see below)
parse_mode = "MarkdownV2"
api_base = "https://tg.local" # optional self-hosted Bot API
polling = true # telegram also long-polls by default| Platform | extra key | Check |
|---|---|---|
| Telegram | webhook_secret | X-Telegram-Bot-Api-Secret-Token (required) |
| Slack | signing_secret | X-Slack-Signature + 5-minute replay window (required) |
| app_secret | X-Hub-Signature-256 HMAC of the raw body (required) | |
| BlueBubbles | password | password query param / x-password header (when set) |
| Mattermost | webhook_token | token in the outgoing-webhook body (when set) |
| WeCom | callback_token + encoding_aes_key | msg_signature SHA1 + AES-256-CBC decrypt (when set) |
| Platform | Variables |
|---|---|
| Global | GATEWAY_ALLOWED_USERS, GATEWAY_ALLOW_ALL_USERS |
| Telegram | TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, TELEGRAM_HOME_CHANNEL[_NAME], TELEGRAM_ALLOWED_USERS |
| Discord | DISCORD_BOT_TOKEN, DISCORD_HOME_CHANNEL[_NAME], DISCORD_ALLOWED_USERS |
| Slack | SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_SIGNING_SECRET, SLACK_HOME_CHANNEL[_NAME], SLACK_ALLOWED_USERS |
| WHATSAPP_CLOUD_PHONE_NUMBER_ID, WHATSAPP_CLOUD_ACCESS_TOKEN, WHATSAPP_CLOUD_API_VERSION, WHATSAPP_CLOUD_VERIFY_TOKEN, WHATSAPP_CLOUD_APP_SECRET, WHATSAPP_HOME_CHANNEL[_NAME], WHATSAPP_ALLOWED_USERS | |
| Signal | SIGNAL_HTTP_URL, SIGNAL_ACCOUNT, SIGNAL_HOME_CHANNEL[_NAME], SIGNAL_ALLOWED_USERS |
| EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, EMAIL_IMAP_HOST, EMAIL_HOME_ADDRESS[_NAME], EMAIL_ALLOWED_USERS | |
| Mattermost | MATTERMOST_TOKEN, MATTERMOST_URL, MATTERMOST_WEBHOOK_TOKEN, MATTERMOST_HOME_CHANNEL[_NAME], MATTERMOST_ALLOWED_USERS |
| Matrix | MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN (or MATRIX_PASSWORD + MATRIX_USER_ID), MATRIX_HOME_ROOM[_NAME], MATRIX_ALLOWED_USERS |
| DingTalk | DINGTALK_CLIENT_ID, DINGTALK_CLIENT_SECRET, DINGTALK_WEBHOOK_URL, DINGTALK_ROBOT_CODE, DINGTALK_HOME_CHANNEL[_NAME], DINGTALK_ALLOWED_USERS |
| Feishu/Lark | FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_DOMAIN, FEISHU_HOME_CHANNEL[_NAME], FEISHU_ALLOWED_USERS |
| WeChat (iLink) | WEIXIN_ACCOUNT_ID, WEIXIN_TOKEN, WEIXIN_BASE_URL, WEIXIN_HOME_CHANNEL[_NAME], WEIXIN_ALLOWED_USERS |
| WeCom | WECOM_CALLBACK_CORP_ID, WECOM_CALLBACK_CORP_SECRET, WECOM_CALLBACK_AGENT_ID, WECOM_CALLBACK_TOKEN, WECOM_CALLBACK_ENCODING_AES_KEY, WECOM_GROUP_ROBOT_WEBHOOK, WECOM_HOME_CHANNEL[_NAME], WECOM_ALLOWED_USERS |
| BlueBubbles | BLUEBUBBLES_SERVER_URL, BLUEBUBBLES_PASSWORD, BLUEBUBBLES_HOME_CHANNEL[_NAME], BLUEBUBBLES_ALLOWED_USERS |
| Home Assistant | HASS_TOKEN, HASS_URL (HASS_HOME_CHANNEL/HASS_HOME_CHANNEL_NAME if configured) |
// Normalize a raw webhook body (any platform).
let events : Array[MessageEvent] = @imrelay.parse_inbound(@imrelay.Telegram, raw)
// Build the gateway with a history-aware agent.
let gw = @imrelay.Gateway::new(cfg, async (ev, session) => "echo: \{ev.text}")
gw.run()
// Classify send failures the way Hermes does.
let kind = @imrelay.classify_send_error("Bad Request: chat not found") // NotFound
assert_true(@imrelay.is_chat_level_not_found("chat not found"))
// Chunk a long reply at a platform's limit, preferring paragraph breaks.
let chunks = @imrelay.chunk_text(long_reply, @imrelay.Platform::max_message_length(p))moon test --target nativepub suberror UtilsError {
SendRefused(String)
MissingConfig(String)
InvalidArgument(String)
}pub(all) struct Gateway {
cfg : GatewayConfig
store : SessionStore
handler : async (MessageEvent, Session) -> String
} derive(Debug)fn Gateway::new(cfg : GatewayConfig, handler : async (MessageEvent, Session) -> String, max_history? : Int) -> Gatewaypub(all) struct GatewayConfig {
platforms : Map[Platform, PlatformConfig]
name : String
host : String
port : Int
public_url : String?
allowed_users : Array[String]
allow_all_users : Bool
} derive(Debug)fn HomeChannel::new(platform : Platform, chat_id : String, name? : String, thread_id? : String) -> HomeChannelpub(all) struct MessageEvent {
text : String
message_type : MessageType
platform : Platform
chat_id : String
user_id : String?
user_name : String?
message_id : String?
reply_to_message_id : String?
reply_to_text : String?
media : Array[MediaAttachment]
timestamp : Int64
} derive(Debug)fn MessageEvent::new(text : String, message_type : MessageType, platform : Platform, chat_id : String, user_id? : String, user_name? : String, message_id? : String, reply_to_message_id? : String, reply_to_text? : String, media? : Array[MediaAttachment], timestamp? : Int64) -> MessageEventpub(all) struct PlatformConfig {
platform : Platform
enabled : Bool
token : String?
api_key : String?
extra : Map[String, String]
home_channel : HomeChannel?
allowed_users : Array[String]
} derive(Debug)fn PlatformConfig::new(platform : Platform, enabled? : Bool, token? : String, api_key? : String, extra? : Map[String, String], home_channel? : HomeChannel, allowed_users? : Array[String]) -> PlatformConfigpub(all) struct RawHttpResult {
code : Int
headers : Map[CaseInsensitiveString, String]
body_bytes : Bytes
} derive(Debug)pub(all) struct SendResult {
success : Bool
message_id : String?
error : String?
retryable : Bool
retry_after : Double?
error_kind : SendErrorKind?
raw : String?
} derive(Debug)fn SendResult::failed(error : String, retryable? : Bool, retry_after? : Double, kind? : SendErrorKind, raw? : String) -> SendResultpub(all) struct SmtpConfig {
host : String
port : Int
user : String
password : String
tls : String
from : String
from_name : String
} derive(Debug)pub(all) struct WecomCallbackParams {
msg_signature : String
timestamp : String
nonce : String
echostr : String?
}async fn bluebubbles_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultasync fn bluebubbles_send_attachment(cfg : PlatformConfig, chat_id : String, attachment_path : String) -> SendResultfn bluebubbles_webhook_token(query : String, header : String?) -> String?fn byte_of_int(n : Int) -> Bytefn constant_time_eq(a : String, b : String) -> Boolfn data_url(mime : String, data : Bytes) -> Stringasync fn dingtalk_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultasync fn dingtalk_send_webhook(cfg : PlatformConfig, content : String, title : String) -> SendResultasync fn dingtalk_session_reply(session_webhook : String, content : String, title? : String) -> SendResultfn discord_channel_type(t : Int) -> Stringasync fn discord_edit(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResultasync fn discord_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResultfn discord_retry_after(body : String) -> Double?async fn discord_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultasync fn discord_send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String) -> SendResultasync fn discord_webhook_send(webhook_url : String, content : String, username? : String) -> SendResultasync fn edit_message(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResultfn email_build_message(from : String, from_name : String, to : String, subject : String, body : String, in_reply_to? : String, references? : String, date_ms? : Int64) -> Stringfn email_header_encode(value : String) -> Stringasync fn email_send(cfg : PlatformConfig, to : String, subject : String, body : String, in_reply_to? : String) -> SendResultfn feishu_challenge(body : String) -> String?async fn feishu_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultasync fn feishu_send_to(cfg : PlatformConfig, receive_id : String, id_type : String, content : String) -> SendResultfn format_rfc5322_date(ms : Int64) -> Stringfn hex_decode(s : String) -> Bytes?fn hmac_sha256_hex(secret : String, data : String) -> Stringasync fn homeassistant_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultfn is_chat_level_not_found(msg : String) -> Boolfn is_silent_reply(text : String) -> Boolasync fn log_line(line : String) -> Unitasync fn matrix_edit(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResultasync fn matrix_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResultasync fn matrix_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_id? : String) -> SendResultasync fn matrix_sync(cfg : PlatformConfig, token : String, since : String, timeout_ms : Int) -> Result[Json, ApiError]async fn mattermost_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultfn mattermost_webhook_token(body : String) -> String?fn md5_hex(data : BytesView) -> Stringfn or_opt(a : String?, b : String?) -> String?fn parse_command(text : String) -> (String, String)?fn parse_ms(s : String?) -> Int64async fn post_octet(url : String, body : Bytes, headers : Map[String, String]) -> Result[RawHttpResult, ApiError]fn query_param(query : String, name : String) -> String?fn random_bytes(len : Int) -> Bytesfn random_hex(len : Int) -> Stringasync fn request_json(meth : RequestMethod, url : String, body : String, headers : Map[String, String]) -> Result[Json, ApiError]async fn request_raw(meth : RequestMethod, url : String, body : Bytes, headers : Map[String, String]) -> Result[RawHttpResult, ApiError]async fn request_text(meth : RequestMethod, url : String, body : String, headers : Map[String, String]) -> Result[String, ApiError]async fn send_chunks(text : String, max_len : Int, send_one : async (String) -> SendResult) -> SendResultasync fn send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String) -> SendResultasync fn send_message(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_id? : String, context_token? : String) -> SendResultasync fn send_reaction(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResultasync fn signal_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultfn slack_challenge(body : String) -> String?async fn slack_edit(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResultasync fn slack_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResultfn slack_request_verified(secret : String, timestamp : String, body : String, signature : String, now_s : Int64, window_s? : Int64) -> Boolasync fn slack_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_ts? : String) -> SendResultfn slack_signature(secret : String, timestamp : String, body : String) -> Stringfn slack_verify_signature(secret : String, timestamp : String, body : String, signature : String) -> Boolfn smtp_dot_stuff(message : String) -> Stringfn strip_markdown(text : String) -> Stringasync fn telegram_edit(cfg : PlatformConfig, chat_id : String, message_id : String, text : String) -> SendResultasync fn telegram_get_updates(cfg : PlatformConfig, token : String, offset : Int, timeout_s : Int) -> Array[Json]async fn telegram_get_updates_result(cfg : PlatformConfig, token : String, offset : Int, timeout_s : Int) -> Result[Array[Json], ApiError]fn telegram_media_method(kind : String) -> Stringfn telegram_media_payload(chat_id : String, kind : String, url : String, caption : String?, parse_mode : String?) -> Jsonasync fn telegram_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResultasync fn telegram_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_id? : String) -> SendResultasync fn telegram_send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String, reply_to? : String) -> SendResultfn telegram_send_payload(chat_id : String, text : String, reply_to : String?, thread_id : String?, parse_mode : String?) -> Jsonasync fn telegram_set_webhook(cfg : PlatformConfig, token : String, url : String, secret_token? : String) -> SendResultfn telegram_webhook_authorized(expected : String, provided : String?) -> Boolfn truncate(text : String, max : Int, suffix? : String) -> Stringfn url_quote(s : String) -> Stringfn user_allowed(cfg : GatewayConfig, pcfg : PlatformConfig, user_id : String?, chat_id : String) -> Boolfn webhook_auth_status(platform : Platform, pcfg : PlatformConfig, request : Request, raw : String) -> (Int, String)fn wecom_aes_key(encoding_aes_key : String) -> Bytes?fn wecom_decrypt_envelope(encoding_aes_key : String, receive_id : String, encrypt : String) -> String?fn wecom_decrypt_message(token : String, encoding_aes_key : String, receive_id : String, msg_signature : String, timestamp : String, nonce : String, encrypt : String) -> String?fn wecom_encrypt_message(token : String, encoding_aes_key : String, receive_id : String, plaintext : String, timestamp : String, nonce : String) -> String?fn wecom_msg_signature(token : String, timestamp : String, nonce : String, encrypt : String) -> Stringfn wecom_pkcs7_pad(data : Bytes) -> Bytesfn wecom_pkcs7_unpad(data : Bytes) -> Bytes?async fn wecom_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultfn wecom_verify_msg_signature(token : String, timestamp : String, nonce : String, encrypt : String, msg_signature : String) -> Boolfn weixin_aes_key(key_b64 : String) -> Result[Bytes, String]fn weixin_base_url_default() -> Stringfn weixin_cdn_host_allowed(url : String) -> Boolasync fn weixin_get_updates(cfg : PlatformConfig, token : String, sync_buf : String) -> (Array[Json], String)async fn weixin_get_updates_result(cfg : PlatformConfig, token : String, sync_buf : String) -> Result[(Array[Json], String), ApiError]fn weixin_is_session_expired(ret : Int, errcode : Int, errmsg : String) -> Boolfn weixin_is_stale_session(ret : Int, errcode : Int, errmsg : String) -> Boolfn weixin_media_decrypt(ciphertext : Bytes, key : Bytes) -> Bytes raisefn weixin_media_download_url(cdn_base : String, encrypted_query_param : String) -> Stringfn weixin_media_encrypt(plaintext : Bytes, key : Bytes) -> Bytes raisefn weixin_media_item(media_type : Int, encrypted_query_param : String, aes_key_hex : String, ciphertext_size : Int, plaintext_size : Int, filename : String, rawfilemd5 : String) -> Jsonfn weixin_media_type_of(filename : String) -> Intfn weixin_media_upload_url(cdn_base : String, upload_param : String, filekey : String) -> Stringfn weixin_mime_of(filename : String) -> Stringfn weixin_pkcs7_pad(data : Bytes) -> Bytesfn weixin_pkcs7_unpad(data : Bytes) -> Bytesasync fn weixin_qr_login(cfg : PlatformConfig, on_qr : (String) -> Unit, persist : (Json) -> Unit, timeout_seconds? : Int) -> Json?async fn weixin_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, context_token? : String) -> SendResultasync fn weixin_send_media(cfg : PlatformConfig, chat_id : String, data : Bytes, filename : String, context_token? : String) -> SendResultfn weixin_send_payload(cfg : PlatformConfig, to : String, text : String, context_token : String?) -> Jsonasync fn weixin_upload_media(cfg : PlatformConfig, token : String, chat_id : String, data : Bytes, filename : String) -> Result[Json, String]fn whatsapp_retry_after(body : String) -> Double?async fn whatsapp_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResultasync fn whatsapp_send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String) -> SendResultfn whatsapp_signature(secret : String, body : String) -> Stringfn whatsapp_verify(mode : String, token : String, challenge : String, expected : String) -> String?fn whatsapp_verify_signature(secret : String, body : String, header : String) -> Boolfn xml_text(xml : String, tag : String) -> String?Install
Download zipHermes-style agent gateway in MoonBit: bridges 14 messaging platforms (Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant, Mattermost, Matrix, DingTalk, Feishu/Lark, WeChat, WeCom, iMessage via BlueBubbles) into a single agent callback, with retries, chunking, typing indicators, per-chat sessions and slash commands. TOML configuration with Hermes-compatible environment-variable fallback.