Sign in

    imrelay

    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.

    messaging
    chat
    relay
    gateway
    agent
    hermes
    telegram
    discord
    slack
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    6 days ago
    Downloads
    14

    #kupas/imrelay

    A Hermes-style agent gateway written in MoonBit — one bridge that feeds chat messages from 14 messaging platforms into your agent and delivers its replies back, with retries, chunking, typing indicators, per-chat session context and slash commands.

    The adapter layer mirrors the vocabulary of Hermes (SendResult / MessageEvent / Platform / classify_send_error / truncate_message, per-adapter env var names and MAX_MESSAGE_LENGTHs), so ports of existing Hermes plugins map onto this codebase 1:1.

    #Supported platforms

    PlatformvalueInboundSends
    Telegramtelegramwebhook + long-polltext / media / edit / delete / typing / reactions
    Discorddiscordgateway events + interactionstext / embeds / edit / delete / typing / reactions
    Slackslackevents API (signature-checked)text / edit / delete / typing / reactions
    WhatsApp (Cloud/Meta API)whatsappwebhook + hub verificationtext / media / read receipts
    Signal (signal-cli HTTP)signalreceive RPC pollingtext via JSON-RPC send
    Email (SMTP/IMAP)emailparsed raw messagesRFC 5322 messages over SMTP (STARTTLS/SSL)
    Mattermostmattermostoutgoing webhookposts via v4 API
    Matrixmatrix/sync pollingtext / edit / reactions / typing (m.* events)
    DingTalkdingtalkrobot callbackinteractive-card / webhook / app messages
    Feishu / Larkfeishuevent webhook + challengeapp messages
    WeChat (iLink Bot API)weixinlong-polliLink bot messages
    WeCom (Enterprise WeChat)wecomcallback (XML + JSON)app messages / group-robot webhook, markdown
    BlueBubbles (iMessage)bluebubblesnew-message webhooktext via AppleScript, attachments
    Home Assistanthomeassistantautomation webhooknotify / any notify.* service

    #Build & test

    moon check --target native moon build --target native moon test --target native # 131 tests

    #Run

    example/main is a runnable gateway. It reads $IMRELAY_CONFIG (or gateway.toml in the working directory); if neither exists it falls back to Hermes-style environment variables.

    IMRELAY_CONFIG=gateway.toml moon run example/main --target native

    Then:

    GET / landing page / health check GET /health {"ok":true,"platforms":N} POST /<platform> inbound webhook for that platform (per table above)

    #Configuration

    All configuration is TOML (gateway.toml); the env-var reader exists so existing Hermes deployments can migrate without rewriting secrets.

    #gateway.toml

    [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

    Per-platform keys: enabled (default true), token, api_key, home_channel, home_channel_name, home_channel_thread, allowed_users, and an extra table of string values. Any [platforms.<name>] section key not listed above goes into extra automatically (arbitrary adapter settings).

    Inbound webhook requests are authenticated before the event is parsed:

    Platformextra keyCheck
    Telegramwebhook_secretX-Telegram-Bot-Api-Secret-Token (required)
    Slacksigning_secretX-Slack-Signature + 5-minute replay window (required)
    WhatsAppapp_secretX-Hub-Signature-256 HMAC of the raw body (required)
    BlueBubblespasswordpassword query param / x-password header (when set)
    Mattermostwebhook_tokentoken in the outgoing-webhook body (when set)
    WeComcallback_token + encoding_aes_keymsg_signature SHA1 + AES-256-CBC decrypt (when set)

    Missing Telegram/Slack/WhatsApp secrets fail closed — POST /<platform> answers 503 with the configuration hint until the secret is set. WeCom callback crypto is opt-in: with encoding_aes_key set, POST /wecom verifies msg_signature, decrypts the <Encrypt> envelope (answering the GET echostr handshake the same way) and acks success; without it, bodies pass through unchanged (relay mode).

    #Environment variables

    The same config is available as env vars (used only when no TOML is present):

    PlatformVariables
    GlobalGATEWAY_ALLOWED_USERS, GATEWAY_ALLOW_ALL_USERS
    TelegramTELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, TELEGRAM_HOME_CHANNEL[_NAME], TELEGRAM_ALLOWED_USERS
    DiscordDISCORD_BOT_TOKEN, DISCORD_HOME_CHANNEL[_NAME], DISCORD_ALLOWED_USERS
    SlackSLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_SIGNING_SECRET, SLACK_HOME_CHANNEL[_NAME], SLACK_ALLOWED_USERS
    WhatsAppWHATSAPP_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
    SignalSIGNAL_HTTP_URL, SIGNAL_ACCOUNT, SIGNAL_HOME_CHANNEL[_NAME], SIGNAL_ALLOWED_USERS
    EmailEMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, EMAIL_IMAP_HOST, EMAIL_HOME_ADDRESS[_NAME], EMAIL_ALLOWED_USERS
    MattermostMATTERMOST_TOKEN, MATTERMOST_URL, MATTERMOST_WEBHOOK_TOKEN, MATTERMOST_HOME_CHANNEL[_NAME], MATTERMOST_ALLOWED_USERS
    MatrixMATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN (or MATRIX_PASSWORD + MATRIX_USER_ID), MATRIX_HOME_ROOM[_NAME], MATRIX_ALLOWED_USERS
    DingTalkDINGTALK_CLIENT_ID, DINGTALK_CLIENT_SECRET, DINGTALK_WEBHOOK_URL, DINGTALK_ROBOT_CODE, DINGTALK_HOME_CHANNEL[_NAME], DINGTALK_ALLOWED_USERS
    Feishu/LarkFEISHU_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
    WeComWECOM_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
    BlueBubblesBLUEBUBBLES_SERVER_URL, BLUEBUBBLES_PASSWORD, BLUEBUBBLES_HOME_CHANNEL[_NAME], BLUEBUBBLES_ALLOWED_USERS
    Home AssistantHASS_TOKEN, HASS_URL (HASS_HOME_CHANNEL/HASS_HOME_CHANNEL_NAME if configured)

    #How it works

    1. Inbound — a webhook server on <host>:<port> routes POST /<platform> to the matching adapter's parser, which normalizes the payload into a MessageEvent. Webhook requests are authenticated first (see the table above); handshakes are answered before that: Slack/Feishu challenge, WhatsApp hub.challenge, WeCom echostr. Telegram and WeChat additionally long-poll, and Matrix polls /sync (each in its own background task).
    2. Authorization — every event must pass allowed_users: the platform or gateway allowlist must contain the user id (or *), unless allow_all_users = true. With no allowlist configured the gate is closed (the gateway logs who was dropped, and warns at startup). A home_channel (with optional thread/name) marks where notifications go.
    3. Session — each (platform, chat_id) gets a Session with bounded history (Gateway::new(cfg, handler, max_history=24)). The agent callback async (MessageEvent, Session) -> String receives both.
    4. Commands — inbound text starting with / is intercepted first: /help, /reset, /new, /status, /platforms are answered locally; unknown commands still reach the agent.
    5. Send — a reply is delivered through the platform adapter: chunked at Platform::max_message_length, trimmed to truncate limits, retried on transient failures with backoff honoring server retry_after (Telegram FloodWait etc.), and suppressed when the handler returns a silence token ([silent], no reply, empty).
    6. Errors — every failure is a typed SendResult with SendErrorKind (TooLong/BadFormat/Forbidden/NotFound/RateLimited/Transient/Unknown) via classify_send_error, mirroring Hermes' SEND_ERROR_KINDS.

    #Library quick tour

    // 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))

    Central types (types.mbt): Platform, MessageType (Text/Command/Photo/ Video/Audio/Voice/Document/Sticker/Location), SendErrorKind, SendResult, HomeChannel, ChatInfo, MediaAttachment, MessageEvent. Supporting modules: http.mbt (typed ApiError, retry/backoff), session.mbt (Session, SessionStore, parse_command/command_reply/ is_silent_reply), config.mbt (GatewayConfig, PlatformConfig, TOML + env loaders), dispatch.mbt (parse_inbound, send_message, platform_configured), gateway.mbt (Gateway, HubParams).

    #Testing

    imrelay_test.mbt (core types / session / config) and platforms_test.mbt (payload builders, inbound parsers, Slack signature vectors, WhatsApp retry_after, WeCom crypto — official WXBizMsgCrypt vector, dispatch routing per adapter) — 131 tests:

    moon test --target native

    #Platform notes

    • BlueBubbles (iMessage) — built in as a first-class adapter: it talks directly to the REST API of your own BlueBubbles server ({server_url}/api/v1, GET /ping, POST /message/text, POST /chat/query, password-authenticated), and receives inbound messages from that server's new-message webhook ({"type":"new-message","data":…}). You only need to run a BlueBubbles server on your Mac — the same way Signal needs your own signal-cli HTTP daemon and Telegram can use a self-hosted Bot API base. Messages use chats[].guid as the chat id.
    • WeChat — via Tencent's official iLink Bot API (base_url default https://ilinkai.weixin.qq.com, long-polled): you scan a QR to bind a bot identity, and only direct messages are reliably delivered — iLink bots typically cannot join regular group chats. Media uploads (encrypted CDN) and the QR setup wizard are not ported; the adapter covers text send, typing and long-poll receive.
    • WhatsApp: Cloud Business API over Graph (graph.facebook.com), with hub verification for webhook subscriptions.
    • DingTalk / WeCom / Feishu: keys/secret pairs for the official app APIs; DingTalk and WeCom also support static inbound-robot webhooks. WeCom callback mode additionally implements the official WXBizMsgCrypt msg_signature + AES-256-CBC verification and decryption (callback_token
      • encoding_aes_key), plus the GET echostr handshake.

    UtilsError

    pub suberror UtilsError {
    SendRefused(String)
    MissingConfig(String)
    InvalidArgument(String)
    }

    Raised when a required field is missing or a send was refused.

    ApiError

    pub(all) struct ApiError {
    code : Int
    body : String
    } derive(
    Debug
    )

    A failed raw HTTP exchange.

    ApiError::http

    fn ApiError::http(code : Int, body : String) -> ApiError

    An HTTP-level failure (code is the status code).

    ApiError::is_transient

    fn ApiError::is_transient(self : ApiError) -> Bool

    ApiError::network

    fn ApiError::network(body : String) -> ApiError

    ApiError::to_repr

    ChatInfo

    pub(all) struct ChatInfo {
    name : String
    chat_type : String
    chat_id : String
    } derive(
    Debug
    )

    Static info about a chat, mirroring get_chat_info results.

    ChatInfo::to_repr

    Gateway

    pub(all) struct Gateway {
    cfg : GatewayConfig
    store : SessionStore
    handler : async (MessageEvent, Session) -> String
    } derive(
    Debug
    )

    The agent callback. Receives the inbound event and returns the reply text; an empty string (or a silence token) suppresses delivery.

    Gateway::handle_event

    async fn Gateway::handle_event(self : Gateway, ev : MessageEvent) -> Unit

    Handle one inbound event: record the turn, answer slash commands locally, otherwise call the agent and deliver the reply.

    Gateway::new

    fn Gateway::new(cfg : GatewayConfig, handler : async (MessageEvent, Session) -> String, max_history? : Int) -> Gateway

    Build a gateway over cfg with handler as the agent.

    Gateway::run

    async fn Gateway::run(self : Gateway) -> Unit

    Start the gateway: an HTTP webhook server plus a long-poll loop for every platform configured for polling. Runs until the task group is cancelled.

    Gateway::to_repr

    GatewayConfig

    pub(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
    )

    The whole gateway: one config per platform plus process-wide settings.

    GatewayConfig::default_config

    fn GatewayConfig::default_config() -> GatewayConfig

    GatewayConfig::enabled_platforms

    fn GatewayConfig::enabled_platforms(self : GatewayConfig) -> Array[Platform]

    Which platforms ended up enabled (Hermes get_connected_platforms()).

    GatewayConfig::from_env

    fn GatewayConfig::from_env() -> GatewayConfig

    Build from the process environment, Hermes-style: a platform is enabled when its required credentials are present (all-of; Matrix accepts the access token or a password).

    GatewayConfig::from_toml

    fn GatewayConfig::from_toml(text : String) -> GatewayConfig raise

    Parse a gateway.toml document into a GatewayConfig.

    GatewayConfig::from_toml_file

    async fn GatewayConfig::from_toml_file(path : String) -> GatewayConfig

    Read and parse a TOML config file.

    GatewayConfig::get

    fn GatewayConfig::get(self : GatewayConfig, platform : Platform) -> PlatformConfig?

    GatewayConfig::has_any_allowed_users

    fn GatewayConfig::has_any_allowed_users(self : GatewayConfig) -> Bool

    Whether any allowlist (gateway-wide or per platform) is configured.

    GatewayConfig::load

    fn GatewayConfig::load(toml_text : String?) -> GatewayConfig raise

    Build the gateway from a single TOML document when available, otherwise from the process environment (Hermes-compatible variable names).

    GatewayConfig::load_default

    async fn GatewayConfig::load_default() -> GatewayConfig

    Load configuration the way the gateway binary wants it: $IMRELAY_CONFIG (or gateway.toml in the working directory) when that file exists, otherwise the Hermes-style environment variables.

    GatewayConfig::webhook_url

    fn GatewayConfig::webhook_url(self : GatewayConfig, path : String) -> String?

    Webhook callback URL for a platform, e.g. https://host/telegram.

    HomeChannel

    pub(all) struct HomeChannel {
    platform : Platform
    chat_id : String
    name : String
    thread_id : String?
    } derive(
    Debug
    )

    Default delivery target for a platform (Hermes HomeChannel): used by cron/notification delivery when no explicit chat is named.

    HomeChannel::new

    fn HomeChannel::new(platform : Platform, chat_id : String, name? : String, thread_id? : String) -> HomeChannel

    HubParams

    pub(all) struct HubParams {
    mode : String
    token : String
    challenge : String
    } derive(
    Debug
    )

    WhatsApp webhook verification parameters (hub.* query keys).

    MediaAttachment

    pub(all) struct MediaAttachment {
    url : String
    kind : String
    } derive(
    Debug
    )

    A media attachment carried by an inbound MessageEvent.

    MessageEvent

    pub(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
    )

    Normalized inbound message (Hermes MessageEvent). Adapters on the receive side produce one of these; this SDK consumes them for send-side routing (e.g. replying in the same thread).

    MessageEvent::get_command

    fn MessageEvent::get_command(self : MessageEvent) -> String?

    The command token (without the leading slash or @bot suffix), if any.

    MessageEvent::is_command

    fn MessageEvent::is_command(self : MessageEvent) -> Bool

    true for /command-style inbound text.

    MessageEvent::new

    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) -> MessageEvent

    MessageType

    pub(all) enum MessageType {
    Text
    Location
    Photo
    Video
    Audio
    Voice
    Document
    Sticker
    Command
    } derive(Eq,
    Debug
    )

    Inbound message kinds, mirroring gateway/platforms/event.py::MessageType.

    MessageType::equal

    fn MessageType::equal(MessageType, MessageType) -> Bool

    MessageType::not_equal

    fn MessageType::not_equal(x : MessageType, y : MessageType) -> Bool

    MessageType::value

    fn MessageType::value(self : MessageType) -> String

    Platform

    pub(all) enum Platform {
    Telegram
    Discord
    Slack
    Whatsapp
    Signal
    Email
    Mattermost
    Matrix
    Dingtalk
    Feishu
    Weixin
    Wecom
    Bluebubbles
    HomeAssistant
    } derive(Eq, Hash,
    Debug
    )

    The messaging platforms this SDK can talk to. Values follow Hermes' Platform enum (gateway/config.py) and the plugin directory names.

    Platform::equal

    fn Platform::equal(Platform, Platform) -> Bool

    Platform::hash

    fn Platform::hash(self : Platform) -> Int

    Platform::hash_combine

    fn Platform::hash_combine(Platform, Hasher) -> Unit

    Platform::max_message_length

    fn Platform::max_message_length(self : Platform) -> Int

    Maximum characters the platform accepts in one message; sends are chunked at this boundary (Hermes' per-adapter MAX_MESSAGE_LENGTH).

    Platform::name

    fn Platform::name(self : Platform) -> String

    Human-readable name (Hermes Platform display names).

    Platform::not_equal

    fn Platform::not_equal(x : Platform, y : Platform) -> Bool

    Platform::of_value

    fn Platform::of_value(s : String) -> Platform?

    Platform::to_repr

    Platform::value

    fn Platform::value(self : Platform) -> String

    Hermes canonical value (the Platform(...) enum value / env-prefix base).

    PlatformConfig

    pub(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
    )

    Per-platform settings, mirroring Hermes' PlatformConfig.

    PlatformConfig::api_key_of

    fn PlatformConfig::api_key_of(self : PlatformConfig) -> String?

    Non-empty api_key, if configured.

    PlatformConfig::bearer

    fn PlatformConfig::bearer(self : PlatformConfig) -> String?

    The credential an adapter should authenticate with: token first, then api_key (Hermes treats both as the platform bearer).

    PlatformConfig::extra_bool

    fn PlatformConfig::extra_bool(self : PlatformConfig, key : String, default : Bool) -> Bool

    Read a boolean from [platform.extra] (true/1/yes).

    PlatformConfig::extra_first

    fn PlatformConfig::extra_first(self : PlatformConfig, keys : Array[String]) -> String?

    First non-empty value among keys in [platform.extra].

    PlatformConfig::extra_int

    fn PlatformConfig::extra_int(self : PlatformConfig, key : String, default : Int) -> Int

    Read an integer from [platform.extra].

    PlatformConfig::extra_str

    fn PlatformConfig::extra_str(self : PlatformConfig, key : String, default : String) -> String

    Read a string from [platform.extra] (or the legacy flat section).

    PlatformConfig::get

    fn PlatformConfig::get(self : PlatformConfig, key : String) -> String?

    PlatformConfig::get_or

    fn PlatformConfig::get_or(self : PlatformConfig, key : String, default : String) -> String

    PlatformConfig::new

    fn PlatformConfig::new(platform : Platform, enabled? : Bool, token? : String, api_key? : String, extra? : Map[String, String], home_channel? : HomeChannel, allowed_users? : Array[String]) -> PlatformConfig

    PlatformConfig::require

    fn PlatformConfig::require(self : PlatformConfig, key : String, what : String) -> String raise

    PlatformConfig::token_of

    fn PlatformConfig::token_of(self : PlatformConfig) -> String?

    Non-empty token, if configured.

    RawHttpResult

    pub(all) struct RawHttpResult {
    code : Int
    headers : Map[
    CaseInsensitiveString
    , String]
    body_bytes : Bytes
    } derive(
    Debug
    )

    Raw response: status code, response headers and the body as bytes.

    SendErrorKind

    pub(all) enum SendErrorKind {
    TooLong
    BadFormat
    Forbidden
    NotFound
    RateLimited
    Transient
    Unknown
    } derive(Eq,
    Debug
    )

    Platform-neutral send-failure kinds, mirroring Hermes' SEND_ERROR_KINDS (gateway/platforms/base.py). Consumers branch on these instead of substring-matching error.

    SendErrorKind::equal

    SendErrorKind::not_equal

    fn SendErrorKind::not_equal(x : SendErrorKind, y : SendErrorKind) -> Bool

    SendErrorKind::value

    fn SendErrorKind::value(self : SendErrorKind) -> String

    SendResult

    pub(all) struct SendResult {
    success : Bool
    message_id : String?
    error : String?
    retryable : Bool
    retry_after : Double?
    error_kind : SendErrorKind?
    raw : String?
    } derive(
    Debug
    )

    Result of a send, mirroring gateway/platforms/base.py::SendResult.

    SendResult::failed

    fn SendResult::failed(error : String, retryable? : Bool, retry_after? : Double, kind? : SendErrorKind, raw? : String) -> SendResult

    SendResult::ok

    fn SendResult::ok(message_id? : String, raw? : String) -> SendResult

    SendResult::unwrap

    fn SendResult::unwrap(self : SendResult) -> String raise

    Raise on failure, return message_id otherwise. Handy for call sites that treat a refused send as an error.

    Session

    pub(all) struct Session {
    platform : Platform
    chat_id : String
    history : Array[(String, String)]
    state : Map[String, String]
    max_history : Int
    } derive(
    Debug
    )

    One conversation. history and state are reference types, so mutating a session obtained from a SessionStore mutates the stored session.

    Session::context_token

    fn Session::context_token(self : Session) -> String?

    The stored iLink context_token, if any.

    Session::last_message_id

    fn Session::last_message_id(self : Session) -> String?

    The last message id this gateway sent, if any.

    Session::messages_json

    fn Session::messages_json(self : Session) -> Json

    Render the history as an OpenAI-style messages array.

    Session::push

    fn Session::push(self : Session, role : String, content : String) -> Unit

    Append a turn, trimming the window to max_history.

    Session::reset

    fn Session::reset(self : Session) -> Unit

    Drop all history for this session.

    Session::set_context_token

    fn Session::set_context_token(self : Session, token : String) -> Unit

    Remember the WeChat iLink context_token so replies stay in session.

    Session::set_last_message_id

    fn Session::set_last_message_id(self : Session, id : String) -> Unit

    Record the id of the last message this gateway sent (reply target).

    Session::set_thread_id

    fn Session::set_thread_id(self : Session, id : String) -> Unit

    Remember a thread id for follow-up sends.

    Session::thread_id

    fn Session::thread_id(self : Session) -> String?

    The stored thread id, if any.

    Session::to_repr

    SessionStore

    pub(all) struct SessionStore {
    sessions : Map[String, Session]
    max_history : Int
    } derive(
    Debug
    )

    Session registry keyed by platform:chat_id.

    SessionStore::len

    fn SessionStore::len(self : SessionStore) -> Int

    Number of tracked sessions.

    SessionStore::new

    fn SessionStore::new(max_history? : Int) -> SessionStore

    SessionStore::session

    fn SessionStore::session(self : SessionStore, platform : Platform, chat_id : String) -> Session

    Fetch (or create) the session for a chat.

    SmtpConfig

    pub(all) struct SmtpConfig {
    host : String
    port : Int
    user : String
    password : String
    tls : String
    from : String
    from_name : String
    } derive(
    Debug
    )

    SMTP settings resolved from [email.extra] (Hermes EMAIL_* keys).

    WecomCallbackParams

    pub(all) struct WecomCallbackParams {
    msg_signature : String
    timestamp : String
    nonce : String
    echostr : String?
    }

    Query parameters of a WeCom callback request (percent-decoded); the GET URL-verification handshake additionally carries echostr.

    api_error_message

    fn api_error_message(e : ApiError) -> String

    Human-readable message for an ApiError (negative code = transport error).

    bearer_headers

    fn bearer_headers(token : String) -> Map[String, String]

    JSON headers plus Authorization: Bearer <token>.

    bluebubbles_api_base

    fn bluebubbles_api_base(cfg : PlatformConfig) -> String

    API root ({server_url}/api/v1).

    bluebubbles_get_chat

    async fn bluebubbles_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Chat metadata (POST /chat/query filtered by guid).

    bluebubbles_parse_event

    fn bluebubbles_parse_event(body : String) -> Array[MessageEvent]

    Parse a BlueBubbles new-message webhook into events. Self-sent messages (isFromMe) are filtered to avoid echo loops.

    bluebubbles_ping

    async fn bluebubbles_ping(cfg : PlatformConfig) -> Bool

    Server health (GET /ping) — also validates the password.

    bluebubbles_send

    async fn bluebubbles_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content, chunked at iMessage's practical 4000-char limit.

    bluebubbles_send_attachment

    async fn bluebubbles_send_attachment(cfg : PlatformConfig, chat_id : String, attachment_path : String) -> SendResult

    Send a media attachment by local file path.

    bluebubbles_send_payload

    fn bluebubbles_send_payload(chat_guid : String, message : String, reply_to : String?) -> Json

    POST /message/text payload.

    bluebubbles_typing

    async fn bluebubbles_typing(cfg : PlatformConfig, chat_id : String) -> Unit

    Typing indicator (POST /chat/{guid}/typing).

    bluebubbles_url

    fn bluebubbles_url(cfg : PlatformConfig, path : String) -> String

    Append the password query parameter to an endpoint.

    bluebubbles_webhook_token

    fn bluebubbles_webhook_token(query : String, header : String?) -> String?

    Webhook credential from a BlueBubbles POST: the password query parameter (BlueBubbles appends it to registered webhook URLs) or the x-password header (Hermes _webhook_token).

    bot_headers

    fn bot_headers(token : String) -> Map[String, String]

    JSON headers plus Authorization: Bot <token> (Discord style).

    byte_of_int

    fn byte_of_int(n : Int) -> Byte

    Byte from an int (0..255) — MoonBit has no Byte::from_int and int-literal method chains parse as floats.

    chunk_text

    fn chunk_text(text : String, max_len : Int) -> Array[String]

    Split text into chunks no longer than max_len characters, breaking on paragraph (blank-line) boundaries first, then on character boundaries. Returns at least one chunk even when the input is empty.

    classify_send_error

    fn classify_send_error(msg : String) -> SendErrorKind

    Hermes' SEND_ERROR_KINDS classifier: map an error string onto a platform-neutral kind. Substring table mirrors gateway/platforms/base.py (_SEND_ERROR_CLASSIFIERS) so classified failures read identically.

    command_reply

    fn command_reply(command : (String, String), cfg : GatewayConfig, session : Session) -> String?

    Answer a built-in slash command locally. Returns None for commands the agent should handle (and for plain messages).

    constant_time_eq

    fn constant_time_eq(a : String, b : String) -> Bool

    Constant-time string comparison (no early exit; length differences are absorbed into the accumulated difference).

    data_url

    fn data_url(mime : String, data : Bytes) -> String

    data: URL carrying the attachment bytes inline — the MoonBit equivalent of Hermes' cached media paths for self-contained events.

    delete_empty

    async fn delete_empty(url : String, headers : Map[String, String]) -> Result[String, ApiError]

    DELETE an endpoint with no body.

    delete_json

    async fn delete_json(url : String, payload : Json, headers : Map[String, String]) -> Result[String, ApiError]

    DELETE an endpoint (many platforms accept an optional JSON body).

    delete_message

    async fn delete_message(cfg : PlatformConfig, chat_id : String, message_id : String) -> SendResult

    Delete a message, where the platform supports it.

    dingtalk_access_token

    async fn dingtalk_access_token(cfg : PlatformConfig) -> String?

    Fetch (and cache) an app-robot access token.

    dingtalk_app_key

    fn dingtalk_app_key(cfg : PlatformConfig) -> String?

    App credentials (DINGTALK_CLIENT_ID + DINGTALK_CLIENT_SECRET).

    dingtalk_app_secret

    fn dingtalk_app_secret(cfg : PlatformConfig) -> String?

    App secret, also accepted as api_key.

    dingtalk_parse_callback

    fn dingtalk_parse_callback(body : String) -> Array[MessageEvent]

    Parse a DingTalk robot callback body into a MessageEvent. The sessionWebhook is preserved in reply_to_text so the caller can answer without a separate token.

    dingtalk_robot_token

    fn dingtalk_robot_token(cfg : PlatformConfig) -> String?

    Robot webhook access token (DINGTALK_ACCESS_TOKEN / [dingtalk.extra]).

    dingtalk_send

    async fn dingtalk_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content. Uses the app-robot API when app credentials are present, otherwise falls back to the group robot webhook.

    dingtalk_send_payload

    fn dingtalk_send_payload(content : String, title : String, use_markdown : Bool) -> Json

    robot/send payload (robot webhook uses markdown, app uses text).

    dingtalk_send_webhook

    async fn dingtalk_send_webhook(cfg : PlatformConfig, content : String, title : String) -> SendResult

    Send through the group robot webhook.

    dingtalk_session_reply

    async fn dingtalk_session_reply(session_webhook : String, content : String, title? : String) -> SendResult

    Post to a fresh sessionWebhook (used to reply to an inbound callback).

    discord_api_base

    fn discord_api_base(cfg : PlatformConfig) -> String

    API root, overridable with [discord.extra] api_base.

    discord_channel_type

    fn discord_channel_type(t : Int) -> String

    Discord channel.type → a readable chat type.

    discord_command_text

    fn discord_command_text(name : String, data : Json) -> String

    Render a slash command plus its options as /name option:value ….

    discord_delete

    async fn discord_delete(cfg : PlatformConfig, chat_id : String, message_id : String) -> SendResult

    Delete a message.

    discord_edit

    async fn discord_edit(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResult

    Edit a message (PATCH /channels/{id}/messages/{mid}).

    discord_get_chat

    async fn discord_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Channel metadata (GET /channels/{id}).

    discord_media_payload

    fn discord_media_payload(kind : String, url : String, caption : String?) -> Json

    Embed payload for a media URL (avoids multipart uploads).

    discord_parse_interaction

    fn discord_parse_interaction(body : String) -> Array[MessageEvent]

    Parse a Discord interaction webhook: PING (type 1) / application command (type 2). Returns the events plus the PONG body when a handshake arrived.

    discord_parse_message_event

    fn discord_parse_message_event(body : String) -> Array[MessageEvent]

    Parse a normalized MESSAGE_CREATE payload (Discord gateway relay): {"t":"MESSAGE_CREATE","d":{"channel_id":…,"content":…,"author":{…},"id":…}} or a bare d object.

    discord_react

    async fn discord_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResult

    Add a reaction (PUT …/reactions/{emoji}/@me).

    discord_retry_after

    fn discord_retry_after(body : String) -> Double?

    Extract retry_after seconds from a Discord 429 body.

    discord_send

    async fn discord_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content, chunked at Discord's 2000-char limit.

    discord_send_media

    async fn discord_send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String) -> SendResult

    Send a media URL inside an embed.

    discord_send_payload

    fn discord_send_payload(content : String, reply_to : String?) -> Json

    POST /channels/{id}/messages payload.

    discord_typing

    async fn discord_typing(cfg : PlatformConfig, chat_id : String) -> Unit

    Typing indicator (POST /channels/{id}/typing, no content).

    discord_webhook_send

    async fn discord_webhook_send(webhook_url : String, content : String, username? : String) -> SendResult

    Normalized outbound webhook send (Discord webhook URL with ?wait=true).

    edit_message

    async fn edit_message(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResult

    Edit a previously sent message, where the platform supports it.

    email_build_message

    fn email_build_message(from : String, from_name : String, to : String, subject : String, body : String, in_reply_to? : String, references? : String, date_ms? : Int64) -> String

    Compose an RFC 5322 message.

    email_header_encode

    fn email_header_encode(value : String) -> String

    Encode a header value: ASCII passes through, anything else becomes a base64 "encoded-word" (RFC 2047).

    email_parse_message

    fn email_parse_message(raw : String) -> MessageEvent?

    Parse a raw RFC 5322 message into a MessageEvent (From becomes the chat id, so replies go back to the sender). Reply-To wins over From when present, matching normal mailing-list behaviour.

    email_send

    async fn email_send(cfg : PlatformConfig, to : String, subject : String, body : String, in_reply_to? : String) -> SendResult

    Deliver body as an email to to. subject defaults to [email.extra] default_subject.

    email_smtp_config

    fn email_smtp_config(cfg : PlatformConfig) -> SmtpConfig

    Resolve SMTP settings, inferring the TLS mode from the port when unset.

    feishu_api_base

    fn feishu_api_base(cfg : PlatformConfig) -> String

    Open-platform root, overridable with [feishu.extra] api_base (https://open.larksuite.com/open-apis for Lark).

    feishu_app_id

    fn feishu_app_id(cfg : PlatformConfig) -> String

    App id (FEISHU_APP_ID).

    feishu_app_secret

    fn feishu_app_secret(cfg : PlatformConfig) -> String?

    App secret (FEISHU_APP_SECRET), also accepted as token.

    feishu_challenge

    fn feishu_challenge(body : String) -> String?

    Event-callback URL verification challenge.

    feishu_get_chat

    async fn feishu_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Chat metadata (GET /im/v1/chats/{chat_id}).

    feishu_message_text

    fn feishu_message_text(message : Json) -> String

    Extract the text body from a Feishu message (JSON-encoded content).

    feishu_parse_event

    fn feishu_parse_event(body : String) -> Array[MessageEvent]

    Parse a Feishu event callback (im.message.receive_v1) into events. Messages sent by the bot itself and plain text-less messages are skipped.

    feishu_reply

    async fn feishu_reply(cfg : PlatformConfig, message_id : String, content : String) -> SendResult

    Reply in-thread to a specific message.

    feishu_send

    async fn feishu_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send to a chat (receive_id_type=chat_id).

    feishu_send_to

    async fn feishu_send_to(cfg : PlatformConfig, receive_id : String, id_type : String, content : String) -> SendResult

    Post to /im/v1/messages. id_type is chat_id or open_id.

    feishu_tenant_token

    async fn feishu_tenant_token(cfg : PlatformConfig) -> String?

    Fetch (and cache) a tenant access token.

    format_rfc5322_date

    fn format_rfc5322_date(ms : Int64) -> String

    RFC 5322 Date: header for an epoch-millisecond timestamp.

    get_bytes

    async fn get_bytes(url : String, headers : Map[String, String]) -> Result[Bytes, ApiError]

    GET a URL and return the raw body bytes on 2xx.

    get_chat_info

    async fn get_chat_info(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Chat metadata, where the platform exposes it.

    get_json

    async fn get_json(url : String, headers : Map[String, String]) -> Result[Json, ApiError]

    GET a JSON endpoint; returns the decoded JSON body on 2xx.

    graph_error_message

    fn graph_error_message(j : Json) -> String?

    true when s looks like a WhatsApp/Graph error payload.

    headers_of

    fn headers_of(headers : Map[String, String]) -> Map[
    CaseInsensitiveString
    , String]

    Convert a plain header map into the case-insensitive map the async HTTP client expects.

    hex_decode

    fn hex_decode(s : String) -> Bytes?

    Decode a lowercase/uppercase hex string into bytes; None on malformed input (odd length or non-hex digit).

    hex_encode

    fn hex_encode(data : BytesView) -> String

    Lowercase hex encoding of raw bytes.

    hex_lower

    fn hex_lower(b : ArrayView[Byte]) -> String

    Lowercase hex encoding of b.

    hmac_sha256_hex

    fn hmac_sha256_hex(secret : String, data : String) -> String

    Lowercase hex HMAC-SHA256 of data under secret (Slack/WhatsApp webhook signatures).

    homeassistant_domain

    fn homeassistant_domain(cfg : PlatformConfig) -> String

    Notify domain, e.g. notify ([homeassistant.extra] notify_domain).

    homeassistant_get_chat

    async fn homeassistant_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Entity state (GET /api/states/{entity_id}).

    homeassistant_parse_event

    fn homeassistant_parse_event(body : String) -> Array[MessageEvent]

    Parse an inbound automation webhook: {"text":"…","chat_id":"…","user_id":"…"} (or message for text).

    homeassistant_send

    async fn homeassistant_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content through the HA service API.

    homeassistant_send_payload

    fn homeassistant_send_payload(message : String, title : String?, data : Json?) -> Json

    services/{domain}/{service} payload.

    homeassistant_service

    fn homeassistant_service(cfg : PlatformConfig, chat_id : String) -> String

    Service name; falls back to the chat id (a notify service or entity id).

    homeassistant_url

    fn homeassistant_url(cfg : PlatformConfig) -> String

    HA base URL (HASS_URL).

    is_chat_level_not_found

    fn is_chat_level_not_found(msg : String) -> Bool

    Hermes' is_chat_level_not_found: whether a not_found failure means the whole chat is gone (only that marks a target dead; a deleted topic or an edited-away message leaves the parent chat reachable). When both kinds of marker are present the sub-chat reading wins.

    is_silent_reply

    fn is_silent_reply(text : String) -> Bool

    Hermes' silence contract: a reply equal to one of these markers is suppressed instead of sent (§"silent" tokens). Case-insensitive.

    json_field_array

    fn json_field_array(obj : Json, key : String) -> Array[Json]?

    Read an array field.

    json_field_bool

    fn json_field_bool(obj : Json, key : String) -> Bool?

    Read a boolean field from a JSON object.

    json_field_int

    fn json_field_int(obj : Json, key : String) -> Int?

    Read a numeric field from a JSON object as Int.

    json_field_obj

    fn json_field_obj(obj : Json, key : String) -> Json?

    Read a nested object field.

    json_field_string

    fn json_field_string(obj : Json, key : String) -> String?

    Read a string field from a JSON object.

    json_headers

    fn json_headers() -> Map[String, String]

    Content-Type: application/json header map.

    json_id

    fn json_id(obj : Json, key : String) -> String?

    Read a string-or-number field as a string id (platform payloads often mix the two, e.g. Telegram chat.id).

    json_obj

    fn json_obj(fields : Array[(String, Json)]) -> Json

    Build a JSON object from explicit fields.

    log_line

    async fn log_line(line : String) -> Unit

    Write one log line to stderr, flushed immediately. Logs must not share stdout with protocol output (the moon test driver parses test results from stdout), and stderr is unbuffered even when stdout is a pipe.

    matrix_api_base

    fn matrix_api_base(cfg : PlatformConfig) -> String

    /_matrix/client/v3 root.

    matrix_edit

    async fn matrix_edit(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResult

    Edit a message with m.replace.

    matrix_edit_payload

    fn matrix_edit_payload(new_text : String, message_id : String) -> Json

    m.replace edit payload.

    matrix_get_chat

    async fn matrix_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Room name via GET /rooms/{roomId}/state/m.room.name.

    matrix_headers

    fn matrix_headers(token : String) -> Map[String, String]

    Authorization: Bearer headers for Matrix.

    matrix_homeserver

    fn matrix_homeserver(cfg : PlatformConfig) -> String

    Homeserver base URL (MATRIX_HOMESERVER), without a trailing slash.

    matrix_parse_event

    fn matrix_parse_event(room_id : String, ev : Json, self_user? : String) -> MessageEvent?

    Convert one room event into a MessageEvent.

    matrix_parse_sync

    fn matrix_parse_sync(body : String, self_user? : String) -> Array[MessageEvent]

    Parse a /sync response (or a bare timeline) into message events. The caller's own user id (self_user) is echoed out.

    matrix_react

    async fn matrix_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResult

    React with m.annotation.

    matrix_reaction_payload

    fn matrix_reaction_payload(message_id : String, emoji : String) -> Json

    m.annotation reaction payload.

    matrix_resolve_token

    async fn matrix_resolve_token(cfg : PlatformConfig) -> String?

    Resolve the access token, logging in when only a password is configured.

    matrix_self_user

    async fn matrix_self_user(cfg : PlatformConfig, token : String) -> String

    The bot's own @user:server id — from the login/typing cache or /account/whoami, falling back to the configured user_id.

    matrix_send

    async fn matrix_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_id? : String) -> SendResult

    Send content to a room, chunked at Matrix's 60000-char limit.

    matrix_send_payload

    fn matrix_send_payload(body : String, reply_to : String?) -> Json

    PUT …/send/m.room.message/{txnId} payload.

    matrix_sync

    async fn matrix_sync(cfg : PlatformConfig, token : String, since : String, timeout_ms : Int) -> Result[Json, ApiError]

    GET /_matrix/client/v3/sync, long-polling for up to timeout_ms.

    matrix_typing

    async fn matrix_typing(cfg : PlatformConfig, chat_id : String) -> Unit

    Typing indicator (PUT /rooms/{room}/typing/{userId}).

    mattermost_api_base

    fn mattermost_api_base(cfg : PlatformConfig) -> String

    API root ({url}/api/v4), overridable with [mattermost.extra] api_base.

    mattermost_get_chat

    async fn mattermost_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Channel metadata (GET /channels/{id}).

    mattermost_parse_webhook

    fn mattermost_parse_webhook(body : String) -> Array[MessageEvent]

    Parse a Mattermost outgoing-webhook body into a MessageEvent.

    mattermost_send

    async fn mattermost_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content, chunked at Mattermost's 16383-char limit.

    mattermost_send_payload

    fn mattermost_send_payload(channel_id : String, message : String, reply_to : String?) -> Json

    POST /posts payload (mentions disabled, optional thread root).

    mattermost_webhook_token

    fn mattermost_webhook_token(body : String) -> String?

    The token carried by a Mattermost outgoing-webhook body (configured on the outgoing webhook in Mattermost; compared with [mattermost.extra]webhook_token when set).

    md5_hex

    fn md5_hex(data : BytesView) -> String

    Lowercase hex MD5 digest (WeChat/WeCom media fingerprints).

    missing_config

    fn missing_config(platform : Platform, what : String) -> SendResult

    SendResult for a missing credential.

    now_ms

    fn now_ms() -> Int64

    Current epoch time in milliseconds.

    obj_with_opt

    fn obj_with_opt(obj : Json, key : String, value : String?) -> Json

    Add key to obj only when value is Some (absent keys stay absent — platforms reject null where a field must not exist).

    or_opt

    fn or_opt(a : String?, b : String?) -> String?

    First present of two optional strings (a when it is Some).

    parse_command

    fn parse_command(text : String) -> (String, String)?

    Parsed slash command: (name, argument tail), lower-cased name and no leading slash.

    parse_inbound

    fn parse_inbound(platform : Platform, body : String) -> Array[MessageEvent]

    Parse a webhook body into normalized events for platform. Unknown platforms and malformed bodies yield an empty array.

    parse_ms

    fn parse_ms(s : String?) -> Int64

    Parse a platform timestamp (seconds or milliseconds) into epoch ms.

    patch_json

    async fn patch_json(url : String, payload : Json, headers : Map[String, String]) -> Result[Json, ApiError]

    PATCH payload as JSON; returns the decoded JSON body on 2xx.

    platform_configured

    fn platform_configured(cfg : PlatformConfig) -> Bool

    true when the platform has enough configuration to be usable.

    post_json

    async fn post_json(url : String, payload : Json, headers : Map[String, String]) -> Result[Json, ApiError]

    POST payload as JSON; returns the decoded JSON body on 2xx.

    post_octet

    async fn post_octet(url : String, body : Bytes, headers : Map[String, String]) -> Result[RawHttpResult, ApiError]

    POST an opaque byte body (Content-Type: application/octet-stream) and return the raw result — Web/WeChat CDN uploads return the confirmation in a response header rather than the body.

    post_text

    async fn post_text(url : String, payload : Json, headers : Map[String, String]) -> Result[String, ApiError]

    POST a JSON payload and return the raw text body (XML/plain platforms).

    put_json

    async fn put_json(url : String, payload : Json, headers : Map[String, String]) -> Result[Json, ApiError]

    PUT payload as JSON; returns the decoded JSON body on 2xx.

    query_param

    fn query_param(query : String, name : String) -> String?

    Read a name=value pair from a URL query string (percent-decoded).

    random_bytes

    fn random_bytes(len : Int) -> Bytes

    Cryptographically-ish random bytes via random_hex (transaction ids).

    random_hex

    fn random_hex(len : Int) -> String

    Random lowercase-hex string of len digits (transaction ids, uuids).

    request_json

    async fn request_json(meth :
    RequestMethod
    , url : String, body : String, headers : Map[String, String]) -> Result[Json, ApiError]

    Perform one HTTP exchange and decode the response body as JSON. An empty 2xx body decodes to Json::null().

    request_raw

    async fn request_raw(meth :
    RequestMethod
    , url : String, body : Bytes, headers : Map[String, String]) -> Result[RawHttpResult, ApiError]

    One HTTP exchange returning status + headers + raw body bytes (the WeChat CDN paths need the x-encrypted-param response header, which the text/json helpers drop).

    request_text

    async fn request_text(meth :
    RequestMethod
    , url : String, body : String, headers : Map[String, String]) -> Result[String, ApiError]

    Perform one HTTP exchange and return the raw response body text on 2xx.

    result_object

    fn result_object(res : Result[Json, ApiError], prefix : String) -> Result[Json, SendResult]

    Pull a nested object out of result (e.g. Telegram {"ok":true,"result":{…}}).

    send_body_failure

    fn send_body_failure(prefix : String, body : String) -> SendResult

    Build a failure from a platform-level error string (HTTP 200 + error body).

    send_chunks

    async fn send_chunks(text : String, max_len : Int, send_one : async (String) -> SendResult) -> SendResult

    Deliver text in platform-sized chunks, calling send_one per chunk and stopping at the first failure. A successful single-chunk send returns the chunk's SendResult; multi-chunk sends return the last success with the first chunk's id preserved.

    send_failure

    fn send_failure(prefix : String, e : ApiError) -> SendResult

    Map a failed HTTP exchange onto a classified SendResult.

    send_media

    async fn send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String) -> SendResult

    Send a media URL (or, for BlueBubbles, a local path).

    send_message

    async fn send_message(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_id? : String, context_token? : String) -> SendResult

    Send a message through the platform's adapter. thread_id is forwarded where the platform has a thread concept; context_token is the WeChat iLink session token.

    send_ok

    fn send_ok(message_id : String?, raw : Json) -> SendResult

    Success result carrying an optional platform message id and raw body.

    send_reaction

    async fn send_reaction(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResult

    React to a message, where the platform supports reactions.

    send_typing

    async fn send_typing(cfg : PlatformConfig, chat_id : String, thread_id? : String) -> Unit

    Send a typing indicator. Platforms without one are a silent no-op.

    send_with_retry

    async fn send_with_retry(send_one : async () -> SendResult, max_attempts? : Int) -> SendResult

    Retry a send up to max_attempts times while the failure is transient (Hermes _send_with_retry). Honours retry_after seconds when the platform supplied one (e.g. Telegram 429 parameters.retry_after).

    signal_account

    fn signal_account(cfg : PlatformConfig) -> String?

    Account phone number (SIGNAL_ACCOUNT).

    signal_http_url

    fn signal_http_url(cfg : PlatformConfig) -> String

    signal-cli HTTP root (SIGNAL_HTTP_URL, e.g. http://127.0.0.1:8080).

    signal_parse_envelope

    fn signal_parse_envelope(envelope : Json) -> MessageEvent?

    Parse a signal-cli dataMessage envelope into a MessageEvent.

    signal_parse_receive

    fn signal_parse_receive(body : String) -> Array[MessageEvent]

    Parse the array returned by GET /v1/receive/{account} (or a JSON-RPC receive result) into events.

    signal_send

    async fn signal_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content, chunked at Signal's 8000-char limit.

    signal_send_params

    fn signal_send_params(account : String, chat_id : String, text : String) -> Json

    JSON-RPC body for send.

    signal_typing

    async fn signal_typing(cfg : PlatformConfig, chat_id : String) -> Unit

    Typing indicator (method: sendTyping), failures ignored.

    signal_typing_params

    fn signal_typing_params(account : String, chat_id : String) -> Json

    JSON-RPC body for sendTyping.

    signal_with_target

    fn signal_with_target(params : Map[String, Json], chat_id : String) -> Unit

    Add the recipient/groupId routing key for chat_id to params.

    slack_api_base

    fn slack_api_base(cfg : PlatformConfig) -> String

    API root, overridable with [slack.extra] api_base.

    slack_challenge

    fn slack_challenge(body : String) -> String?

    The url_verification challenge, if this body is a Slack handshake.

    slack_delete

    async fn slack_delete(cfg : PlatformConfig, chat_id : String, message_id : String) -> SendResult

    Delete a message (chat.delete).

    slack_edit

    async fn slack_edit(cfg : PlatformConfig, chat_id : String, message_id : String, content : String) -> SendResult

    Update a previously sent message (chat.update).

    slack_get_chat

    async fn slack_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Channel metadata (conversations.info).

    slack_headers

    fn slack_headers(token : String) -> Map[String, String]

    Slack headers (Authorization: Bearer, JSON content type).

    slack_parse_event

    fn slack_parse_event(body : String, bot_user_id? : String) -> Array[MessageEvent]

    Parse a Slack Events API body (event_callback) into message events. Bot/echo messages and message subtypes (edits, joins) are skipped.

    slack_react

    async fn slack_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResult

    Add a reaction (reactions.add).

    slack_request_verified

    fn slack_request_verified(secret : String, timestamp : String, body : String, signature : String, now_s : Int64, window_s? : Int64) -> Bool

    Verify a whole Slack webhook request: the X-Slack-Signature must match and X-Slack-Request-Timestamp must be within window_s seconds of now_s (Slack's recommended replay window is five minutes).

    slack_send

    async fn slack_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_ts? : String) -> SendResult

    Send content, chunked at Slack's 39000-char limit, threaded on thread_ts (defaults to reply_to).

    slack_send_payload

    fn slack_send_payload(channel : String, text : String, thread_ts : String?) -> Json

    chat.postMessage payload.

    slack_signature

    fn slack_signature(secret : String, timestamp : String, body : String) -> String

    Slack request signature (v0=<hex hmac-sha256>).

    slack_typing

    async fn slack_typing(cfg : PlatformConfig, chat_id : String, thread_ts? : String) -> Unit

    Slack has no typing indicator; we surface the assistant thread status (assistant.threads.setStatus) and ignore failures.

    slack_update_payload

    fn slack_update_payload(channel : String, ts : String, text : String) -> Json

    chat.update payload (used for streaming/draft edits).

    slack_verify_signature

    fn slack_verify_signature(secret : String, timestamp : String, body : String, signature : String) -> Bool

    Verify a Slack X-Slack-Signature header (constant-time comparison).

    smtp_dot_stuff

    fn smtp_dot_stuff(message : String) -> String

    Escape a message body for SMTP DATA (dot-stuffing).

    strip_cdata

    fn strip_cdata(s : String) -> String

    Strip a surrounding <![CDATA[ … ]]> wrapper.

    strip_markdown

    fn strip_markdown(text : String) -> String

    Strip common markdown to a platform-safe plain-text rendering. Used by text-only adapters (DingTalk fallback, email, WeCom, WhatsApp body, ...).

    telegram_api_base

    fn telegram_api_base(cfg : PlatformConfig, token : String) -> String

    Bot API root, e.g. https://api.telegram.org/bot<token>.

    telegram_delete

    async fn telegram_delete(cfg : PlatformConfig, chat_id : String, message_id : String) -> SendResult

    Delete a message.

    telegram_delete_webhook

    async fn telegram_delete_webhook(cfg : PlatformConfig, token : String) -> SendResult

    Remove any registered webhook (required before long polling).

    telegram_edit

    async fn telegram_edit(cfg : PlatformConfig, chat_id : String, message_id : String, text : String) -> SendResult

    Edit a previously sent message.

    telegram_get_chat

    async fn telegram_get_chat(cfg : PlatformConfig, chat_id : String) -> ChatInfo?

    Chat metadata via getChat.

    telegram_get_updates

    async fn telegram_get_updates(cfg : PlatformConfig, token : String, offset : Int, timeout_s : Int) -> Array[Json]

    Fetch updates (getUpdates) for long-polling transports; failures are reported as an empty batch (see telegram_get_updates_result).

    telegram_get_updates_result

    async fn telegram_get_updates_result(cfg : PlatformConfig, token : String, offset : Int, timeout_s : Int) -> Result[Array[Json], ApiError]

    Fetch updates (getUpdates) for long-polling transports, surfacing transport/API failures so pollers can back off instead of hammering a broken endpoint. offset acknowledges every earlier update.

    telegram_media_method

    fn telegram_media_method(kind : String) -> String

    Telegram method name for a media kind (sendPhoto, sendDocument, ...).

    telegram_media_payload

    fn telegram_media_payload(chat_id : String, kind : String, url : String, caption : String?, parse_mode : String?) -> Json

    send<Kind> payload for media by URL.

    telegram_parse_message

    fn telegram_parse_message(msg : Json) -> MessageEvent?

    Convert one Telegram Message/ChannelPost object into a MessageEvent.

    telegram_parse_mode

    fn telegram_parse_mode(cfg : PlatformConfig) -> String?

    Parse mode to request, from [telegram.extra] parse_mode. "" (default) sends plain text; MarkdownV2/HTML opt in.

    telegram_parse_update

    fn telegram_parse_update(update : Json) -> Array[MessageEvent]

    Parse a {"update_id":N,"message":{…}} webhook body into events.

    telegram_parse_updates

    fn telegram_parse_updates(updates : Array[Json]) -> Array[MessageEvent]

    Parse a whole getUpdates result array.

    telegram_react

    async fn telegram_react(cfg : PlatformConfig, chat_id : String, message_id : String, emoji : String) -> SendResult

    React to a message with an emoji (Bot API 7.0 setMessageReaction).

    telegram_send

    async fn telegram_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, thread_id? : String) -> SendResult

    Send content to chat_id, chunked at the platform limit. Only the first chunk replies to reply_to; every chunk stays in thread_id.

    telegram_send_media

    async fn telegram_send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String, reply_to? : String) -> SendResult

    Send a media URL with an optional caption.

    telegram_send_payload

    fn telegram_send_payload(chat_id : String, text : String, reply_to : String?, thread_id : String?, parse_mode : String?) -> Json

    sendMessage payload.

    telegram_set_webhook

    async fn telegram_set_webhook(cfg : PlatformConfig, token : String, url : String, secret_token? : String) -> SendResult

    Register a webhook.

    telegram_typing

    async fn telegram_typing(cfg : PlatformConfig, chat_id : String, thread_id? : String) -> Unit

    Typing indicator (sendChatAction), errors are ignored.

    telegram_typing_payload

    fn telegram_typing_payload(chat_id : String, thread_id : String?) -> Json

    sendChatAction payload.

    telegram_webhook_authorized

    fn telegram_webhook_authorized(expected : String, provided : String?) -> Bool

    Whether a Telegram webhook carries the configured X-Telegram-Bot-Api-Secret-Token (constant-time; Telegram echoes the secret_token passed to setWebhook).

    truncate

    fn truncate(text : String, max : Int, suffix? : String) -> String

    Truncate text to at most max characters, appending suffix when it was cut (Hermes _truncate_preview).

    url_quote

    fn url_quote(s : String) -> String

    Percent-encode a URL query component (RFC 3986, uppercase hex) — the same encoding Hermes applies with urllib.parse.quote.

    user_allowed

    fn user_allowed(cfg : GatewayConfig, pcfg : PlatformConfig, user_id : String?, chat_id : String) -> Bool

    Whether a sender may talk to the agent. allow_all_users admits everyone; otherwise the user id (falling back to the chat id when the platform has no per-user id) must appear in the platform or gateway allowlist — or match the * wildcard. With no allowlist configured and no opt-in the gate is closed (Hermes' GATEWAY_ALLOW_ALL_USERS posture).

    webhook_auth_status

    fn webhook_auth_status(platform : Platform, pcfg : PlatformConfig, request :
    Request
    , raw : String) -> (Int, String)

    Authenticate one inbound webhook request: (0, "") when the request may be processed, or an HTTP status and reason when it must be rejected. Webhook payloads that carry a platform signature (slack, whatsapp, telegram) fail closed while no secret is configured; platforms with no signature scheme only check once a shared secret is configured. WeCom checks msg_signature once callback crypto (encoding_aes_key) is configured.

    webhook_path

    fn webhook_path(platform : Platform) -> String

    Webhook path for a platform (/telegram, /whatsapp, …).

    wecom_access_token

    async fn wecom_access_token(cfg : PlatformConfig) -> String?

    Fetch (and cache) a WeCom access token.

    wecom_aes_key

    fn wecom_aes_key(encoding_aes_key : String) -> Bytes?

    Raw AES key bytes from the 43-char encoding_aes_key (Hermes appends = before base64-decoding). None unless exactly 32 bytes come out.

    wecom_agent_id

    fn wecom_agent_id(cfg : PlatformConfig) -> Int?

    Agent id (agentid) required by message/send.

    wecom_api_base

    fn wecom_api_base(cfg : PlatformConfig) -> String

    API root, overridable with [wecom.extra] api_base.

    wecom_callback_params

    fn wecom_callback_params(path : String) -> WecomCallbackParams

    Parse msg_signature / timestamp / nonce / echostr from a callback path's query string.

    wecom_callback_token

    fn wecom_callback_token(cfg : PlatformConfig) -> String

    Callback token used to build/check msg_signature (WECOM_CALLBACK_TOKEN / [wecom.extra] callback_token).

    wecom_corp_id

    fn wecom_corp_id(cfg : PlatformConfig) -> String

    Corp id (WECOM_CALLBACK_CORP_ID).

    wecom_corp_secret

    fn wecom_corp_secret(cfg : PlatformConfig) -> String?

    Corp secret (WECOM_CALLBACK_CORP_SECRET), also accepted as token.

    wecom_decrypt_envelope

    fn wecom_decrypt_envelope(encoding_aes_key : String, receive_id : String, encrypt : String) -> String?

    Decrypt one encrypted WeCom envelope (Hermes WXBizMsgCrypt.decrypt without the signature step): base64 → AES-256-CBC (IV = first 16 key bytes) → strip 32-byte PKCS#7 → skip the 16-byte random prefix → 4-byte big-endian length → message → the trailing receive_id must match. None on any malformed input or mismatch.

    wecom_decrypt_message

    fn wecom_decrypt_message(token : String, encoding_aes_key : String, receive_id : String, msg_signature : String, timestamp : String, nonce : String, encrypt : String) -> String?

    Verify msg_signature and decrypt one encrypted WeCom payload (Hermes WXBizMsgCrypt.decrypt). None on a signature mismatch or any decryption/correspondence failure.

    wecom_encoding_aes_key

    fn wecom_encoding_aes_key(cfg : PlatformConfig) -> String

    Callback encryption key (WECOM_CALLBACK_ENCODING_AES_KEY / [wecom.extra] encoding_aes_key): 43 base64 characters decoding to 32 AES key bytes (the first 16 of which double as the CBC IV).

    wecom_encrypt_message

    fn wecom_encrypt_message(token : String, encoding_aes_key : String, receive_id : String, plaintext : String, timestamp : String, nonce : String) -> String?

    Encrypt one plaintext into the <xml><Encrypt>…</Encrypt>…</xml> callback envelope (Hermes WXBizMsgCrypt.encrypt): 16 random bytes, the UTF-8 length in 4 big-endian bytes, the message, then receive_id, PKCS#7 padded to 32 and AES-256-CBC encrypted with iv = key[:16].

    wecom_msg_signature

    fn wecom_msg_signature(token : String, timestamp : String, nonce : String, encrypt : String) -> String

    WeCom callback signature (Hermes _sha1_signature): SHA1 over the four parts sorted lexicographically and concatenated, lowercase hex.

    wecom_parse_callback

    fn wecom_parse_callback(body : String) -> Array[MessageEvent]

    Parse a WeCom callback (classic XML or the newer JSON aibot shape).

    wecom_pkcs7_pad

    fn wecom_pkcs7_pad(data : Bytes) -> Bytes

    PKCS#7 pad to the 32-byte WeCom block boundary (Hermes PKCS7Encoder). A payload already aligned to 32 still gains a full block.

    wecom_pkcs7_unpad

    fn wecom_pkcs7_unpad(data : Bytes) -> Bytes?

    Strip 32-byte PKCS#7 padding; None when malformed (Hermes PKCS7Encoder.decode).

    wecom_send

    async fn wecom_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content to a user or party (@all for everyone).

    wecom_send_payload

    fn wecom_send_payload(to : String, content : String, agent_id : Int?, markdown : Bool) -> Json

    message/send payload.

    wecom_unwrap_callback

    fn wecom_unwrap_callback(cfg : PlatformConfig, raw : String) -> Result[String, String]

    Decrypt one inbound WeCom callback body when callback crypto is configured (encoding_aes_key set): extract the <Encrypt> element and decrypt it against corp_id. Without an AES key the body passes through unchanged (relay mode, where an upstream relay already decrypted it).

    wecom_verify_msg_signature

    fn wecom_verify_msg_signature(token : String, timestamp : String, nonce : String, encrypt : String, msg_signature : String) -> Bool

    Constant-time msg_signature check.

    weixin_aes_key

    fn weixin_aes_key(key_b64 : String) -> Result[Bytes, String]

    Parse an iLink aes_key value (Hermes _parse_aes_key): either base64 of 16 raw key bytes, or base64 of the ASCII hex key.

    weixin_app_id

    fn weixin_app_id(cfg : PlatformConfig) -> String

    iLink app id ("bot").

    weixin_base_url

    fn weixin_base_url(cfg : PlatformConfig) -> String

    API root, overridable with [weixin.extra] base_url.

    weixin_base_url_default

    fn weixin_base_url_default() -> String

    The default iLink base URL (ilinkai.weixin.qq.com).

    weixin_cdn_base_url

    fn weixin_cdn_base_url(cfg : PlatformConfig) -> String

    CDN root, overridable via [weixin.extra] cdn_base_url (trailing / trimmed).

    weixin_cdn_host_allowed

    fn weixin_cdn_host_allowed(url : String) -> Bool

    WeChat CDN host allowlist (Hermes _WEIXIN_CDN_ALLOWLIST) — SSRF guard for full_url downloads.

    weixin_channel_version

    fn weixin_channel_version(cfg : PlatformConfig) -> String

    Channel version advertised in base_info.

    weixin_client_version

    fn weixin_client_version(cfg : PlatformConfig) -> String

    Client version integer ((2<<16)|(2<<8) = 131584).

    weixin_download_media

    async fn weixin_download_media(cfg : PlatformConfig, item : Json) -> MediaAttachment?

    Download + decrypt one inbound media item into a data: URL attachment.

    weixin_get_qrcode

    async fn weixin_get_qrcode(cfg : PlatformConfig, bot_type? : String) -> Json?

    Fetch the QR login payload (get_bot_qrcode), keyed by bot_type (Hermes default "3").

    weixin_get_updates

    async fn weixin_get_updates(cfg : PlatformConfig, token : String, sync_buf : String) -> (Array[Json], String)

    Long-poll updates (ilink/bot/getupdates). Returns the raw msgs array and the new sync buffer; failures yield an empty batch (see weixin_get_updates_result).

    weixin_get_updates_result

    async fn weixin_get_updates_result(cfg : PlatformConfig, token : String, sync_buf : String) -> Result[(Array[Json], String), ApiError]

    Long-poll updates (ilink/bot/getupdates), surfacing failures so pollers can back off. Returns the raw msgs array and the new sync buffer.

    weixin_headers

    fn weixin_headers(cfg : PlatformConfig, token : String?) -> Map[String, String]

    iLink request headers.

    weixin_is_session_expired

    fn weixin_is_session_expired(ret : Int, errcode : Int, errmsg : String) -> Bool

    A response that means the session context went stale (Hermes _is_session_expired).

    weixin_is_stale_session

    fn weixin_is_stale_session(ret : Int, errcode : Int, errmsg : String) -> Bool

    iLink stale-session variance of the -2 frequency limit (Hermes _is_stale_session_ret).

    weixin_item_text

    fn weixin_item_text(msg : Json) -> String

    Concatenate the text_item entries of an iLink message (Hermes _extract_text): first text item wins (quoting handled), then a voice-item STT fallback for transcripts with no raw audio.

    weixin_media_attachments

    async fn weixin_media_attachments(cfg : PlatformConfig, msg : Json) -> Array[MediaAttachment]

    Download + decrypt every media item (and quoted media) of an update message.

    weixin_media_decrypt

    fn weixin_media_decrypt(ciphertext : Bytes, key : Bytes) -> Bytes raise

    AES-128-ECB decrypt then strip PKCS#7 padding.

    weixin_media_descriptor

    fn weixin_media_descriptor(item : Json) -> (String, String, String, String)?

    Describe an inbound media item → (kind, mime, filename, payload_key) or None when not a media item.

    weixin_media_download_url

    fn weixin_media_download_url(cdn_base : String, encrypted_query_param : String) -> String

    CDN download URL for an encrypted media item.

    weixin_media_encrypt

    fn weixin_media_encrypt(plaintext : Bytes, key : Bytes) -> Bytes raise

    AES-128-ECB encrypt with PKCS#7 padding.

    weixin_media_item

    fn weixin_media_item(media_type : Int, encrypted_query_param : String, aes_key_hex : String, ciphertext_size : Int, plaintext_size : Int, filename : String, rawfilemd5 : String) -> Json

    Build an outbound media item (Hermes _media_item + _ITEM_SHAPES); aes_key is shipped as base64(hex string), not base64(raw key).

    weixin_media_type_of

    fn weixin_media_type_of(filename : String) -> Int

    media_type (getuploadurl) for a local file name (Hermes _outbound_media_builder).

    weixin_media_upload_url

    fn weixin_media_upload_url(cdn_base : String, upload_param : String, filekey : String) -> String

    CDN upload URL constructed from upload_param (Hermes fallback path).

    weixin_message_type_from_kinds

    fn weixin_message_type_from_kinds(kinds : Array[String]) -> MessageType

    Map a previously-collected media kind list onto a MessageType (Hermes _message_type_from_media): media wins over text/command classification.

    weixin_mime_of

    fn weixin_mime_of(filename : String) -> String

    Guess a MIME type from a file name.

    weixin_parse_message

    fn weixin_parse_message(msg : Json) -> MessageEvent?

    Parse an iLink update message ({from_user_id, msg_id, item_list, …}).

    weixin_pkcs7_pad

    fn weixin_pkcs7_pad(data : Bytes) -> Bytes

    PKCS#7 padding to a 16-byte block boundary (Hermes _pkcs7_pad).

    weixin_pkcs7_unpad

    fn weixin_pkcs7_unpad(data : Bytes) -> Bytes

    Strip PKCS#7 padding when well-formed, else return the raw data (Hermes _aes128_ecb_decrypt).

    weixin_qr_creds

    fn weixin_qr_creds(status : Json) -> Json?

    Extract confirmed credentials from the QR status payload → {account_id, token, base_url, user_id} or None.

    weixin_qr_login

    async fn weixin_qr_login(cfg : PlatformConfig, on_qr : (String) -> Unit, persist : (Json) -> Unit, timeout_seconds? : Int) -> Json?

    Drive the QR login state machine (Hermes qr_login): prints the liteapp URL via on_qr, polls until confirmed (persisting credentials via persist), refreshes expired QRs, follows scaned_but_redirect hosts.

    weixin_qr_status

    async fn weixin_qr_status(cfg : PlatformConfig, qrcode : String) -> Json?

    Poll QR login status (get_qrcode_status?qrcode=…).

    weixin_send

    async fn weixin_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String, context_token? : String) -> SendResult

    Send content, chunked at WeChat's 4000-char limit. context_token opts into session-accurate delivery for the same chat.

    weixin_send_media

    async fn weixin_send_media(cfg : PlatformConfig, chat_id : String, data : Bytes, filename : String, context_token? : String) -> SendResult

    Send one media file (image/video/file/voice by extension) with caption-less delivery; stale sessions retry once without context_token.

    weixin_send_payload

    fn weixin_send_payload(cfg : PlatformConfig, to : String, text : String, context_token : String?) -> Json

    Full sendmessage payload (message wrapper + base_info).

    weixin_text_item

    fn weixin_text_item(text : String) -> Json

    item_list entry for a text message (ITEM_TEXT = 1).

    weixin_text_of

    fn weixin_text_of(item : Json) -> String

    Extract text from a single item (text_item) — used for quoted messages.

    weixin_typing

    async fn weixin_typing(cfg : PlatformConfig, chat_id : String) -> Unit

    Typing indicator (ilink/bot/sendtyping), failures ignored.

    weixin_upload_media

    async fn weixin_upload_media(cfg : PlatformConfig, token : String, chat_id : String, data : Bytes, filename : String) -> Result[Json, String]

    Upload one media file to the encrypted CDN and return the iLink item seed (encrypted_query_param, aes_key hex, sizes, md5).

    weixin_with_media

    fn weixin_with_media(ev : MessageEvent, media : Array[MediaAttachment]) -> MessageEvent

    Rebuild an event with media attachments and the media-derived message type.

    whatsapp_api_base

    fn whatsapp_api_base(cfg : PlatformConfig) -> String

    API root, overridable with [whatsapp.extra] api_base.

    whatsapp_api_version

    fn whatsapp_api_version(cfg : PlatformConfig) -> String

    Graph API version, overridable with [whatsapp.extra] api_version.

    whatsapp_hub_params

    fn whatsapp_hub_params(path : String) -> HubParams?

    Extract hub.mode / hub.verify_token / hub.challenge from a path.

    whatsapp_mark_read

    async fn whatsapp_mark_read(cfg : PlatformConfig, message_id : String) -> Unit

    Mark an inbound message as read (blue ticks).

    whatsapp_media_payload

    fn whatsapp_media_payload(to : String, kind : String, url : String, caption : String?) -> Json

    POST /messages media payload for a public URL.

    whatsapp_parse_webhook

    fn whatsapp_parse_webhook(body : String) -> Array[MessageEvent]

    Parse a WhatsApp webhook POST body into message events.

    whatsapp_phone_number_id

    fn whatsapp_phone_number_id(cfg : PlatformConfig) -> String?

    Phone-number id (WHATSAPP_CLOUD_PHONE_NUMBER_ID).

    whatsapp_retry_after

    fn whatsapp_retry_after(body : String) -> Double?

    Graph 429 bodies carry error.message containing "(#131056) … retry after n".

    whatsapp_send

    async fn whatsapp_send(cfg : PlatformConfig, chat_id : String, content : String, reply_to? : String) -> SendResult

    Send content, chunked at WhatsApp's 4096-char limit.

    whatsapp_send_media

    async fn whatsapp_send_media(cfg : PlatformConfig, chat_id : String, kind : String, url : String, caption? : String) -> SendResult

    Send a media URL by public link.

    whatsapp_send_payload

    fn whatsapp_send_payload(to : String, body : String, reply_to : String?) -> Json

    POST /messages text payload.

    whatsapp_signature

    fn whatsapp_signature(secret : String, body : String) -> String

    Meta's X-Hub-Signature-256 value for body (sha256=<hex hmac>).

    whatsapp_verify

    fn whatsapp_verify(mode : String, token : String, challenge : String, expected : String) -> String?

    Webhook verification handshake (hub.mode=subscribe).

    whatsapp_verify_signature

    fn whatsapp_verify_signature(secret : String, body : String, header : String) -> Bool

    Verify a WhatsApp X-Hub-Signature-256 header: sha256=<hex> HMAC of the raw body keyed by app_secret, case-insensitive hex, constant-time (Hermes whatsapp_cloud._verify_signature).

    xml_text

    fn xml_text(xml : String, tag : String) -> String?

    Extract the text of the first tag element in an XML document.