webex-message-handler 0.4.4__tar.gz → 0.5.0__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.4.4 → webex_message_handler-0.5.0}/PKG-INFO +1 -1
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/pyproject.toml +1 -1
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/__init__.py +2 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/handler.py +25 -1
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/kms_client.py +27 -18
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/mercury_socket.py +5 -8
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/message_decryptor.py +10 -2
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/types.py +31 -1
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/tests/conftest.py +5 -3
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/tests/test_handler.py +116 -2
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/tests/test_integration.py +3 -1
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/tests/test_message_decryptor.py +0 -1
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/.gitignore +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/API.md +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/LICENSE +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/README.md +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/examples/basic_bot.py +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/device_manager.py +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/errors.py +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/logger.py +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/tests/__init__.py +0 -0
- {webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/tests/test_device_manager.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.5.0
|
|
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.
|
|
7
|
+
version = "0.5.0"
|
|
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.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/__init__.py
RENAMED
|
@@ -24,6 +24,7 @@ from .types import (
|
|
|
24
24
|
FetchResponse,
|
|
25
25
|
HandlerStatus,
|
|
26
26
|
InjectedWebSocket,
|
|
27
|
+
MembershipActivity,
|
|
27
28
|
MercuryActivity,
|
|
28
29
|
MercuryActor,
|
|
29
30
|
MercuryEnvelope,
|
|
@@ -63,6 +64,7 @@ __all__ = [
|
|
|
63
64
|
"MercuryEnvelope",
|
|
64
65
|
"DecryptedMessage",
|
|
65
66
|
"DeletedMessage",
|
|
67
|
+
"MembershipActivity",
|
|
66
68
|
"HandlerStatus",
|
|
67
69
|
"ConnectionStatus",
|
|
68
70
|
# Networking types
|
{webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/handler.py
RENAMED
|
@@ -23,6 +23,7 @@ from .types import (
|
|
|
23
23
|
FetchResponse,
|
|
24
24
|
HandlerStatus,
|
|
25
25
|
InjectedWebSocket,
|
|
26
|
+
MembershipActivity,
|
|
26
27
|
MercuryActivity,
|
|
27
28
|
WebexMessageHandlerConfig,
|
|
28
29
|
WebSocketFactory,
|
|
@@ -131,6 +132,7 @@ class WebexMessageHandler:
|
|
|
131
132
|
self._listeners: dict[str, list[EventCallback]] = {
|
|
132
133
|
"message:created": [],
|
|
133
134
|
"message:deleted": [],
|
|
135
|
+
"membership:created": [],
|
|
134
136
|
"connected": [],
|
|
135
137
|
"disconnected": [],
|
|
136
138
|
"reconnecting": [],
|
|
@@ -452,7 +454,11 @@ class WebexMessageHandler:
|
|
|
452
454
|
raw=decrypted,
|
|
453
455
|
)
|
|
454
456
|
# Filter self-messages if enabled
|
|
455
|
-
if
|
|
457
|
+
if (
|
|
458
|
+
self._ignore_self_messages
|
|
459
|
+
and self._bot_person_id
|
|
460
|
+
and extract_person_uuid(message.person_id) == self._bot_person_id
|
|
461
|
+
):
|
|
456
462
|
self._logger.debug(f"Ignoring self-message from bot ({self._bot_person_id})")
|
|
457
463
|
return
|
|
458
464
|
|
|
@@ -469,6 +475,24 @@ class WebexMessageHandler:
|
|
|
469
475
|
person_id=activity.actor.id,
|
|
470
476
|
),
|
|
471
477
|
)
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
# membership:created — membership verbs + objectType=person
|
|
481
|
+
membership_verbs = {"add", "leave", "assignModerator", "unassignModerator"}
|
|
482
|
+
if activity.verb in membership_verbs and activity.object.object_type == "person":
|
|
483
|
+
self._emit(
|
|
484
|
+
"membership:created",
|
|
485
|
+
MembershipActivity(
|
|
486
|
+
id=activity.id,
|
|
487
|
+
actor_id=activity.actor.id,
|
|
488
|
+
person_id=activity.object.id,
|
|
489
|
+
room_id=activity.target.id,
|
|
490
|
+
action=activity.verb,
|
|
491
|
+
created=activity.published,
|
|
492
|
+
room_type=self._infer_room_type(activity),
|
|
493
|
+
raw=activity,
|
|
494
|
+
),
|
|
495
|
+
)
|
|
472
496
|
|
|
473
497
|
@staticmethod
|
|
474
498
|
def _infer_room_type(activity: MercuryActivity) -> str | None:
|
{webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/kms_client.py
RENAMED
|
@@ -141,13 +141,18 @@ class KmsClient:
|
|
|
141
141
|
response_body = _unwrap_kms_response(wrapped_response, local_ecdh_key)
|
|
142
142
|
response_data = json.loads(response_body)
|
|
143
143
|
|
|
144
|
-
remote_jwk_data =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
remote_jwk_data = (
|
|
145
|
+
response_data.get("body", {}).get("key", {}).get("jwk")
|
|
146
|
+
or response_data.get("key", {}).get("jwk")
|
|
147
|
+
)
|
|
148
|
+
if (
|
|
149
|
+
not remote_jwk_data
|
|
150
|
+
and "body" in response_data
|
|
151
|
+
and "key" in response_data["body"]
|
|
152
|
+
):
|
|
153
|
+
remote_jwk_data = response_data["body"]["key"]
|
|
154
|
+
if isinstance(remote_jwk_data, dict) and "jwk" in remote_jwk_data:
|
|
155
|
+
remote_jwk_data = remote_jwk_data["jwk"]
|
|
151
156
|
|
|
152
157
|
if not remote_jwk_data:
|
|
153
158
|
raise KmsError(
|
|
@@ -155,7 +160,10 @@ class KmsClient:
|
|
|
155
160
|
)
|
|
156
161
|
|
|
157
162
|
# Step 7: Derive shared key via ECDH
|
|
158
|
-
|
|
163
|
+
if isinstance(remote_jwk_data, dict):
|
|
164
|
+
remote_ecdh_key = jwk.JWK(**remote_jwk_data)
|
|
165
|
+
else:
|
|
166
|
+
remote_ecdh_key = jwk.JWK(**json.loads(remote_jwk_data))
|
|
159
167
|
|
|
160
168
|
# Get the remote key URI for use as kid on the derived key
|
|
161
169
|
remote_key_uri = (
|
|
@@ -239,14 +247,17 @@ class KmsClient:
|
|
|
239
247
|
response_data = json.loads(response_body)
|
|
240
248
|
|
|
241
249
|
# Extract the content key
|
|
242
|
-
key_data =
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
key_data = (
|
|
251
|
+
response_data.get("body", {}).get("key", {}).get("jwk")
|
|
252
|
+
or response_data.get("key", {}).get("jwk")
|
|
253
|
+
)
|
|
254
|
+
if (
|
|
255
|
+
not key_data
|
|
256
|
+
and "body" in response_data
|
|
257
|
+
and "key" in response_data["body"]
|
|
258
|
+
):
|
|
259
|
+
key_obj = response_data["body"]["key"]
|
|
260
|
+
key_data = key_obj["jwk"] if isinstance(key_obj, dict) and "jwk" in key_obj else key_obj
|
|
250
261
|
|
|
251
262
|
if not key_data:
|
|
252
263
|
raise KmsError("No key found in KMS response")
|
|
@@ -370,8 +381,6 @@ def _derive_ecdh_shared_key(local_key: jwk.JWK, remote_key: jwk.JWK, *, kid: str
|
|
|
370
381
|
from cryptography.hazmat.primitives.hashes import SHA256
|
|
371
382
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
372
383
|
|
|
373
|
-
# Get cryptography private key from local JWK
|
|
374
|
-
local_private = local_key.get_op_key("sign") if local_key.has_private else local_key.get_op_key("unwrapKey")
|
|
375
384
|
# For EC keys, get the actual private key object
|
|
376
385
|
local_crypto_key = local_key._get_private_key() if hasattr(local_key, '_get_private_key') else None
|
|
377
386
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextlib
|
|
6
7
|
import json
|
|
7
8
|
import math
|
|
8
9
|
import uuid
|
|
@@ -160,9 +161,9 @@ class MercurySocket:
|
|
|
160
161
|
# Wait for connection ready or error
|
|
161
162
|
try:
|
|
162
163
|
await asyncio.wait_for(ready_event.wait(), timeout=30.0)
|
|
163
|
-
except asyncio.TimeoutError:
|
|
164
|
+
except asyncio.TimeoutError as exc:
|
|
164
165
|
await self._cleanup_ws()
|
|
165
|
-
raise MercuryConnectionError("Mercury connection timeout waiting for ready signal")
|
|
166
|
+
raise MercuryConnectionError("Mercury connection timeout waiting for ready signal") from exc
|
|
166
167
|
|
|
167
168
|
if connect_error:
|
|
168
169
|
await self._cleanup_ws()
|
|
@@ -337,10 +338,8 @@ class MercurySocket:
|
|
|
337
338
|
self._stop_ping_loop()
|
|
338
339
|
if self._read_task and not self._read_task.done():
|
|
339
340
|
self._read_task.cancel()
|
|
340
|
-
|
|
341
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
341
342
|
await self._read_task
|
|
342
|
-
except asyncio.CancelledError:
|
|
343
|
-
pass
|
|
344
343
|
await self._close_websocket()
|
|
345
344
|
self._ws = None
|
|
346
345
|
|
|
@@ -351,10 +350,8 @@ class MercurySocket:
|
|
|
351
350
|
self._stop_ping_loop()
|
|
352
351
|
if self._read_task and not self._read_task.done():
|
|
353
352
|
self._read_task.cancel()
|
|
354
|
-
|
|
353
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
355
354
|
await self._read_task
|
|
356
|
-
except asyncio.CancelledError:
|
|
357
|
-
pass
|
|
358
355
|
await self._close_websocket()
|
|
359
356
|
self._ws = None
|
|
360
357
|
self._connection_ready = False
|
|
@@ -53,7 +53,11 @@ class MessageDecryptor:
|
|
|
53
53
|
decrypted.object = copy.copy(activity.object)
|
|
54
54
|
|
|
55
55
|
# Decrypt displayName
|
|
56
|
-
if
|
|
56
|
+
if (
|
|
57
|
+
decrypted.object.display_name
|
|
58
|
+
and isinstance(decrypted.object.display_name, str)
|
|
59
|
+
and len(decrypted.object.display_name) > 0
|
|
60
|
+
):
|
|
57
61
|
try:
|
|
58
62
|
jwe_obj = jwe.JWE()
|
|
59
63
|
jwe_obj.deserialize(decrypted.object.display_name, key=key)
|
|
@@ -64,7 +68,11 @@ class MessageDecryptor:
|
|
|
64
68
|
)
|
|
65
69
|
|
|
66
70
|
# Decrypt content
|
|
67
|
-
if
|
|
71
|
+
if (
|
|
72
|
+
decrypted.object.content
|
|
73
|
+
and isinstance(decrypted.object.content, str)
|
|
74
|
+
and len(decrypted.object.content) > 0
|
|
75
|
+
):
|
|
68
76
|
try:
|
|
69
77
|
jwe_obj = jwe.JWE()
|
|
70
78
|
jwe_obj.deserialize(decrypted.object.content, key=key)
|
{webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/types.py
RENAMED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
|
-
from typing import TYPE_CHECKING, Any,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
import aiohttp
|
|
@@ -228,6 +229,35 @@ class DeletedMessage:
|
|
|
228
229
|
person_id: str
|
|
229
230
|
|
|
230
231
|
|
|
232
|
+
@dataclass
|
|
233
|
+
class MembershipActivity:
|
|
234
|
+
"""A membership activity from Mercury."""
|
|
235
|
+
|
|
236
|
+
id: str
|
|
237
|
+
"""Activity ID."""
|
|
238
|
+
|
|
239
|
+
actor_id: str
|
|
240
|
+
"""ID of the person who performed the action."""
|
|
241
|
+
|
|
242
|
+
person_id: str
|
|
243
|
+
"""ID of the member affected."""
|
|
244
|
+
|
|
245
|
+
room_id: str
|
|
246
|
+
"""Conversation/space ID."""
|
|
247
|
+
|
|
248
|
+
action: str
|
|
249
|
+
"""Membership action: "add", "leave", "assignModerator", or "unassignModerator"."""
|
|
250
|
+
|
|
251
|
+
created: str
|
|
252
|
+
"""ISO 8601 timestamp."""
|
|
253
|
+
|
|
254
|
+
room_type: str | None = None
|
|
255
|
+
"""'direct' | 'group' | None."""
|
|
256
|
+
|
|
257
|
+
raw: MercuryActivity | None = None
|
|
258
|
+
"""Full raw activity for advanced use."""
|
|
259
|
+
|
|
260
|
+
|
|
231
261
|
# --- Status ---
|
|
232
262
|
|
|
233
263
|
ConnectionStatus = Literal["connected", "connecting", "reconnecting", "disconnected"]
|
|
@@ -8,13 +8,15 @@ from webex_message_handler.types import FetchRequest, FetchResponse
|
|
|
8
8
|
async def create_test_http_do(connector: aiohttp.BaseConnector | None = None):
|
|
9
9
|
"""Create a test HTTP adapter for use in tests."""
|
|
10
10
|
async def http_do(request: FetchRequest) -> FetchResponse:
|
|
11
|
-
async with
|
|
12
|
-
|
|
11
|
+
async with (
|
|
12
|
+
aiohttp.ClientSession(connector=connector) as session,
|
|
13
|
+
session.request(
|
|
13
14
|
request.method,
|
|
14
15
|
request.url,
|
|
15
16
|
headers=request.headers,
|
|
16
17
|
data=request.body,
|
|
17
|
-
) as response
|
|
18
|
+
) as response,
|
|
19
|
+
):
|
|
18
20
|
# Eagerly read the response body before the context closes
|
|
19
21
|
body_bytes = await response.read()
|
|
20
22
|
status = response.status
|
|
@@ -6,7 +6,6 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
from webex_message_handler.handler import WebexMessageHandler
|
|
8
8
|
from webex_message_handler.types import (
|
|
9
|
-
DecryptedMessage,
|
|
10
9
|
DeviceRegistration,
|
|
11
10
|
MercuryActivity,
|
|
12
11
|
MercuryActor,
|
|
@@ -37,7 +36,10 @@ def _make_activity(**overrides) -> MercuryActivity:
|
|
|
37
36
|
"id": "msg-123",
|
|
38
37
|
"verb": "post",
|
|
39
38
|
"actor": MercuryActor(id="person-456", object_type="person", email_address="user@example.com"),
|
|
40
|
-
"object": MercuryObject(
|
|
39
|
+
"object": MercuryObject(
|
|
40
|
+
id="comment-789", object_type="comment",
|
|
41
|
+
display_name="Test Message", content="<p>Test Message</p>",
|
|
42
|
+
),
|
|
41
43
|
"target": MercuryTarget(id="room-101", object_type="conversation", tags=["GROUP"]),
|
|
42
44
|
"published": "2024-01-01T00:00:00Z",
|
|
43
45
|
}
|
|
@@ -282,6 +284,118 @@ class TestMessageHandling:
|
|
|
282
284
|
assert len(messages) == 0
|
|
283
285
|
|
|
284
286
|
|
|
287
|
+
class TestMembershipHandling:
|
|
288
|
+
async def test_handle_membership_add(self):
|
|
289
|
+
handler = _make_handler()
|
|
290
|
+
activity = _make_activity(
|
|
291
|
+
verb="add",
|
|
292
|
+
object=MercuryObject(id="member-789", object_type="person"),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
events = []
|
|
296
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
297
|
+
|
|
298
|
+
await handler._handle_activity(activity)
|
|
299
|
+
|
|
300
|
+
assert len(events) == 1
|
|
301
|
+
evt = events[0]
|
|
302
|
+
assert evt.id == "msg-123"
|
|
303
|
+
assert evt.actor_id == "person-456"
|
|
304
|
+
assert evt.person_id == "member-789"
|
|
305
|
+
assert evt.room_id == "room-101"
|
|
306
|
+
assert evt.action == "add"
|
|
307
|
+
assert evt.created == "2024-01-01T00:00:00Z"
|
|
308
|
+
assert evt.room_type == "group"
|
|
309
|
+
|
|
310
|
+
async def test_handle_membership_leave(self):
|
|
311
|
+
handler = _make_handler()
|
|
312
|
+
activity = _make_activity(
|
|
313
|
+
verb="leave",
|
|
314
|
+
object=MercuryObject(id="member-789", object_type="person"),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
events = []
|
|
318
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
319
|
+
|
|
320
|
+
await handler._handle_activity(activity)
|
|
321
|
+
|
|
322
|
+
assert len(events) == 1
|
|
323
|
+
assert events[0].action == "leave"
|
|
324
|
+
|
|
325
|
+
async def test_handle_membership_assign_moderator(self):
|
|
326
|
+
handler = _make_handler()
|
|
327
|
+
activity = _make_activity(
|
|
328
|
+
verb="assignModerator",
|
|
329
|
+
object=MercuryObject(id="member-789", object_type="person"),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
events = []
|
|
333
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
334
|
+
|
|
335
|
+
await handler._handle_activity(activity)
|
|
336
|
+
|
|
337
|
+
assert len(events) == 1
|
|
338
|
+
assert events[0].action == "assignModerator"
|
|
339
|
+
|
|
340
|
+
async def test_handle_membership_unassign_moderator(self):
|
|
341
|
+
handler = _make_handler()
|
|
342
|
+
activity = _make_activity(
|
|
343
|
+
verb="unassignModerator",
|
|
344
|
+
object=MercuryObject(id="member-789", object_type="person"),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
events = []
|
|
348
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
349
|
+
|
|
350
|
+
await handler._handle_activity(activity)
|
|
351
|
+
|
|
352
|
+
assert len(events) == 1
|
|
353
|
+
assert events[0].action == "unassignModerator"
|
|
354
|
+
|
|
355
|
+
async def test_non_membership_verb_with_person_object(self):
|
|
356
|
+
handler = _make_handler()
|
|
357
|
+
activity = _make_activity(
|
|
358
|
+
verb="post",
|
|
359
|
+
object=MercuryObject(id="person-789", object_type="person"),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
events = []
|
|
363
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
364
|
+
|
|
365
|
+
await handler._handle_activity(activity)
|
|
366
|
+
|
|
367
|
+
assert len(events) == 0
|
|
368
|
+
|
|
369
|
+
async def test_membership_verb_with_non_person_object(self):
|
|
370
|
+
handler = _make_handler()
|
|
371
|
+
activity = _make_activity(
|
|
372
|
+
verb="add",
|
|
373
|
+
object=MercuryObject(id="comment-789", object_type="comment"),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
events = []
|
|
377
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
378
|
+
|
|
379
|
+
await handler._handle_activity(activity)
|
|
380
|
+
|
|
381
|
+
assert len(events) == 0
|
|
382
|
+
|
|
383
|
+
async def test_membership_includes_raw_activity(self):
|
|
384
|
+
handler = _make_handler()
|
|
385
|
+
activity = _make_activity(
|
|
386
|
+
verb="add",
|
|
387
|
+
object=MercuryObject(id="member-789", object_type="person"),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
events = []
|
|
391
|
+
handler.on("membership:created", lambda a: events.append(a))
|
|
392
|
+
|
|
393
|
+
await handler._handle_activity(activity)
|
|
394
|
+
|
|
395
|
+
assert len(events) == 1
|
|
396
|
+
assert events[0].raw is activity
|
|
397
|
+
|
|
398
|
+
|
|
285
399
|
class TestStatus:
|
|
286
400
|
def test_disconnected_status(self):
|
|
287
401
|
handler = _make_handler()
|
|
@@ -81,7 +81,9 @@ async def test_integration_send_and_receive():
|
|
|
81
81
|
"https://webexapis.com/v1/people/me",
|
|
82
82
|
headers={"Authorization": f"Bearer {sender_token}"}
|
|
83
83
|
) as sender_response:
|
|
84
|
-
assert receiver_response.status == 200,
|
|
84
|
+
assert receiver_response.status == 200, (
|
|
85
|
+
f"Failed to get receiver bot identity: {receiver_response.status}"
|
|
86
|
+
)
|
|
85
87
|
assert sender_response.status == 200, f"Failed to get sender bot identity: {sender_response.status}"
|
|
86
88
|
receiver = await receiver_response.json()
|
|
87
89
|
sender = await sender_response.json()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/errors.py
RENAMED
|
File without changes
|
{webex_message_handler-0.4.4 → webex_message_handler-0.5.0}/src/webex_message_handler/logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|