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".
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 |
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 connectionstatus()— ReturnsHandlerStatushealth checkconnected—boolproperty: whether currently connected
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
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file webex_message_handler-0.6.8.tar.gz.
File metadata
- Download URL: webex_message_handler-0.6.8.tar.gz
- Upload date:
- Size: 35.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a45e4e488756edf64387cc819980d172282c6134569e895920d084e8076d2508
|
|
| MD5 |
4b35070d96b78efa9f683e950ec08e7e
|
|
| BLAKE2b-256 |
f5d8895905c9ec7d52eddf4b51edd00cd6e3f4b91bd8123174e7cce7ff0cc09b
|
Provenance
The following attestation bundles were made for webex_message_handler-0.6.8.tar.gz:
Publisher:
publish.yml on 3rg0n/webex-message-handler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
webex_message_handler-0.6.8.tar.gz -
Subject digest:
a45e4e488756edf64387cc819980d172282c6134569e895920d084e8076d2508 - Sigstore transparency entry: 1286255870
- Sigstore integration time:
-
Permalink:
3rg0n/webex-message-handler@fb0cdd56f4c8b09b556aaba890c4f1186c244424 -
Branch / Tag:
refs/tags/v0.6.8 - Owner: https://github.com/3rg0n
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fb0cdd56f4c8b09b556aaba890c4f1186c244424 -
Trigger Event:
release
-
Statement type:
File details
Details for the file webex_message_handler-0.6.8-py3-none-any.whl.
File metadata
- Download URL: webex_message_handler-0.6.8-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84cfc53634e724555d16ea8c4a88b1d45665e3399dfe2f13d9183ecadcfcd0ce
|
|
| MD5 |
9e0c5f522d53bf1753a815319b0f5bbf
|
|
| BLAKE2b-256 |
c6bd898f90ec0d6cc292dc05d30cad34a6cd5692abb3866b6480f9ae4156264c
|
Provenance
The following attestation bundles were made for webex_message_handler-0.6.8-py3-none-any.whl:
Publisher:
publish.yml on 3rg0n/webex-message-handler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
webex_message_handler-0.6.8-py3-none-any.whl -
Subject digest:
84cfc53634e724555d16ea8c4a88b1d45665e3399dfe2f13d9183ecadcfcd0ce - Sigstore transparency entry: 1286255921
- Sigstore integration time:
-
Permalink:
3rg0n/webex-message-handler@fb0cdd56f4c8b09b556aaba890c4f1186c244424 -
Branch / Tag:
refs/tags/v0.6.8 - Owner: https://github.com/3rg0n
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fb0cdd56f4c8b09b556aaba890c4f1186c244424 -
Trigger Event:
release
-
Statement type: