webex-message-handler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webex_message_handler-0.1.0/.gitignore +19 -0
- webex_message_handler-0.1.0/API.md +291 -0
- webex_message_handler-0.1.0/LICENSE +21 -0
- webex_message_handler-0.1.0/PKG-INFO +172 -0
- webex_message_handler-0.1.0/README.md +141 -0
- webex_message_handler-0.1.0/examples/basic_bot.py +75 -0
- webex_message_handler-0.1.0/pyproject.toml +62 -0
- webex_message_handler-0.1.0/src/webex_message_handler/__init__.py +62 -0
- webex_message_handler-0.1.0/src/webex_message_handler/device_manager.py +147 -0
- webex_message_handler-0.1.0/src/webex_message_handler/errors.py +46 -0
- webex_message_handler-0.1.0/src/webex_message_handler/handler.py +339 -0
- webex_message_handler-0.1.0/src/webex_message_handler/kms_client.py +416 -0
- webex_message_handler-0.1.0/src/webex_message_handler/logger.py +42 -0
- webex_message_handler-0.1.0/src/webex_message_handler/mercury_socket.py +410 -0
- webex_message_handler-0.1.0/src/webex_message_handler/message_decryptor.py +84 -0
- webex_message_handler-0.1.0/src/webex_message_handler/types.py +177 -0
- webex_message_handler-0.1.0/tests/__init__.py +0 -0
- webex_message_handler-0.1.0/tests/test_device_manager.py +149 -0
- webex_message_handler-0.1.0/tests/test_handler.py +270 -0
- webex_message_handler-0.1.0/tests/test_message_decryptor.py +253 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# webex-message-handler API Reference
|
|
2
|
+
|
|
3
|
+
Lightweight, standalone package for receiving and decrypting Webex messages over a persistent WebSocket. No full Webex SDK required — just provide a bot token.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install webex-message-handler
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
from webex_message_handler import WebexMessageHandler, WebexMessageHandlerConfig, console_logger
|
|
16
|
+
|
|
17
|
+
handler = WebexMessageHandler(
|
|
18
|
+
WebexMessageHandlerConfig(token="YOUR_BOT_TOKEN", logger=console_logger)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@handler.on("message:created")
|
|
22
|
+
async def on_message(msg):
|
|
23
|
+
print(f"[{msg.person_email}] {msg.text}")
|
|
24
|
+
|
|
25
|
+
handler.on("error", lambda err: print(f"Error: {err}"))
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
await handler.connect()
|
|
29
|
+
|
|
30
|
+
asyncio.run(main())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Public API Surface
|
|
36
|
+
|
|
37
|
+
| Method | Purpose |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `await connect()` | Start the connection (device registration, WebSocket, KMS handshake). |
|
|
40
|
+
| `await disconnect()` | Tear down the connection cleanly. |
|
|
41
|
+
| `await reconnect(new_token)` | Update the token and re-establish everything from scratch. |
|
|
42
|
+
| `status()` | Health check — returns structured connection state of all subsystems. |
|
|
43
|
+
| `connected` | Quick boolean: is the handler connected and WebSocket open? |
|
|
44
|
+
| `on(event, callback)` | Subscribe to events. Can also be used as `@handler.on(event)` decorator. |
|
|
45
|
+
| `off(event, callback)` | Unsubscribe from events. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## WebexMessageHandler
|
|
50
|
+
|
|
51
|
+
### Constructor
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**`WebexMessageHandlerConfig` fields:**
|
|
58
|
+
|
|
59
|
+
| Field | Type | Required | Default | Description |
|
|
60
|
+
|---|---|---|---|---|
|
|
61
|
+
| `token` | `str` | Yes | — | Webex bot or user access token. |
|
|
62
|
+
| `logger` | `Logger` | No | silent | Logger implementation (`console_logger` provided). |
|
|
63
|
+
| `ping_interval` | `float` | No | `15.0` | WebSocket heartbeat ping interval in seconds. |
|
|
64
|
+
| `pong_timeout` | `float` | No | `14.0` | How long to wait for pong before triggering reconnect. |
|
|
65
|
+
| `reconnect_backoff_max` | `float` | No | `32.0` | Max backoff delay between reconnection attempts. |
|
|
66
|
+
| `max_reconnect_attempts` | `int` | No | `10` | Max consecutive reconnection attempts before giving up. |
|
|
67
|
+
|
|
68
|
+
### Methods
|
|
69
|
+
|
|
70
|
+
#### `await connect()`
|
|
71
|
+
|
|
72
|
+
Establishes the full connection pipeline:
|
|
73
|
+
|
|
74
|
+
1. Registers a virtual device with Webex WDM (Web Device Management)
|
|
75
|
+
2. Opens a Mercury WebSocket and authenticates
|
|
76
|
+
3. Performs KMS ECDH key exchange (for end-to-end message encryption)
|
|
77
|
+
4. Begins listening for encrypted messages, decrypting them automatically
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
await handler.connect()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `await disconnect()`
|
|
84
|
+
|
|
85
|
+
Tears down the connection cleanly:
|
|
86
|
+
|
|
87
|
+
1. Closes the Mercury WebSocket (stops heartbeat, cancels auto-reconnection)
|
|
88
|
+
2. Unregisters the virtual device with WDM
|
|
89
|
+
3. Clears KMS context and decryption keys from memory
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
await handler.disconnect()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `await reconnect(new_token: str)`
|
|
96
|
+
|
|
97
|
+
Updates the access token and re-establishes the connection from scratch.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
fresh_token = await my_auth_system.refresh_token()
|
|
101
|
+
await handler.reconnect(fresh_token)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `status() -> HandlerStatus`
|
|
105
|
+
|
|
106
|
+
Returns a structured health check of all connection subsystems.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
health = handler.status()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**`HandlerStatus` fields:**
|
|
113
|
+
|
|
114
|
+
| Field | Type | Description |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `status` | `ConnectionStatus` | `'connected'`, `'connecting'`, `'reconnecting'`, or `'disconnected'`. |
|
|
117
|
+
| `web_socket_open` | `bool` | Whether the Mercury WebSocket is currently open. |
|
|
118
|
+
| `kms_initialized` | `bool` | Whether the KMS encryption context has been established. |
|
|
119
|
+
| `device_registered` | `bool` | Whether a virtual device is registered with WDM. |
|
|
120
|
+
| `reconnect_attempt` | `int` | Current auto-reconnect attempt number (`0` if not reconnecting). |
|
|
121
|
+
|
|
122
|
+
#### `connected: bool` (property)
|
|
123
|
+
|
|
124
|
+
Quick boolean shorthand. Returns `True` only when fully connected.
|
|
125
|
+
|
|
126
|
+
### Events
|
|
127
|
+
|
|
128
|
+
Subscribe with `handler.on(event, callback)` or `@handler.on(event)` decorator. Remove with `handler.off(event, callback)`.
|
|
129
|
+
|
|
130
|
+
#### `'message:created'`
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
@handler.on("message:created")
|
|
134
|
+
async def on_message(msg: DecryptedMessage):
|
|
135
|
+
msg.id # str — unique message ID
|
|
136
|
+
msg.room_id # str — conversation/room ID
|
|
137
|
+
msg.person_id # str — sender's Webex user ID
|
|
138
|
+
msg.person_email # str — sender's email address
|
|
139
|
+
msg.text # str — decrypted plain text
|
|
140
|
+
msg.html # str | None — decrypted HTML content
|
|
141
|
+
msg.created # str — ISO 8601 timestamp
|
|
142
|
+
msg.room_type # 'direct' | 'group' | None
|
|
143
|
+
msg.raw # MercuryActivity — full decrypted activity
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### `'message:deleted'`
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
@handler.on("message:deleted")
|
|
150
|
+
def on_deleted(data: DeletedMessage):
|
|
151
|
+
data.message_id # str — ID of the deleted message
|
|
152
|
+
data.room_id # str — conversation/room ID
|
|
153
|
+
data.person_id # str — who deleted it
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `'connected'`
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
@handler.on("connected")
|
|
160
|
+
def on_connected():
|
|
161
|
+
print("Ready to receive messages")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `'disconnected'`
|
|
165
|
+
|
|
166
|
+
| Reason | Meaning | Action |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `'client'` | You called `disconnect()`. | None — intentional. |
|
|
169
|
+
| `'auth-failed'` | Token is invalid or expired (code 4401). | Call `reconnect(new_token)`. |
|
|
170
|
+
| `'permanent-failure'` | Server rejected permanently (code 4400/4403). | Investigate. |
|
|
171
|
+
| `'max-attempts-exceeded'` | Auto-reconnect gave up. | Call `reconnect(new_token)`. |
|
|
172
|
+
| `'manual'` | WebSocket closed, reconnection disabled. | None. |
|
|
173
|
+
|
|
174
|
+
#### `'reconnecting'`
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
@handler.on("reconnecting")
|
|
178
|
+
def on_reconnecting(attempt: int):
|
|
179
|
+
print(f"Reconnect attempt {attempt}...")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `'error'`
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
@handler.on("error")
|
|
186
|
+
def on_error(err: Exception):
|
|
187
|
+
print(f"Handler error: {err}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Logger Interface
|
|
193
|
+
|
|
194
|
+
Any object with `debug`, `info`, `warning`, and `error` methods works. Python's `logging.Logger` is directly compatible.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
import logging
|
|
198
|
+
logger = logging.getLogger("my_bot")
|
|
199
|
+
handler = WebexMessageHandler(WebexMessageHandlerConfig(token="...", logger=logger))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Built-in loggers:**
|
|
203
|
+
|
|
204
|
+
| Export | Behavior |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `noop_logger` | Silent. All methods are no-ops. This is the default. |
|
|
207
|
+
| `console_logger` | Logs via `logging.getLogger("webex_message_handler")`. |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Error Classes
|
|
212
|
+
|
|
213
|
+
All errors extend `WebexError` which extends `Exception`. Each has a `.code` string.
|
|
214
|
+
|
|
215
|
+
| Class | `.code` | When |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `AuthError` | `AUTH_ERROR` | Token is invalid, expired, or unauthorized. |
|
|
218
|
+
| `DeviceRegistrationError` | `DEVICE_REGISTRATION_ERROR` | WDM operations failed. Has `.status_code`. |
|
|
219
|
+
| `MercuryConnectionError` | `MERCURY_CONNECTION_ERROR` | WebSocket connection failed. Has `.close_code`. |
|
|
220
|
+
| `KmsError` | `KMS_ERROR` | KMS key exchange or key retrieval failed. |
|
|
221
|
+
| `DecryptionError` | `DECRYPTION_ERROR` | Message decryption failed. |
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from webex_message_handler import AuthError, KmsError
|
|
225
|
+
|
|
226
|
+
@handler.on("error")
|
|
227
|
+
def on_error(err):
|
|
228
|
+
if isinstance(err, AuthError):
|
|
229
|
+
# token issue — reconnect with fresh token
|
|
230
|
+
pass
|
|
231
|
+
elif isinstance(err, KmsError):
|
|
232
|
+
# KMS issue — will auto-recover on reconnect
|
|
233
|
+
pass
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Type Exports
|
|
239
|
+
|
|
240
|
+
All types are dataclasses:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from webex_message_handler import (
|
|
244
|
+
WebexMessageHandlerConfig,
|
|
245
|
+
DeviceRegistration,
|
|
246
|
+
MercuryActor,
|
|
247
|
+
MercuryObject,
|
|
248
|
+
MercuryTarget,
|
|
249
|
+
MercuryActivity,
|
|
250
|
+
MercuryEnvelope,
|
|
251
|
+
DecryptedMessage,
|
|
252
|
+
DeletedMessage,
|
|
253
|
+
HandlerStatus,
|
|
254
|
+
ConnectionStatus,
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Connection Lifecycle
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
+-----------+ +----------+ +----------+ +---------+
|
|
264
|
+
| register | --> | Mercury | --> | KMS | --> | ready |
|
|
265
|
+
| device | | connect | | ECDH | | |
|
|
266
|
+
+-----------+ +----------+ +----------+ +---------+
|
|
267
|
+
| | | |
|
|
268
|
+
| heartbeat encrypts emits:
|
|
269
|
+
| ping/pong messages message:created
|
|
270
|
+
| | | message:deleted
|
|
271
|
+
| v | |
|
|
272
|
+
| (connection drop) | |
|
|
273
|
+
| | | |
|
|
274
|
+
| v v |
|
|
275
|
+
| +-----------+ +------------+ |
|
|
276
|
+
| | auto |--> | re-init |----------->+
|
|
277
|
+
| | reconnect | | KMS + WDM |
|
|
278
|
+
| +-----------+ +------------+
|
|
279
|
+
|
|
|
280
|
+
reconnect(new_token) --> disconnect() --> connect() with new token
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Notes for Implementers
|
|
286
|
+
|
|
287
|
+
- **Async throughout**: All I/O uses `asyncio` + `aiohttp`. Run inside an async context.
|
|
288
|
+
- **Memory**: Encryption keys are cached. On auto-reconnect, the key cache is preserved.
|
|
289
|
+
- **One instance per token**: Each instance registers its own virtual device.
|
|
290
|
+
- **No outbound messaging**: This package only *receives* messages. To send messages, use the Webex REST API.
|
|
291
|
+
- **Heartbeat**: Ping every `ping_interval` seconds. Pong timeout triggers auto-reconnect.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ergon Copeland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webex-message-handler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK
|
|
5
|
+
Project-URL: Homepage, https://github.com/3rg0n/webex-message-handler
|
|
6
|
+
Project-URL: Repository, https://github.com/3rg0n/webex-message-handler
|
|
7
|
+
Author: Ergon Copeland
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: bot,kms,mercury,messaging,webex,websocket
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Communications :: Chat
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: aiohttp>=3.9
|
|
23
|
+
Requires-Dist: jwcrypto>=1.5
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: aioresponses>=0.7; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# webex-message-handler
|
|
33
|
+
|
|
34
|
+
Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages — no Webex SDK required.
|
|
35
|
+
|
|
36
|
+
Python port of the [TypeScript webex-message-handler](https://github.com/ecopelan/webex-message-handler).
|
|
37
|
+
|
|
38
|
+
## Why?
|
|
39
|
+
|
|
40
|
+
- **The Webex Python SDK has heavy dependencies and limited WebSocket support**
|
|
41
|
+
- **Bots behind corporate firewalls need persistent connections, not webhooks**
|
|
42
|
+
- **This package extracts only the essential Mercury + KMS logic (~2 dependencies)**
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install webex-message-handler
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from webex_message_handler import WebexMessageHandler, WebexMessageHandlerConfig, console_logger
|
|
55
|
+
|
|
56
|
+
handler = WebexMessageHandler(
|
|
57
|
+
WebexMessageHandlerConfig(
|
|
58
|
+
token="YOUR_BOT_TOKEN",
|
|
59
|
+
logger=console_logger,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@handler.on("message:created")
|
|
64
|
+
async def on_message(msg):
|
|
65
|
+
print(f"[{msg.person_email}] {msg.text}")
|
|
66
|
+
if msg.html:
|
|
67
|
+
print(f" HTML: {msg.html}")
|
|
68
|
+
|
|
69
|
+
@handler.on("message:deleted")
|
|
70
|
+
def on_deleted(data):
|
|
71
|
+
print(f"Message {data.message_id} deleted by {data.person_id}")
|
|
72
|
+
|
|
73
|
+
@handler.on("connected")
|
|
74
|
+
def on_connected():
|
|
75
|
+
print("Connected to Webex")
|
|
76
|
+
|
|
77
|
+
@handler.on("disconnected")
|
|
78
|
+
def on_disconnected(reason):
|
|
79
|
+
print(f"Disconnected: {reason}")
|
|
80
|
+
|
|
81
|
+
@handler.on("reconnecting")
|
|
82
|
+
def on_reconnecting(attempt):
|
|
83
|
+
print(f"Reconnecting (attempt {attempt})...")
|
|
84
|
+
|
|
85
|
+
@handler.on("error")
|
|
86
|
+
def on_error(err):
|
|
87
|
+
print(f"Error: {err}")
|
|
88
|
+
|
|
89
|
+
async def main():
|
|
90
|
+
await handler.connect()
|
|
91
|
+
# Keep running until interrupted
|
|
92
|
+
try:
|
|
93
|
+
await asyncio.Event().wait()
|
|
94
|
+
finally:
|
|
95
|
+
await handler.disconnect()
|
|
96
|
+
|
|
97
|
+
asyncio.run(main())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
See `examples/basic_bot.py` for a complete working example.
|
|
101
|
+
|
|
102
|
+
## API Reference
|
|
103
|
+
|
|
104
|
+
### `WebexMessageHandler`
|
|
105
|
+
|
|
106
|
+
Main class for receiving and decrypting Webex messages.
|
|
107
|
+
|
|
108
|
+
#### Constructor
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Configuration options:**
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Description |
|
|
117
|
+
|--------|------|---------|-------------|
|
|
118
|
+
| `token` | `str` | required | Webex bot access token |
|
|
119
|
+
| `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
|
|
120
|
+
| `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
|
|
121
|
+
| `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
|
|
122
|
+
| `reconnect_backoff_max` | `float` | `32.0` | Max reconnect backoff (seconds) |
|
|
123
|
+
| `max_reconnect_attempts` | `int` | `10` | Max reconnect attempts |
|
|
124
|
+
|
|
125
|
+
#### Methods
|
|
126
|
+
|
|
127
|
+
- **`await connect()`** — Connects to Webex (registers device, initializes KMS, opens Mercury WebSocket)
|
|
128
|
+
- **`await disconnect()`** — Gracefully disconnects (closes WebSocket, unregisters device)
|
|
129
|
+
- **`await reconnect(new_token)`** — Update token and re-establish connection
|
|
130
|
+
- **`status()`** — Returns `HandlerStatus` health check
|
|
131
|
+
- **`connected`** — `bool` property: whether currently connected
|
|
132
|
+
|
|
133
|
+
#### Events
|
|
134
|
+
|
|
135
|
+
| Event | Payload | Description |
|
|
136
|
+
|-------|---------|-------------|
|
|
137
|
+
| `message:created` | `DecryptedMessage` | New message received and decrypted |
|
|
138
|
+
| `message:deleted` | `DeletedMessage` | Message was deleted |
|
|
139
|
+
| `connected` | — | Connected/reconnected to Mercury |
|
|
140
|
+
| `disconnected` | `reason: str` | Disconnected from Mercury |
|
|
141
|
+
| `reconnecting` | `attempt: int` | Attempting to reconnect |
|
|
142
|
+
| `error` | `Exception` | Error occurred |
|
|
143
|
+
|
|
144
|
+
### `DecryptedMessage`
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
@dataclass
|
|
148
|
+
class DecryptedMessage:
|
|
149
|
+
id: str
|
|
150
|
+
room_id: str
|
|
151
|
+
person_id: str
|
|
152
|
+
person_email: str
|
|
153
|
+
text: str
|
|
154
|
+
created: str
|
|
155
|
+
html: str | None
|
|
156
|
+
room_type: str | None # "direct" | "group"
|
|
157
|
+
raw: MercuryActivity | None
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Architecture
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
WebexMessageHandler (orchestrator)
|
|
164
|
+
├── DeviceManager — WDM registration
|
|
165
|
+
├── MercurySocket — WebSocket + ping/pong + reconnect
|
|
166
|
+
├── KmsClient — ECDH handshake + key retrieval
|
|
167
|
+
└── MessageDecryptor — JWE decryption
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# webex-message-handler
|
|
2
|
+
|
|
3
|
+
Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages — no Webex SDK required.
|
|
4
|
+
|
|
5
|
+
Python port of the [TypeScript webex-message-handler](https://github.com/ecopelan/webex-message-handler).
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
- **The Webex Python SDK has heavy dependencies and limited WebSocket support**
|
|
10
|
+
- **Bots behind corporate firewalls need persistent connections, not webhooks**
|
|
11
|
+
- **This package extracts only the essential Mercury + KMS logic (~2 dependencies)**
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install webex-message-handler
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from webex_message_handler import WebexMessageHandler, WebexMessageHandlerConfig, console_logger
|
|
24
|
+
|
|
25
|
+
handler = WebexMessageHandler(
|
|
26
|
+
WebexMessageHandlerConfig(
|
|
27
|
+
token="YOUR_BOT_TOKEN",
|
|
28
|
+
logger=console_logger,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@handler.on("message:created")
|
|
33
|
+
async def on_message(msg):
|
|
34
|
+
print(f"[{msg.person_email}] {msg.text}")
|
|
35
|
+
if msg.html:
|
|
36
|
+
print(f" HTML: {msg.html}")
|
|
37
|
+
|
|
38
|
+
@handler.on("message:deleted")
|
|
39
|
+
def on_deleted(data):
|
|
40
|
+
print(f"Message {data.message_id} deleted by {data.person_id}")
|
|
41
|
+
|
|
42
|
+
@handler.on("connected")
|
|
43
|
+
def on_connected():
|
|
44
|
+
print("Connected to Webex")
|
|
45
|
+
|
|
46
|
+
@handler.on("disconnected")
|
|
47
|
+
def on_disconnected(reason):
|
|
48
|
+
print(f"Disconnected: {reason}")
|
|
49
|
+
|
|
50
|
+
@handler.on("reconnecting")
|
|
51
|
+
def on_reconnecting(attempt):
|
|
52
|
+
print(f"Reconnecting (attempt {attempt})...")
|
|
53
|
+
|
|
54
|
+
@handler.on("error")
|
|
55
|
+
def on_error(err):
|
|
56
|
+
print(f"Error: {err}")
|
|
57
|
+
|
|
58
|
+
async def main():
|
|
59
|
+
await handler.connect()
|
|
60
|
+
# Keep running until interrupted
|
|
61
|
+
try:
|
|
62
|
+
await asyncio.Event().wait()
|
|
63
|
+
finally:
|
|
64
|
+
await handler.disconnect()
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See `examples/basic_bot.py` for a complete working example.
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### `WebexMessageHandler`
|
|
74
|
+
|
|
75
|
+
Main class for receiving and decrypting Webex messages.
|
|
76
|
+
|
|
77
|
+
#### Constructor
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Configuration options:**
|
|
84
|
+
|
|
85
|
+
| Option | Type | Default | Description |
|
|
86
|
+
|--------|------|---------|-------------|
|
|
87
|
+
| `token` | `str` | required | Webex bot access token |
|
|
88
|
+
| `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
|
|
89
|
+
| `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
|
|
90
|
+
| `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
|
|
91
|
+
| `reconnect_backoff_max` | `float` | `32.0` | Max reconnect backoff (seconds) |
|
|
92
|
+
| `max_reconnect_attempts` | `int` | `10` | Max reconnect attempts |
|
|
93
|
+
|
|
94
|
+
#### Methods
|
|
95
|
+
|
|
96
|
+
- **`await connect()`** — Connects to Webex (registers device, initializes KMS, opens Mercury WebSocket)
|
|
97
|
+
- **`await disconnect()`** — Gracefully disconnects (closes WebSocket, unregisters device)
|
|
98
|
+
- **`await reconnect(new_token)`** — Update token and re-establish connection
|
|
99
|
+
- **`status()`** — Returns `HandlerStatus` health check
|
|
100
|
+
- **`connected`** — `bool` property: whether currently connected
|
|
101
|
+
|
|
102
|
+
#### Events
|
|
103
|
+
|
|
104
|
+
| Event | Payload | Description |
|
|
105
|
+
|-------|---------|-------------|
|
|
106
|
+
| `message:created` | `DecryptedMessage` | New message received and decrypted |
|
|
107
|
+
| `message:deleted` | `DeletedMessage` | Message was deleted |
|
|
108
|
+
| `connected` | — | Connected/reconnected to Mercury |
|
|
109
|
+
| `disconnected` | `reason: str` | Disconnected from Mercury |
|
|
110
|
+
| `reconnecting` | `attempt: int` | Attempting to reconnect |
|
|
111
|
+
| `error` | `Exception` | Error occurred |
|
|
112
|
+
|
|
113
|
+
### `DecryptedMessage`
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
@dataclass
|
|
117
|
+
class DecryptedMessage:
|
|
118
|
+
id: str
|
|
119
|
+
room_id: str
|
|
120
|
+
person_id: str
|
|
121
|
+
person_email: str
|
|
122
|
+
text: str
|
|
123
|
+
created: str
|
|
124
|
+
html: str | None
|
|
125
|
+
room_type: str | None # "direct" | "group"
|
|
126
|
+
raw: MercuryActivity | None
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
WebexMessageHandler (orchestrator)
|
|
133
|
+
├── DeviceManager — WDM registration
|
|
134
|
+
├── MercurySocket — WebSocket + ping/pong + reconnect
|
|
135
|
+
├── KmsClient — ECDH handshake + key retrieval
|
|
136
|
+
└── MessageDecryptor — JWE decryption
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|