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.
Files changed (26) hide show
  1. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/PKG-INFO +1 -1
  2. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/pyproject.toml +1 -1
  3. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/__init__.py +6 -0
  4. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/handler.py +3 -2
  5. webex_message_handler-0.6.4/src/webex_message_handler/id_utils.py +39 -0
  6. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/mercury_socket.py +10 -1
  7. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/types.py +13 -1
  8. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/test_handler.py +21 -1
  9. webex_message_handler-0.6.4/tests/test_id_utils.py +37 -0
  10. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/.gitignore +0 -0
  11. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/API.md +0 -0
  12. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/LICENSE +0 -0
  13. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/README.md +0 -0
  14. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/examples/basic_bot.py +0 -0
  15. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/device_manager.py +0 -0
  16. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/errors.py +0 -0
  17. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/kms_client.py +0 -0
  18. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/logger.py +0 -0
  19. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/message_decryptor.py +0 -0
  20. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/src/webex_message_handler/url_validation.py +0 -0
  21. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/test-proxy.py +0 -0
  22. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/__init__.py +0 -0
  23. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/conftest.py +0 -0
  24. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/test_device_manager.py +0 -0
  25. {webex_message_handler-0.6.3 → webex_message_handler-0.6.4}/tests/test_integration.py +0 -0
  26. {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
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.3"
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"
@@ -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",
@@ -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.object.id,
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
  )
@@ -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
- """Unique message ID."""
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 == "comment-789"
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)