webex-message-handler 0.3.0__tar.gz → 0.4.1__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.3.0 → webex_message_handler-0.4.1}/PKG-INFO +12 -1
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/README.md +198 -187
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/pyproject.toml +1 -1
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/device_manager.py +161 -161
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/handler.py +486 -421
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/kms_client.py +420 -420
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/mercury_socket.py +401 -401
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/types.py +253 -250
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/tests/test_handler.py +2 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/tests/test_integration.py +131 -131
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/.gitignore +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/API.md +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/LICENSE +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/examples/basic_bot.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/__init__.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/errors.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/logger.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/src/webex_message_handler/message_decryptor.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/tests/__init__.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/tests/conftest.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/tests/test_device_manager.py +0 -0
- {webex_message_handler-0.3.0 → webex_message_handler-0.4.1}/tests/test_message_decryptor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webex-message-handler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK
|
|
5
5
|
Project-URL: Homepage, https://github.com/3rg0n/webex-message-handler
|
|
6
6
|
Project-URL: Repository, https://github.com/3rg0n/webex-message-handler
|
|
@@ -99,6 +99,16 @@ asyncio.run(main())
|
|
|
99
99
|
|
|
100
100
|
See `examples/basic_bot.py` for a complete working example.
|
|
101
101
|
|
|
102
|
+
## Important: Implementing Loop Detection
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
**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.
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
102
112
|
## Proxy Support (Enterprise)
|
|
103
113
|
|
|
104
114
|
For corporate environments behind a proxy, pass a configured connector:
|
|
@@ -162,6 +172,7 @@ WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
|
162
172
|
|--------|------|---------|-------------|
|
|
163
173
|
| `token` | `str` | required | Webex bot access token |
|
|
164
174
|
| `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
|
|
175
|
+
| `ignore_self_messages` | `bool` | `True` | Filter out messages sent by this bot |
|
|
165
176
|
| `connector` | `aiohttp.BaseConnector` | `None` | HTTP/HTTPS connector for proxy support |
|
|
166
177
|
| `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
|
|
167
178
|
| `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
|
|
@@ -1,187 +1,198 @@
|
|
|
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
|
-
##
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
## Important: Implementing Loop Detection
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
**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.
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
## Proxy Support (Enterprise)
|
|
82
|
+
|
|
83
|
+
For corporate environments behind a proxy, pass a configured connector:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import aiohttp
|
|
87
|
+
from aiohttp_socks import ProxyConnector
|
|
88
|
+
|
|
89
|
+
# Using HTTP/HTTPS proxy
|
|
90
|
+
connector = ProxyConnector.from_url(
|
|
91
|
+
"http://proxy.example.com:8080"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
handler = WebexMessageHandler(
|
|
95
|
+
WebexMessageHandlerConfig(
|
|
96
|
+
token="YOUR_BOT_TOKEN",
|
|
97
|
+
connector=connector, # Pass configured connector
|
|
98
|
+
logger=console_logger,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await handler.connect()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or using environment variables:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import os
|
|
109
|
+
import aiohttp
|
|
110
|
+
from aiohttp_socks import ProxyConnector
|
|
111
|
+
|
|
112
|
+
proxy_url = os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY")
|
|
113
|
+
connector = ProxyConnector.from_url(proxy_url) if proxy_url else None
|
|
114
|
+
|
|
115
|
+
handler = WebexMessageHandler(
|
|
116
|
+
WebexMessageHandlerConfig(
|
|
117
|
+
token=os.getenv("WEBEX_BOT_TOKEN"),
|
|
118
|
+
connector=connector,
|
|
119
|
+
logger=console_logger,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Requires: `pip install aiohttp-socks[asyncio]`
|
|
125
|
+
|
|
126
|
+
## API Reference
|
|
127
|
+
|
|
128
|
+
### `WebexMessageHandler`
|
|
129
|
+
|
|
130
|
+
Main class for receiving and decrypting Webex messages.
|
|
131
|
+
|
|
132
|
+
#### Constructor
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Configuration options:**
|
|
139
|
+
|
|
140
|
+
| Option | Type | Default | Description |
|
|
141
|
+
|--------|------|---------|-------------|
|
|
142
|
+
| `token` | `str` | required | Webex bot access token |
|
|
143
|
+
| `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
|
|
144
|
+
| `ignore_self_messages` | `bool` | `True` | Filter out messages sent by this bot |
|
|
145
|
+
| `connector` | `aiohttp.BaseConnector` | `None` | HTTP/HTTPS connector for proxy support |
|
|
146
|
+
| `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
|
|
147
|
+
| `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
|
|
148
|
+
| `reconnect_backoff_max` | `float` | `32.0` | Max reconnect backoff (seconds) |
|
|
149
|
+
| `max_reconnect_attempts` | `int` | `10` | Max reconnect attempts |
|
|
150
|
+
|
|
151
|
+
#### Methods
|
|
152
|
+
|
|
153
|
+
- **`await connect()`** — Connects to Webex (registers device, initializes KMS, opens Mercury WebSocket)
|
|
154
|
+
- **`await disconnect()`** — Gracefully disconnects (closes WebSocket, unregisters device)
|
|
155
|
+
- **`await reconnect(new_token)`** — Update token and re-establish connection
|
|
156
|
+
- **`status()`** — Returns `HandlerStatus` health check
|
|
157
|
+
- **`connected`** — `bool` property: whether currently connected
|
|
158
|
+
|
|
159
|
+
#### Events
|
|
160
|
+
|
|
161
|
+
| Event | Payload | Description |
|
|
162
|
+
|-------|---------|-------------|
|
|
163
|
+
| `message:created` | `DecryptedMessage` | New message received and decrypted |
|
|
164
|
+
| `message:deleted` | `DeletedMessage` | Message was deleted |
|
|
165
|
+
| `connected` | — | Connected/reconnected to Mercury |
|
|
166
|
+
| `disconnected` | `reason: str` | Disconnected from Mercury |
|
|
167
|
+
| `reconnecting` | `attempt: int` | Attempting to reconnect |
|
|
168
|
+
| `error` | `Exception` | Error occurred |
|
|
169
|
+
|
|
170
|
+
### `DecryptedMessage`
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
@dataclass
|
|
174
|
+
class DecryptedMessage:
|
|
175
|
+
id: str
|
|
176
|
+
room_id: str
|
|
177
|
+
person_id: str
|
|
178
|
+
person_email: str
|
|
179
|
+
text: str
|
|
180
|
+
created: str
|
|
181
|
+
html: str | None
|
|
182
|
+
room_type: str | None # "direct" | "group"
|
|
183
|
+
raw: MercuryActivity | None
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Architecture
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
WebexMessageHandler (orchestrator)
|
|
190
|
+
├── DeviceManager — WDM registration
|
|
191
|
+
├── MercurySocket — WebSocket + ping/pong + reconnect
|
|
192
|
+
├── KmsClient — ECDH handshake + key retrieval
|
|
193
|
+
└── MessageDecryptor — JWE decryption
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webex-message-handler"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|