webex-message-handler 0.2.0__tar.gz → 0.3.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.2.0 → webex_message_handler-0.3.1}/PKG-INFO +1 -1
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/README.md +187 -187
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/pyproject.toml +1 -1
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/__init__.py +13 -0
- webex_message_handler-0.3.1/src/webex_message_handler/device_manager.py +161 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/handler.py +457 -345
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/kms_client.py +420 -415
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/mercury_socket.py +401 -413
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/types.py +253 -183
- webex_message_handler-0.3.1/tests/conftest.py +38 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/tests/test_device_manager.py +29 -14
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/tests/test_handler.py +51 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/tests/test_integration.py +131 -131
- webex_message_handler-0.2.0/src/webex_message_handler/device_manager.py +0 -150
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/.gitignore +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/API.md +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/LICENSE +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/examples/basic_bot.py +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/errors.py +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/logger.py +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/message_decryptor.py +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/tests/__init__.py +0 -0
- {webex_message_handler-0.2.0 → webex_message_handler-0.3.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.3.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
|
|
@@ -1,187 +1,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
|
-
## Proxy Support (Enterprise)
|
|
72
|
-
|
|
73
|
-
For corporate environments behind a proxy, pass a configured connector:
|
|
74
|
-
|
|
75
|
-
```python
|
|
76
|
-
import aiohttp
|
|
77
|
-
from aiohttp_socks import ProxyConnector
|
|
78
|
-
|
|
79
|
-
# Using HTTP/HTTPS proxy
|
|
80
|
-
connector = ProxyConnector.from_url(
|
|
81
|
-
"http://proxy.example.com:8080"
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
handler = WebexMessageHandler(
|
|
85
|
-
WebexMessageHandlerConfig(
|
|
86
|
-
token="YOUR_BOT_TOKEN",
|
|
87
|
-
connector=connector, # Pass configured connector
|
|
88
|
-
logger=console_logger,
|
|
89
|
-
)
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
await handler.connect()
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Or using environment variables:
|
|
96
|
-
|
|
97
|
-
```python
|
|
98
|
-
import os
|
|
99
|
-
import aiohttp
|
|
100
|
-
from aiohttp_socks import ProxyConnector
|
|
101
|
-
|
|
102
|
-
proxy_url = os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY")
|
|
103
|
-
connector = ProxyConnector.from_url(proxy_url) if proxy_url else None
|
|
104
|
-
|
|
105
|
-
handler = WebexMessageHandler(
|
|
106
|
-
WebexMessageHandlerConfig(
|
|
107
|
-
token=os.getenv("WEBEX_BOT_TOKEN"),
|
|
108
|
-
connector=connector,
|
|
109
|
-
logger=console_logger,
|
|
110
|
-
)
|
|
111
|
-
)
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Requires: `pip install aiohttp-socks[asyncio]`
|
|
115
|
-
|
|
116
|
-
## API Reference
|
|
117
|
-
|
|
118
|
-
### `WebexMessageHandler`
|
|
119
|
-
|
|
120
|
-
Main class for receiving and decrypting Webex messages.
|
|
121
|
-
|
|
122
|
-
#### Constructor
|
|
123
|
-
|
|
124
|
-
```python
|
|
125
|
-
WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Configuration options:**
|
|
129
|
-
|
|
130
|
-
| Option | Type | Default | Description |
|
|
131
|
-
|--------|------|---------|-------------|
|
|
132
|
-
| `token` | `str` | required | Webex bot access token |
|
|
133
|
-
| `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
|
|
134
|
-
| `connector` | `aiohttp.BaseConnector` | `None` | HTTP/HTTPS connector for proxy support |
|
|
135
|
-
| `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
|
|
136
|
-
| `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
|
|
137
|
-
| `reconnect_backoff_max` | `float` | `32.0` | Max reconnect backoff (seconds) |
|
|
138
|
-
| `max_reconnect_attempts` | `int` | `10` | Max reconnect attempts |
|
|
139
|
-
|
|
140
|
-
#### Methods
|
|
141
|
-
|
|
142
|
-
- **`await connect()`** — Connects to Webex (registers device, initializes KMS, opens Mercury WebSocket)
|
|
143
|
-
- **`await disconnect()`** — Gracefully disconnects (closes WebSocket, unregisters device)
|
|
144
|
-
- **`await reconnect(new_token)`** — Update token and re-establish connection
|
|
145
|
-
- **`status()`** — Returns `HandlerStatus` health check
|
|
146
|
-
- **`connected`** — `bool` property: whether currently connected
|
|
147
|
-
|
|
148
|
-
#### Events
|
|
149
|
-
|
|
150
|
-
| Event | Payload | Description |
|
|
151
|
-
|-------|---------|-------------|
|
|
152
|
-
| `message:created` | `DecryptedMessage` | New message received and decrypted |
|
|
153
|
-
| `message:deleted` | `DeletedMessage` | Message was deleted |
|
|
154
|
-
| `connected` | — | Connected/reconnected to Mercury |
|
|
155
|
-
| `disconnected` | `reason: str` | Disconnected from Mercury |
|
|
156
|
-
| `reconnecting` | `attempt: int` | Attempting to reconnect |
|
|
157
|
-
| `error` | `Exception` | Error occurred |
|
|
158
|
-
|
|
159
|
-
### `DecryptedMessage`
|
|
160
|
-
|
|
161
|
-
```python
|
|
162
|
-
@dataclass
|
|
163
|
-
class DecryptedMessage:
|
|
164
|
-
id: str
|
|
165
|
-
room_id: str
|
|
166
|
-
person_id: str
|
|
167
|
-
person_email: str
|
|
168
|
-
text: str
|
|
169
|
-
created: str
|
|
170
|
-
html: str | None
|
|
171
|
-
room_type: str | None # "direct" | "group"
|
|
172
|
-
raw: MercuryActivity | None
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Architecture
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
WebexMessageHandler (orchestrator)
|
|
179
|
-
├── DeviceManager — WDM registration
|
|
180
|
-
├── MercurySocket — WebSocket + ping/pong + reconnect
|
|
181
|
-
├── KmsClient — ECDH handshake + key retrieval
|
|
182
|
-
└── MessageDecryptor — JWE decryption
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## License
|
|
186
|
-
|
|
187
|
-
MIT
|
|
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
|
+
## Proxy Support (Enterprise)
|
|
72
|
+
|
|
73
|
+
For corporate environments behind a proxy, pass a configured connector:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import aiohttp
|
|
77
|
+
from aiohttp_socks import ProxyConnector
|
|
78
|
+
|
|
79
|
+
# Using HTTP/HTTPS proxy
|
|
80
|
+
connector = ProxyConnector.from_url(
|
|
81
|
+
"http://proxy.example.com:8080"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
handler = WebexMessageHandler(
|
|
85
|
+
WebexMessageHandlerConfig(
|
|
86
|
+
token="YOUR_BOT_TOKEN",
|
|
87
|
+
connector=connector, # Pass configured connector
|
|
88
|
+
logger=console_logger,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
await handler.connect()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or using environment variables:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import os
|
|
99
|
+
import aiohttp
|
|
100
|
+
from aiohttp_socks import ProxyConnector
|
|
101
|
+
|
|
102
|
+
proxy_url = os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY")
|
|
103
|
+
connector = ProxyConnector.from_url(proxy_url) if proxy_url else None
|
|
104
|
+
|
|
105
|
+
handler = WebexMessageHandler(
|
|
106
|
+
WebexMessageHandlerConfig(
|
|
107
|
+
token=os.getenv("WEBEX_BOT_TOKEN"),
|
|
108
|
+
connector=connector,
|
|
109
|
+
logger=console_logger,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Requires: `pip install aiohttp-socks[asyncio]`
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `WebexMessageHandler`
|
|
119
|
+
|
|
120
|
+
Main class for receiving and decrypting Webex messages.
|
|
121
|
+
|
|
122
|
+
#### Constructor
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
WebexMessageHandler(config: WebexMessageHandlerConfig)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Configuration options:**
|
|
129
|
+
|
|
130
|
+
| Option | Type | Default | Description |
|
|
131
|
+
|--------|------|---------|-------------|
|
|
132
|
+
| `token` | `str` | required | Webex bot access token |
|
|
133
|
+
| `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
|
|
134
|
+
| `connector` | `aiohttp.BaseConnector` | `None` | HTTP/HTTPS connector for proxy support |
|
|
135
|
+
| `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
|
|
136
|
+
| `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
|
|
137
|
+
| `reconnect_backoff_max` | `float` | `32.0` | Max reconnect backoff (seconds) |
|
|
138
|
+
| `max_reconnect_attempts` | `int` | `10` | Max reconnect attempts |
|
|
139
|
+
|
|
140
|
+
#### Methods
|
|
141
|
+
|
|
142
|
+
- **`await connect()`** — Connects to Webex (registers device, initializes KMS, opens Mercury WebSocket)
|
|
143
|
+
- **`await disconnect()`** — Gracefully disconnects (closes WebSocket, unregisters device)
|
|
144
|
+
- **`await reconnect(new_token)`** — Update token and re-establish connection
|
|
145
|
+
- **`status()`** — Returns `HandlerStatus` health check
|
|
146
|
+
- **`connected`** — `bool` property: whether currently connected
|
|
147
|
+
|
|
148
|
+
#### Events
|
|
149
|
+
|
|
150
|
+
| Event | Payload | Description |
|
|
151
|
+
|-------|---------|-------------|
|
|
152
|
+
| `message:created` | `DecryptedMessage` | New message received and decrypted |
|
|
153
|
+
| `message:deleted` | `DeletedMessage` | Message was deleted |
|
|
154
|
+
| `connected` | — | Connected/reconnected to Mercury |
|
|
155
|
+
| `disconnected` | `reason: str` | Disconnected from Mercury |
|
|
156
|
+
| `reconnecting` | `attempt: int` | Attempting to reconnect |
|
|
157
|
+
| `error` | `Exception` | Error occurred |
|
|
158
|
+
|
|
159
|
+
### `DecryptedMessage`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
@dataclass
|
|
163
|
+
class DecryptedMessage:
|
|
164
|
+
id: str
|
|
165
|
+
room_id: str
|
|
166
|
+
person_id: str
|
|
167
|
+
person_email: str
|
|
168
|
+
text: str
|
|
169
|
+
created: str
|
|
170
|
+
html: str | None
|
|
171
|
+
room_type: str | None # "direct" | "group"
|
|
172
|
+
raw: MercuryActivity | None
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Architecture
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
WebexMessageHandler (orchestrator)
|
|
179
|
+
├── DeviceManager — WDM registration
|
|
180
|
+
├── MercurySocket — WebSocket + ping/pong + reconnect
|
|
181
|
+
├── KmsClient — ECDH handshake + key retrieval
|
|
182
|
+
└── MessageDecryptor — JWE decryption
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
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.3.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"
|
{webex_message_handler-0.2.0 → webex_message_handler-0.3.1}/src/webex_message_handler/__init__.py
RENAMED
|
@@ -19,13 +19,19 @@ from .types import (
|
|
|
19
19
|
DecryptedMessage,
|
|
20
20
|
DeletedMessage,
|
|
21
21
|
DeviceRegistration,
|
|
22
|
+
FetchFunction,
|
|
23
|
+
FetchRequest,
|
|
24
|
+
FetchResponse,
|
|
22
25
|
HandlerStatus,
|
|
26
|
+
InjectedWebSocket,
|
|
23
27
|
MercuryActivity,
|
|
24
28
|
MercuryActor,
|
|
25
29
|
MercuryEnvelope,
|
|
26
30
|
MercuryObject,
|
|
27
31
|
MercuryTarget,
|
|
32
|
+
NetworkMode,
|
|
28
33
|
WebexMessageHandlerConfig,
|
|
34
|
+
WebSocketFactory,
|
|
29
35
|
)
|
|
30
36
|
|
|
31
37
|
__all__ = [
|
|
@@ -59,4 +65,11 @@ __all__ = [
|
|
|
59
65
|
"DeletedMessage",
|
|
60
66
|
"HandlerStatus",
|
|
61
67
|
"ConnectionStatus",
|
|
68
|
+
# Networking types
|
|
69
|
+
"NetworkMode",
|
|
70
|
+
"FetchRequest",
|
|
71
|
+
"FetchResponse",
|
|
72
|
+
"FetchFunction",
|
|
73
|
+
"InjectedWebSocket",
|
|
74
|
+
"WebSocketFactory",
|
|
62
75
|
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""WDM device registration, refresh, and unregistration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .errors import AuthError, DeviceRegistrationError
|
|
9
|
+
from .logger import Logger, noop_logger
|
|
10
|
+
from .types import DeviceRegistration, FetchFunction, FetchRequest
|
|
11
|
+
|
|
12
|
+
WDM_API_BASE = "https://wdm-a.wbx2.com/wdm/api/v1/devices"
|
|
13
|
+
|
|
14
|
+
_DEVICE_BODY = {
|
|
15
|
+
"deviceName": "webex-message-handler",
|
|
16
|
+
"deviceType": "DESKTOP",
|
|
17
|
+
"localizedModel": "python",
|
|
18
|
+
"model": "python",
|
|
19
|
+
"name": "webex-message-handler",
|
|
20
|
+
"systemName": "webex-message-handler",
|
|
21
|
+
"systemVersion": "1.0.0",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeviceManager:
|
|
26
|
+
"""Manages WDM device registration lifecycle."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
logger: Logger | None = None,
|
|
32
|
+
http_do: FetchFunction,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._logger: Logger = logger or noop_logger # type: ignore[assignment]
|
|
35
|
+
self._http_do = http_do
|
|
36
|
+
self._device_url: str | None = None
|
|
37
|
+
|
|
38
|
+
async def register(self, token: str) -> DeviceRegistration:
|
|
39
|
+
"""Register a new device with WDM."""
|
|
40
|
+
self._logger.debug("Registering device with WDM")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
response = await self._http_do(
|
|
44
|
+
FetchRequest(
|
|
45
|
+
url=WDM_API_BASE,
|
|
46
|
+
method="POST",
|
|
47
|
+
headers={
|
|
48
|
+
"Authorization": f"Bearer {token}",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
body=json.dumps(_DEVICE_BODY),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if response.status == 401:
|
|
56
|
+
self._logger.error("Device registration failed: Unauthorized")
|
|
57
|
+
raise AuthError("Unauthorized to register device")
|
|
58
|
+
|
|
59
|
+
if not response.ok:
|
|
60
|
+
self._logger.error(f"Device registration failed with status {response.status}")
|
|
61
|
+
raise DeviceRegistrationError("Failed to register device", response.status)
|
|
62
|
+
|
|
63
|
+
data = await response.json()
|
|
64
|
+
self._device_url = data["url"]
|
|
65
|
+
registration = self._parse_device_response(data)
|
|
66
|
+
self._logger.info("Device registered successfully")
|
|
67
|
+
return registration
|
|
68
|
+
|
|
69
|
+
except (AuthError, DeviceRegistrationError):
|
|
70
|
+
raise
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
self._logger.error(f"Device registration error: {exc}")
|
|
73
|
+
raise DeviceRegistrationError("Failed to register device") from exc
|
|
74
|
+
|
|
75
|
+
async def refresh(self, token: str) -> DeviceRegistration:
|
|
76
|
+
"""Refresh an existing device registration."""
|
|
77
|
+
if not self._device_url:
|
|
78
|
+
raise DeviceRegistrationError("Device not registered. Call register() first.")
|
|
79
|
+
|
|
80
|
+
self._logger.debug("Refreshing device registration")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
response = await self._http_do(
|
|
84
|
+
FetchRequest(
|
|
85
|
+
url=self._device_url,
|
|
86
|
+
method="PUT",
|
|
87
|
+
headers={
|
|
88
|
+
"Authorization": f"Bearer {token}",
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
},
|
|
91
|
+
body=json.dumps(_DEVICE_BODY),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if response.status == 401:
|
|
96
|
+
self._logger.error("Device refresh failed: Unauthorized")
|
|
97
|
+
raise AuthError("Unauthorized to refresh device")
|
|
98
|
+
|
|
99
|
+
if not response.ok:
|
|
100
|
+
self._logger.error(f"Device refresh failed with status {response.status}")
|
|
101
|
+
raise DeviceRegistrationError("Failed to refresh device", response.status)
|
|
102
|
+
|
|
103
|
+
data = await response.json()
|
|
104
|
+
registration = self._parse_device_response(data)
|
|
105
|
+
self._logger.info("Device refreshed successfully")
|
|
106
|
+
return registration
|
|
107
|
+
|
|
108
|
+
except (AuthError, DeviceRegistrationError):
|
|
109
|
+
raise
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
self._logger.error(f"Device refresh error: {exc}")
|
|
112
|
+
raise DeviceRegistrationError("Failed to refresh device") from exc
|
|
113
|
+
|
|
114
|
+
async def unregister(self, token: str) -> None:
|
|
115
|
+
"""Unregister the device from WDM."""
|
|
116
|
+
if not self._device_url:
|
|
117
|
+
raise DeviceRegistrationError("Device not registered. Call register() first.")
|
|
118
|
+
|
|
119
|
+
self._logger.debug("Unregistering device")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
response = await self._http_do(
|
|
123
|
+
FetchRequest(
|
|
124
|
+
url=self._device_url,
|
|
125
|
+
method="DELETE",
|
|
126
|
+
headers={
|
|
127
|
+
"Authorization": f"Bearer {token}",
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if response.status == 401:
|
|
134
|
+
self._logger.error("Device unregistration failed: Unauthorized")
|
|
135
|
+
raise AuthError("Unauthorized to unregister device")
|
|
136
|
+
|
|
137
|
+
if not response.ok:
|
|
138
|
+
self._logger.error(f"Device unregistration failed with status {response.status}")
|
|
139
|
+
raise DeviceRegistrationError("Failed to unregister device", response.status)
|
|
140
|
+
|
|
141
|
+
self._device_url = None
|
|
142
|
+
self._logger.info("Device unregistered successfully")
|
|
143
|
+
|
|
144
|
+
except (AuthError, DeviceRegistrationError):
|
|
145
|
+
raise
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
self._logger.error(f"Device unregistration error: {exc}")
|
|
148
|
+
raise DeviceRegistrationError("Failed to unregister device") from exc
|
|
149
|
+
|
|
150
|
+
def _parse_device_response(self, data: dict[str, Any]) -> DeviceRegistration:
|
|
151
|
+
services: dict[str, str] = data.get("services", {})
|
|
152
|
+
if not isinstance(services, dict):
|
|
153
|
+
services = {}
|
|
154
|
+
|
|
155
|
+
return DeviceRegistration(
|
|
156
|
+
web_socket_url=data["webSocketUrl"],
|
|
157
|
+
device_url=data["url"],
|
|
158
|
+
user_id=data["userId"],
|
|
159
|
+
services=services,
|
|
160
|
+
encryption_service_url=services.get("encryptionServiceUrl", ""),
|
|
161
|
+
)
|