Skip to main content

Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK

Project description

webex-message-handler

Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages — no Webex SDK required.

Python port of the TypeScript webex-message-handler.

Why?

  • The Webex Python SDK has heavy dependencies and limited WebSocket support
  • Bots behind corporate firewalls need persistent connections, not webhooks
  • This package extracts only the essential Mercury + KMS logic (~2 dependencies)

Install

pip install webex-message-handler

Quick Start

import asyncio
from webex_message_handler import WebexMessageHandler, WebexMessageHandlerConfig, console_logger

handler = WebexMessageHandler(
    WebexMessageHandlerConfig(
        token="YOUR_BOT_TOKEN",
        logger=console_logger,
    )
)

@handler.on("message:created")
async def on_message(msg):
    print(f"[{msg.person_email}] {msg.text}")
    if msg.html:
        print(f"  HTML: {msg.html}")

@handler.on("message:deleted")
def on_deleted(data):
    print(f"Message {data.message_id} deleted by {data.person_id}")

@handler.on("message:updated")
async def on_updated(msg):
    print(f"[EDIT] [{msg.person_email}] {msg.text}")

@handler.on("attachmentAction:created")
def on_card(action):
    print(f"Card submitted by {action.person_email}: {action.inputs}")

@handler.on("room:updated")
def on_room_updated(room):
    print(f"Room {room.room_id} updated by {room.actor_id}")

@handler.on("connected")
def on_connected():
    print("Connected to Webex")

@handler.on("disconnected")
def on_disconnected(reason):
    print(f"Disconnected: {reason}")

@handler.on("reconnecting")
def on_reconnecting(attempt):
    print(f"Reconnecting (attempt {attempt})...")

@handler.on("error")
def on_error(err):
    print(f"Error: {err}")

async def main():
    await handler.connect()
    # Keep running until interrupted
    try:
        await asyncio.Event().wait()
    finally:
        await handler.disconnect()

asyncio.run(main())

See examples/basic_bot.py for a complete working example.

Important: Implementing Loop Detection

This library only handles the receive side of messaging — it decrypts incoming messages from the Mercury WebSocket. It has no visibility into messages your bot sends via the REST API. This means it cannot detect message loops on its own.

If your bot replies to incoming messages, you must implement loop detection in your wrapper code. Without it, a bug or misconfiguration could cause your bot to endlessly reply to its own messages. Webex enforces a server-side rate limit (approximately 11 consecutive messages before throttling), but that still results in spam before the cutoff.

Recommended approach: Track your bot's outgoing message rate. If it exceeds a threshold (e.g., 5 messages in 3 seconds to the same room), pause sending and log a warning.

The ignore_self_messages option (default: True) provides a first line of defense by filtering out messages sent by this bot's own identity. If the library cannot verify the bot's identity during connect() (e.g., /people/me API failure), connection will fail rather than silently running without protection. Set ignore_self_messages=False to opt out, but only if you have your own loop prevention in place.

Proxy Support (Enterprise)

For corporate environments behind a proxy, pass a configured connector:

import aiohttp
from aiohttp_socks import ProxyConnector

# Using HTTP/HTTPS proxy
connector = ProxyConnector.from_url(
    "http://proxy.example.com:8080"
)

handler = WebexMessageHandler(
    WebexMessageHandlerConfig(
        token="YOUR_BOT_TOKEN",
        connector=connector,  # Pass configured connector
        logger=console_logger,
    )
)

await handler.connect()

Or using environment variables:

import os
import aiohttp
from aiohttp_socks import ProxyConnector

proxy_url = os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY")
connector = ProxyConnector.from_url(proxy_url) if proxy_url else None

handler = WebexMessageHandler(
    WebexMessageHandlerConfig(
        token=os.getenv("WEBEX_BOT_TOKEN"),
        connector=connector,
        logger=console_logger,
    )
)

Requires: pip install aiohttp-socks[asyncio]

Threading & Message IDs

Mercury uses raw activity UUIDs while the Webex REST API uses base64-encoded IDs. Use the conversion utilities to bridge them:

from webex_message_handler import to_rest_id, from_rest_id

@handler.on("message:created")
async def on_message(msg):
    # Convert Mercury UUID to REST API ID for GET requests
    rest_id = to_rest_id(msg.id, "MESSAGE")

    # Thread replies: msg.parent_id contains the parent activity UUID
    if msg.parent_id:
        # Use msg.parent_id as parentId in POST /v1/messages
        pass

Resource types: "MESSAGE", "PEOPLE", "ROOM".

Delivery Guarantees

This library provides at-most-once delivery semantics:

  • Mercury WebSocket acknowledges messages at the protocol level on receipt, before decryption or consumer delivery.
  • If decryption fails (e.g., KMS outage) or your callback throws an error, the message is not redelivered.
  • Mercury does not support application-level ACK/NACK — this is an inherent constraint of the Webex platform.

For consumers requiring stronger guarantees:

  • Wrap your callback with a persistent queue (e.g., database, Redis, or message broker) to ensure processing completes.
  • Use the error event to detect and log decryption failures.
  • The KMS circuit breaker (v0.6.9+) prevents 30-second stalls during KMS outages by failing fast after 3 consecutive failures.

API Reference

WebexMessageHandler

Main class for receiving and decrypting Webex messages.

Constructor

WebexMessageHandler(config: WebexMessageHandlerConfig)

Configuration options:

Option Type Default Description
token str required Webex bot access token
logger Logger noop Custom logger (console_logger provided)
ignore_self_messages bool True Filter out messages sent by this bot
connector aiohttp.BaseConnector None HTTP/HTTPS connector for proxy support
ping_interval float 15.0 Mercury ping interval (seconds)
pong_timeout float 14.0 Pong response timeout (seconds)
reconnect_backoff_max float 32.0 Max reconnect backoff (seconds)
max_reconnect_attempts int 10 Max reconnect attempts
metrics_callback MetricsCallback | None None Optional callback for timing metrics (connect, decrypt events)

Methods

  • await connect() — Connects to Webex (registers device, initializes KMS, opens Mercury WebSocket)
  • await disconnect() — Gracefully disconnects (closes WebSocket, unregisters device)
  • await reconnect(new_token) — Update token and re-establish connection
  • status() — Returns HandlerStatus health check
  • connectedbool property: whether currently connected
  • device_registration() — Read-only copy of the WDM registration (None before connect)
  • service_url(name) — Look up a single WDM service URL by name (None if unknown)
Outbound calls from wrappers

This library is inbound-only — it never makes outbound calls. If your wrapper needs to send something back to Webex (e.g. a Conversation-service read-receipt), discover the service base URL from the WDM catalog the library already holds rather than hardcoding cluster hostnames (which vary across clusters and orgs):

# Resolve a cluster-correct service URL after connect()
conv_url = handler.service_url("conversationServiceUrl")
if conv_url:
    # Build your outbound acknowledge/activity request against conv_url.
    # The activity URL needed for the acknowledge object is msg.url.
    ...

# Or grab the whole (read-only) registration:
reg = handler.device_registration()  # None before connect()

Events

Event Payload Description
message:created DecryptedMessage New message received and decrypted
message:deleted DeletedMessage Message was deleted
message:updated DecryptedMessage Message was edited and re-decrypted
attachmentAction:created AttachmentAction Adaptive Card submitted
room:created RoomActivity New room/space created
room:updated RoomActivity Room/space updated
membership:created MembershipActivity Member added/removed or moderator changed
connected Connected/reconnected to Mercury
disconnected reason: str Disconnected from Mercury
reconnecting attempt: int Attempting to reconnect
error Exception Error occurred

DecryptedMessage

@dataclass
class DecryptedMessage:
    id: str
    url: str | None         # Conversation-service activity URL (when present)
    parent_id: str | None   # Parent activity UUID (threaded replies)
    room_id: str
    person_id: str
    person_email: str
    text: str
    created: str
    html: str | None
    room_type: str | None        # "direct" | "group"
    mentioned_people: list[str]  # Person UUIDs from <spark-mention> tags
    mentioned_groups: list[str]  # e.g. ["all"] from group mentions
    files: list[str]             # File attachment URLs
    raw: MercuryActivity | None

AttachmentAction

Emitted when a user submits an Adaptive Card.

@dataclass
class AttachmentAction:
    id: str               # Activity UUID
    message_id: str       # Parent message containing the card
    person_id: str        # Person who submitted
    person_email: str
    room_id: str
    inputs: dict[str, Any]  # Card form data
    created: str
    raw: MercuryActivity

RoomActivity

Emitted for room lifecycle events.

@dataclass
class RoomActivity:
    id: str             # Activity UUID
    room_id: str
    actor_id: str       # Person who triggered the event
    action: str         # "create" or "update"
    created: str
    raw: MercuryActivity

parse_mentions(html)

Extracts mentions from decrypted HTML. Called automatically during decryption — the results populate DecryptedMessage.mentioned_people and DecryptedMessage.mentioned_groups. Exported for standalone use.

from webex_message_handler import parse_mentions

result = parse_mentions(msg.html)
# result.mentioned_people: ["uuid-1", "uuid-2"]
# result.mentioned_groups: ["all"]

Architecture

WebexMessageHandler (orchestrator)
├── DeviceManager  — WDM registration
├── MercurySocket  — WebSocket + ping/pong + reconnect
├── KmsClient      — ECDH handshake + key retrieval
└── MessageDecryptor — JWE decryption

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

webex_message_handler-0.6.10.tar.gz (38.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

webex_message_handler-0.6.10-py3-none-any.whl (32.8 kB view details)

Uploaded Python 3

File details

Details for the file webex_message_handler-0.6.10.tar.gz.

File metadata

  • Download URL: webex_message_handler-0.6.10.tar.gz
  • Upload date:
  • Size: 38.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for webex_message_handler-0.6.10.tar.gz
Algorithm Hash digest
SHA256 7141797063cb4f5c832559ae3bc3df4c3eabe79416a6be216bff5a728d37232d
MD5 dacc717b1e48d48189184cd63e9a4669
BLAKE2b-256 dcdfe93e61603bb68852bb1916659e049ec40a8f238e498784d4d7b41ebfcfa6

See more details on using hashes here.

Provenance

The following attestation bundles were made for webex_message_handler-0.6.10.tar.gz:

Publisher: publish.yml on 3rg0n/webex-message-handler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file webex_message_handler-0.6.10-py3-none-any.whl.

File metadata

File hashes

Hashes for webex_message_handler-0.6.10-py3-none-any.whl
Algorithm Hash digest
SHA256 9d7c6777302b3824a8fc5748f413410d28372d00e33e62213cf801b85e68b65d
MD5 be7f39ed27f0bd1e6f352f6d235dfde4
BLAKE2b-256 36f6c1662e989b313671baf1f6db093306e55bac45cfa45d8180a7dfc2a4f214

See more details on using hashes here.

Provenance

The following attestation bundles were made for webex_message_handler-0.6.10-py3-none-any.whl:

Publisher: publish.yml on 3rg0n/webex-message-handler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page