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.
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .env
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ *.log
18
+ .DS_Store
19
+ Thumbs.db
@@ -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