webex-message-handler 0.3.1__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.
Files changed (22) hide show
  1. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/PKG-INFO +12 -1
  2. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/README.md +11 -0
  3. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/pyproject.toml +1 -1
  4. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/handler.py +50 -21
  5. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/tests/test_handler.py +2 -0
  6. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/.gitignore +0 -0
  7. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/API.md +0 -0
  8. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/LICENSE +0 -0
  9. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/examples/basic_bot.py +0 -0
  10. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/__init__.py +0 -0
  11. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/device_manager.py +0 -0
  12. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/errors.py +0 -0
  13. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/kms_client.py +0 -0
  14. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/logger.py +0 -0
  15. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/mercury_socket.py +0 -0
  16. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/message_decryptor.py +0 -0
  17. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/src/webex_message_handler/types.py +0 -0
  18. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/tests/__init__.py +0 -0
  19. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/tests/conftest.py +0 -0
  20. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/tests/test_device_manager.py +0 -0
  21. {webex_message_handler-0.3.1 → webex_message_handler-0.4.1}/tests/test_integration.py +0 -0
  22. {webex_message_handler-0.3.1 → 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.1
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) |
@@ -68,6 +68,16 @@ asyncio.run(main())
68
68
 
69
69
  See `examples/basic_bot.py` for a complete working example.
70
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
+
71
81
  ## Proxy Support (Enterprise)
72
82
 
73
83
  For corporate environments behind a proxy, pass a configured connector:
@@ -131,6 +141,7 @@ WebexMessageHandler(config: WebexMessageHandlerConfig)
131
141
  |--------|------|---------|-------------|
132
142
  | `token` | `str` | required | Webex bot access token |
133
143
  | `logger` | `Logger` | noop | Custom logger (`console_logger` provided) |
144
+ | `ignore_self_messages` | `bool` | `True` | Filter out messages sent by this bot |
134
145
  | `connector` | `aiohttp.BaseConnector` | `None` | HTTP/HTTPS connector for proxy support |
135
146
  | `ping_interval` | `float` | `15.0` | Mercury ping interval (seconds) |
136
147
  | `pong_timeout` | `float` | `14.0` | Pong response timeout (seconds) |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "webex-message-handler"
7
- version = "0.3.1"
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"
@@ -31,10 +31,35 @@ from .types import (
31
31
  if TYPE_CHECKING:
32
32
  pass
33
33
 
34
+ import base64
35
+
34
36
  # Type alias for event callbacks
35
37
  EventCallback = Callable[..., Any]
36
38
 
37
39
 
40
+ def extract_person_uuid(person_id: str) -> str:
41
+ """Extract the raw UUID from a Webex person ID.
42
+
43
+ The Webex REST API returns base64-encoded IDs like:
44
+ "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mYjUx..." → "ciscospark://us/PEOPLE/fb51254f-..."
45
+
46
+ Mercury wire format uses raw UUIDs:
47
+ "fb51254f-3b37-4e50-aa04-45744c2effc7"
48
+
49
+ This function normalizes both formats to the raw UUID for comparison.
50
+ """
51
+ try:
52
+ decoded = base64.b64decode(person_id).decode("utf-8")
53
+ if decoded.startswith("ciscospark://"):
54
+ uuid = decoded.rsplit("/", 1)[-1]
55
+ if uuid:
56
+ return uuid
57
+ except Exception:
58
+ # Not base64 — treat as raw UUID
59
+ pass
60
+ return person_id
61
+
62
+
38
63
  class WebexMessageHandler:
39
64
  """Receives and decrypts Webex messages over Mercury WebSocket.
40
65
 
@@ -321,27 +346,31 @@ class WebexMessageHandler:
321
346
  )
322
347
 
323
348
  async def _fetch_bot_person_id(self) -> None:
324
- """Fetch the bot's person ID for self-message filtering."""
325
- try:
326
- self._logger.debug("Fetching bot person info for self-message filtering")
327
- response = await self._http_do(
328
- FetchRequest(
329
- url="https://webexapis.com/v1/people/me",
330
- method="GET",
331
- headers={
332
- "Authorization": f"Bearer {self._token}",
333
- "Content-Type": "application/json",
334
- },
335
- )
349
+ """Fetch the bot's person ID for self-message filtering.
350
+
351
+ Raises on failure — connect() will not proceed without a valid bot ID
352
+ when ignore_self_messages is enabled.
353
+ """
354
+ self._logger.debug("Fetching bot person info for self-message filtering")
355
+ response = await self._http_do(
356
+ FetchRequest(
357
+ url="https://webexapis.com/v1/people/me",
358
+ method="GET",
359
+ headers={
360
+ "Authorization": f"Bearer {self._token}",
361
+ "Content-Type": "application/json",
362
+ },
336
363
  )
337
- if not response.ok:
338
- self._logger.warning(f"Failed to fetch bot person info: HTTP {response.status}")
339
- return
340
- data = await response.json()
341
- self._bot_person_id = data.get("id")
342
- self._logger.info(f"Bot person ID cached for self-message filtering: {self._bot_person_id}")
343
- except Exception as exc:
344
- self._logger.warning(f"Error fetching bot person info: {exc}")
364
+ )
365
+ if not response.ok:
366
+ raise RuntimeError(
367
+ f"Failed to fetch bot identity for self-message filtering: HTTP {response.status}. "
368
+ "Set ignore_self_messages=False to skip this check (not recommended — may cause message loops)."
369
+ )
370
+ data = await response.json()
371
+ raw_id = data.get("id", "")
372
+ self._bot_person_id = extract_person_uuid(raw_id)
373
+ self._logger.info(f"Bot person ID cached for self-message filtering: {self._bot_person_id}")
345
374
 
346
375
  def _setup_mercury_listeners(self) -> None:
347
376
  # Forward KMS messages from Mercury to the KMS client
@@ -409,7 +438,7 @@ class WebexMessageHandler:
409
438
  raw=decrypted,
410
439
  )
411
440
  # Filter self-messages if enabled
412
- if self._ignore_self_messages and self._bot_person_id and message.person_id == self._bot_person_id:
441
+ if self._ignore_self_messages and self._bot_person_id and extract_person_uuid(message.person_id) == self._bot_person_id:
413
442
  self._logger.debug(f"Ignoring self-message from bot ({self._bot_person_id})")
414
443
  return
415
444
 
@@ -130,6 +130,7 @@ class TestConnect:
130
130
  mock_kms_cls.return_value = mock_kms
131
131
 
132
132
  handler = _make_handler()
133
+ handler._fetch_bot_person_id = AsyncMock()
133
134
  connected_events = []
134
135
  handler.on("connected", lambda: connected_events.append(True))
135
136
 
@@ -183,6 +184,7 @@ class TestDisconnect:
183
184
  mock_kms_cls.return_value = mock_kms
184
185
 
185
186
  handler = _make_handler()
187
+ handler._fetch_bot_person_id = AsyncMock()
186
188
  await handler.connect()
187
189
  await handler.disconnect()
188
190