webex-message-handler 0.6.3__tar.gz → 0.6.4__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.6.3 → webex_message_handler-0.6.4}/PKG-INFO +1 -1
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/pyproject.toml +1 -1
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/__init__.py +6 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/handler.py +3 -2
- webex_message_handler-0.6.4/src/webex_message_handler/id_utils.py +39 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/mercury_socket.py +10 -1
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/types.py +13 -1
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/test_handler.py +21 -1
- webex_message_handler-0.6.4/tests/test_id_utils.py +37 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/.gitignore +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/API.md +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/LICENSE +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/README.md +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/examples/basic_bot.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/device_manager.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/errors.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/kms_client.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/logger.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/message_decryptor.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/url_validation.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/test-proxy.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/__init__.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/conftest.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/test_device_manager.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/test_integration.py +0 -0
- {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/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.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webex-message-handler"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.4"
|
|
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.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/__init__.py
RENAMED
|
@@ -10,6 +10,7 @@ from .errors import (
|
|
|
10
10
|
WebexError,
|
|
11
11
|
)
|
|
12
12
|
from .handler import WebexMessageHandler
|
|
13
|
+
from .id_utils import from_rest_id, to_rest_id
|
|
13
14
|
from .kms_client import KmsClient
|
|
14
15
|
from .logger import Logger, console_logger, noop_logger
|
|
15
16
|
from .mercury_socket import MercurySocket
|
|
@@ -29,6 +30,7 @@ from .types import (
|
|
|
29
30
|
MercuryActor,
|
|
30
31
|
MercuryEnvelope,
|
|
31
32
|
MercuryObject,
|
|
33
|
+
MercuryParent,
|
|
32
34
|
MercuryTarget,
|
|
33
35
|
NetworkMode,
|
|
34
36
|
WebexMessageHandlerConfig,
|
|
@@ -54,11 +56,15 @@ __all__ = [
|
|
|
54
56
|
"Logger",
|
|
55
57
|
"noop_logger",
|
|
56
58
|
"console_logger",
|
|
59
|
+
# ID utilities
|
|
60
|
+
"to_rest_id",
|
|
61
|
+
"from_rest_id",
|
|
57
62
|
# Types
|
|
58
63
|
"WebexMessageHandlerConfig",
|
|
59
64
|
"DeviceRegistration",
|
|
60
65
|
"MercuryActor",
|
|
61
66
|
"MercuryObject",
|
|
67
|
+
"MercuryParent",
|
|
62
68
|
"MercuryTarget",
|
|
63
69
|
"MercuryActivity",
|
|
64
70
|
"MercuryEnvelope",
|
{webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/handler.py
RENAMED
|
@@ -463,13 +463,14 @@ class WebexMessageHandler:
|
|
|
463
463
|
|
|
464
464
|
decrypted = await self._message_decryptor.decrypt_activity(activity)
|
|
465
465
|
message = DecryptedMessage(
|
|
466
|
-
id=decrypted.
|
|
466
|
+
id=decrypted.id,
|
|
467
467
|
room_id=decrypted.target.id,
|
|
468
468
|
person_id=decrypted.actor.id,
|
|
469
469
|
person_email=decrypted.actor.email_address or "",
|
|
470
470
|
text=decrypted.object.display_name or "",
|
|
471
|
-
html=decrypted.object.content,
|
|
472
471
|
created=decrypted.published,
|
|
472
|
+
parent_id=decrypted.parent.id if decrypted.parent else None,
|
|
473
|
+
html=decrypted.object.content,
|
|
473
474
|
room_type=self._infer_room_type(decrypted),
|
|
474
475
|
raw=decrypted,
|
|
475
476
|
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Convert between Mercury activity UUIDs and Webex REST API IDs."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def to_rest_id(uuid: str, resource_type: str) -> str:
|
|
7
|
+
"""Convert a Mercury activity UUID to a Webex REST API ID.
|
|
8
|
+
|
|
9
|
+
Mercury uses raw UUIDs; the REST API uses base64-encoded
|
|
10
|
+
``ciscospark://us/{type}/{uuid}`` URIs.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
uuid: Mercury UUID (e.g. activity.id).
|
|
14
|
+
resource_type: Resource type — 'MESSAGE', 'PEOPLE', or 'ROOM'.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
REST API–compatible ID string.
|
|
18
|
+
"""
|
|
19
|
+
uri = f"ciscospark://us/{resource_type}/{uuid}"
|
|
20
|
+
return base64.b64encode(uri.encode()).decode()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def from_rest_id(rest_id: str) -> str:
|
|
24
|
+
"""Convert a Webex REST API ID back to a raw UUID.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
rest_id: Base64-encoded REST API ID.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The raw UUID portion.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the format is invalid.
|
|
34
|
+
"""
|
|
35
|
+
decoded = base64.b64decode(rest_id).decode()
|
|
36
|
+
last_slash = decoded.rfind("/")
|
|
37
|
+
if last_slash == -1:
|
|
38
|
+
raise ValueError(f"Invalid REST ID format: {rest_id}")
|
|
39
|
+
return decoded[last_slash + 1 :]
|
|
@@ -15,7 +15,7 @@ import aiohttp
|
|
|
15
15
|
|
|
16
16
|
from .errors import AuthError, MercuryConnectionError
|
|
17
17
|
from .logger import Logger, noop_logger
|
|
18
|
-
from .types import InjectedWebSocket, MercuryActivity, MercuryActor, MercuryObject, MercuryTarget, WebSocketFactory
|
|
18
|
+
from .types import InjectedWebSocket, MercuryActivity, MercuryActor, MercuryObject, MercuryParent, MercuryTarget, WebSocketFactory
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class MercurySocket:
|
|
@@ -388,6 +388,14 @@ def _parse_activity(raw: dict[str, Any]) -> MercuryActivity:
|
|
|
388
388
|
actor_raw = raw.get("actor", {})
|
|
389
389
|
object_raw = raw.get("object", {})
|
|
390
390
|
target_raw = raw.get("target", {})
|
|
391
|
+
parent_raw = raw.get("parent")
|
|
392
|
+
|
|
393
|
+
parent = None
|
|
394
|
+
if parent_raw:
|
|
395
|
+
parent = MercuryParent(
|
|
396
|
+
id=parent_raw.get("id", ""),
|
|
397
|
+
type=parent_raw.get("type", ""),
|
|
398
|
+
)
|
|
391
399
|
|
|
392
400
|
return MercuryActivity(
|
|
393
401
|
id=raw.get("id", ""),
|
|
@@ -412,4 +420,5 @@ def _parse_activity(raw: dict[str, Any]) -> MercuryActivity:
|
|
|
412
420
|
),
|
|
413
421
|
published=raw.get("published", ""),
|
|
414
422
|
encryption_key_url=raw.get("encryptionKeyUrl"),
|
|
423
|
+
parent=parent,
|
|
415
424
|
)
|
{webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/types.py
RENAMED
|
@@ -132,6 +132,14 @@ class DeviceRegistration:
|
|
|
132
132
|
|
|
133
133
|
# --- Mercury Activity ---
|
|
134
134
|
|
|
135
|
+
@dataclass
|
|
136
|
+
class MercuryParent:
|
|
137
|
+
"""Parent reference for threaded replies."""
|
|
138
|
+
|
|
139
|
+
id: str
|
|
140
|
+
type: str
|
|
141
|
+
|
|
142
|
+
|
|
135
143
|
@dataclass
|
|
136
144
|
class MercuryActor:
|
|
137
145
|
"""Actor in a Mercury activity."""
|
|
@@ -173,6 +181,7 @@ class MercuryActivity:
|
|
|
173
181
|
target: MercuryTarget
|
|
174
182
|
published: str
|
|
175
183
|
encryption_key_url: str | None = None
|
|
184
|
+
parent: MercuryParent | None = None
|
|
176
185
|
|
|
177
186
|
|
|
178
187
|
@dataclass
|
|
@@ -193,7 +202,7 @@ class DecryptedMessage:
|
|
|
193
202
|
"""A decrypted Webex message."""
|
|
194
203
|
|
|
195
204
|
id: str
|
|
196
|
-
"""
|
|
205
|
+
"""Mercury activity UUID. Works as parentId for threaded replies."""
|
|
197
206
|
|
|
198
207
|
room_id: str
|
|
199
208
|
"""Conversation/space ID."""
|
|
@@ -210,6 +219,9 @@ class DecryptedMessage:
|
|
|
210
219
|
created: str
|
|
211
220
|
"""ISO 8601 timestamp."""
|
|
212
221
|
|
|
222
|
+
parent_id: str | None = None
|
|
223
|
+
"""Parent activity UUID for threaded replies. None if not a thread reply."""
|
|
224
|
+
|
|
213
225
|
html: str | None = None
|
|
214
226
|
"""Decrypted HTML content (rich text messages)."""
|
|
215
227
|
|
|
@@ -10,6 +10,7 @@ from webex_message_handler.types import (
|
|
|
10
10
|
MercuryActivity,
|
|
11
11
|
MercuryActor,
|
|
12
12
|
MercuryObject,
|
|
13
|
+
MercuryParent,
|
|
13
14
|
MercuryTarget,
|
|
14
15
|
WebexMessageHandlerConfig,
|
|
15
16
|
)
|
|
@@ -228,13 +229,32 @@ class TestMessageHandling:
|
|
|
228
229
|
|
|
229
230
|
assert len(messages) == 1
|
|
230
231
|
msg = messages[0]
|
|
231
|
-
assert msg.id == "
|
|
232
|
+
assert msg.id == "msg-123"
|
|
232
233
|
assert msg.room_id == "room-101"
|
|
233
234
|
assert msg.person_id == "person-456"
|
|
234
235
|
assert msg.person_email == "user@example.com"
|
|
235
236
|
assert msg.text == "Test Message"
|
|
236
237
|
assert msg.html == "<p>Test Message</p>"
|
|
237
238
|
assert msg.room_type == "group"
|
|
239
|
+
assert msg.parent_id is None
|
|
240
|
+
|
|
241
|
+
async def test_handle_threaded_reply(self):
|
|
242
|
+
handler = _make_handler()
|
|
243
|
+
handler._message_decryptor = MagicMock()
|
|
244
|
+
activity = _make_activity(
|
|
245
|
+
parent=MercuryParent(id="parent-activity-uuid", type="reply"),
|
|
246
|
+
)
|
|
247
|
+
handler._message_decryptor.decrypt_activity = AsyncMock(return_value=activity)
|
|
248
|
+
|
|
249
|
+
messages = []
|
|
250
|
+
handler.on("message:created", lambda msg: messages.append(msg))
|
|
251
|
+
|
|
252
|
+
await handler._handle_activity(activity)
|
|
253
|
+
|
|
254
|
+
assert len(messages) == 1
|
|
255
|
+
msg = messages[0]
|
|
256
|
+
assert msg.id == "msg-123"
|
|
257
|
+
assert msg.parent_id == "parent-activity-uuid"
|
|
238
258
|
|
|
239
259
|
async def test_handle_message_deleted(self):
|
|
240
260
|
handler = _make_handler()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Tests for ID conversion utilities."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from webex_message_handler.id_utils import from_rest_id, to_rest_id
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestToRestId:
|
|
9
|
+
def test_roundtrip(self):
|
|
10
|
+
uuid = "abc-123-def"
|
|
11
|
+
rest_id = to_rest_id(uuid, "MESSAGE")
|
|
12
|
+
assert from_rest_id(rest_id) == uuid
|
|
13
|
+
|
|
14
|
+
def test_resource_types(self):
|
|
15
|
+
for rt in ("MESSAGE", "PEOPLE", "ROOM"):
|
|
16
|
+
rest_id = to_rest_id("uuid-1", rt)
|
|
17
|
+
assert from_rest_id(rest_id) == "uuid-1"
|
|
18
|
+
|
|
19
|
+
def test_non_empty(self):
|
|
20
|
+
rest_id = to_rest_id("test-uuid", "MESSAGE")
|
|
21
|
+
assert rest_id != ""
|
|
22
|
+
assert rest_id != "test-uuid"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestFromRestId:
|
|
26
|
+
def test_invalid_base64(self):
|
|
27
|
+
# Not valid base64 — should raise
|
|
28
|
+
with pytest.raises(Exception):
|
|
29
|
+
from_rest_id("!!!invalid!!!")
|
|
30
|
+
|
|
31
|
+
def test_invalid_format(self):
|
|
32
|
+
# Valid base64 but no slash in decoded string
|
|
33
|
+
import base64
|
|
34
|
+
|
|
35
|
+
encoded = base64.b64encode(b"noslash").decode()
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
from_rest_id(encoded)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/errors.py
RENAMED
|
File without changes
|
{webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/kms_client.py
RENAMED
|
File without changes
|
{webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|