Skip to content

Commit cbfbae6

Browse files
chore: fix bug
1 parent 3e7031b commit cbfbae6

2 files changed

Lines changed: 371 additions & 1 deletion

File tree

slack_bolt/request/payload_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
8282
def is_bot_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
8383
if is_event(body):
8484
return (
85-
is_im_message_event(body)
85+
is_any_im_message_event(body)
86+
and body["event"].get("subtype") is None
8687
and body["event"].get("thread_ts") is not None
8788
and body["event"].get("bot_id") is not None
8889
)
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
from slack_bolt.request.payload_utils import (
2+
is_event,
3+
is_message_event,
4+
is_any_im_message_event,
5+
is_im_message_event,
6+
is_user_message_event_in_assistant_thread,
7+
is_bot_message_event_in_assistant_thread,
8+
is_other_message_sub_event_in_assistant_thread,
9+
is_assistant_event,
10+
is_assistant_thread_started_event,
11+
is_assistant_thread_context_changed_event,
12+
)
13+
from tests.scenario_tests.test_events_assistant import (
14+
build_payload,
15+
thread_started_event_body,
16+
thread_context_changed_event_body,
17+
user_message_event_body,
18+
user_message_event_body_with_assistant_thread,
19+
message_changed_event_body,
20+
channel_user_message_event_body,
21+
channel_message_changed_event_body,
22+
)
23+
from tests.scenario_tests.test_message_bot import (
24+
bot_message_event_payload,
25+
classic_bot_message_event_payload,
26+
)
27+
from tests.scenario_tests.test_message_deleted import event_payload as message_deleted_channel_body
28+
from tests.scenario_tests.test_events_ignore_self import event_body as reaction_added_event_body
29+
from tests.scenario_tests.test_block_actions import body as block_actions_body
30+
31+
file_share_im_message_body = build_payload(
32+
{
33+
"user": "W222",
34+
"type": "message",
35+
"subtype": "file_share",
36+
"ts": "1726133700.887259",
37+
"text": "uploaded a file",
38+
"files": [
39+
{
40+
"id": "F111",
41+
"created": 1726133700,
42+
"name": "test.png",
43+
"title": "test.png",
44+
"mimetype": "image/png",
45+
"filetype": "png",
46+
"user": "W222",
47+
"size": 12345,
48+
"mode": "hosted",
49+
"is_external": False,
50+
"is_public": False,
51+
"url_private": "https://files.slack.com/files-pri/T111-F111/test.png",
52+
"permalink": "https://example.slack.com/files/W222/F111/test.png",
53+
}
54+
],
55+
"upload": True,
56+
"display_as_bot": False,
57+
"thread_ts": "1726133698.626339",
58+
"channel": "D111",
59+
"event_ts": "1726133700.887259",
60+
"channel_type": "im",
61+
}
62+
)
63+
64+
bot_im_thread_message_body = build_payload(
65+
{
66+
"type": "message",
67+
"ts": "1726133700.887259",
68+
"text": "Here is your answer",
69+
"user": "UB111",
70+
"bot_id": "B111",
71+
"app_id": "A222",
72+
"bot_profile": {
73+
"id": "B111",
74+
"deleted": False,
75+
"name": "assistant-app",
76+
"updated": 1726133600,
77+
"app_id": "A222",
78+
"team_id": "T111",
79+
},
80+
"thread_ts": "1726133698.626339",
81+
"channel": "D111",
82+
"event_ts": "1726133700.887259",
83+
"channel_type": "im",
84+
}
85+
)
86+
87+
im_message_no_thread_ts_body = build_payload(
88+
{
89+
"user": "W222",
90+
"type": "message",
91+
"ts": "1726133700.887259",
92+
"text": "A top-level DM, not in a thread",
93+
"channel": "D111",
94+
"event_ts": "1726133700.887259",
95+
"channel_type": "im",
96+
}
97+
)
98+
99+
slash_command_body = {
100+
"token": "verification_token",
101+
"command": "/test",
102+
"text": "hello",
103+
"user_id": "U111",
104+
"user_name": "primary-owner",
105+
"channel_id": "C111",
106+
"channel_name": "test-channel",
107+
"team_id": "T111",
108+
"team_domain": "test-domain",
109+
"api_app_id": "A111",
110+
"is_enterprise_install": "false",
111+
"response_url": "https://hooks.slack.com/commands/T111/111/xxx",
112+
"trigger_id": "111.222.xxx",
113+
}
114+
115+
116+
class TestPayloadUtils:
117+
def test_is_event(self):
118+
positives = {
119+
"thread_started": thread_started_event_body,
120+
"thread_context_changed": thread_context_changed_event_body,
121+
"user_message_im": user_message_event_body,
122+
"user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread,
123+
"message_changed_im": message_changed_event_body,
124+
"channel_user_message": channel_user_message_event_body,
125+
"channel_message_changed": channel_message_changed_event_body,
126+
"bot_message_channel": bot_message_event_payload,
127+
"classic_bot_message_channel": classic_bot_message_event_payload,
128+
"message_deleted_channel": message_deleted_channel_body,
129+
"reaction_added": reaction_added_event_body,
130+
"file_share_im": file_share_im_message_body,
131+
"bot_im_thread": bot_im_thread_message_body,
132+
"im_no_thread_ts": im_message_no_thread_ts_body,
133+
}
134+
negatives = {
135+
"block_actions": block_actions_body,
136+
"slash_command": slash_command_body,
137+
"empty_dict": {},
138+
}
139+
for key, body in positives.items():
140+
assert is_event(body), f"{key} should be recognized as an event"
141+
for key, body in negatives.items():
142+
assert not is_event(body), f"{key} should NOT be recognized as an event"
143+
144+
def test_is_message_event(self):
145+
positives = {
146+
"user_message_im": user_message_event_body,
147+
"user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread,
148+
"message_changed_im": message_changed_event_body,
149+
"channel_user_message": channel_user_message_event_body,
150+
"channel_message_changed": channel_message_changed_event_body,
151+
"bot_message_channel": bot_message_event_payload,
152+
"classic_bot_message_channel": classic_bot_message_event_payload,
153+
"message_deleted_channel": message_deleted_channel_body,
154+
"file_share_im": file_share_im_message_body,
155+
"bot_im_thread": bot_im_thread_message_body,
156+
"im_no_thread_ts": im_message_no_thread_ts_body,
157+
}
158+
negatives = {
159+
"thread_started": thread_started_event_body,
160+
"thread_context_changed": thread_context_changed_event_body,
161+
"reaction_added": reaction_added_event_body,
162+
"block_actions": block_actions_body,
163+
"slash_command": slash_command_body,
164+
"empty_dict": {},
165+
}
166+
for key, body in positives.items():
167+
assert is_message_event(body), f"{key} should be recognized as a message event"
168+
for key, body in negatives.items():
169+
assert not is_message_event(body), f"{key} should NOT be recognized as a message event"
170+
171+
def test_is_any_im_message_event(self):
172+
positives = {
173+
"user_message_im": user_message_event_body,
174+
"user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread,
175+
"message_changed_im": message_changed_event_body,
176+
"file_share_im": file_share_im_message_body,
177+
"bot_im_thread": bot_im_thread_message_body,
178+
"im_no_thread_ts": im_message_no_thread_ts_body,
179+
}
180+
negatives = {
181+
"channel_user_message": channel_user_message_event_body,
182+
"channel_message_changed": channel_message_changed_event_body,
183+
"bot_message_channel": bot_message_event_payload,
184+
"classic_bot_message_channel": classic_bot_message_event_payload,
185+
"message_deleted_channel": message_deleted_channel_body,
186+
"thread_started": thread_started_event_body,
187+
"thread_context_changed": thread_context_changed_event_body,
188+
"reaction_added": reaction_added_event_body,
189+
"block_actions": block_actions_body,
190+
"slash_command": slash_command_body,
191+
}
192+
for key, body in positives.items():
193+
assert is_any_im_message_event(body), f"{key} should pass {is_any_im_message_event.__name__}"
194+
for key, body in negatives.items():
195+
assert not is_any_im_message_event(body), f"{key} should NOT pass {is_any_im_message_event.__name__}"
196+
197+
def test_is_im_message_event(self):
198+
# subtype must be None or "file_share" to pass
199+
positives = {
200+
"user_message_im": user_message_event_body,
201+
"user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread,
202+
"file_share_im": file_share_im_message_body,
203+
"bot_im_thread": bot_im_thread_message_body,
204+
"im_no_thread_ts": im_message_no_thread_ts_body,
205+
}
206+
negatives = {
207+
"message_changed_im": message_changed_event_body,
208+
"channel_user_message": channel_user_message_event_body,
209+
"channel_message_changed": channel_message_changed_event_body,
210+
"classic_bot_message_channel": classic_bot_message_event_payload,
211+
"bot_message_channel": bot_message_event_payload,
212+
"message_deleted_channel": message_deleted_channel_body,
213+
"thread_started": thread_started_event_body,
214+
"thread_context_changed": thread_context_changed_event_body,
215+
"reaction_added": reaction_added_event_body,
216+
"block_actions": block_actions_body,
217+
}
218+
for key, body in positives.items():
219+
assert is_im_message_event(body), f"{key} should pass {is_im_message_event.__name__}"
220+
for key, body in negatives.items():
221+
assert not is_im_message_event(body), f"{key} should NOT pass {is_im_message_event.__name__}"
222+
223+
def test_is_user_message_event_in_assistant_thread(self):
224+
# Requires: is_im_message_event + thread_ts present + bot_id absent
225+
positives = {
226+
"user_message_im": user_message_event_body,
227+
"user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread,
228+
"file_share_im": file_share_im_message_body,
229+
}
230+
negatives = {
231+
"bot_im_thread": bot_im_thread_message_body,
232+
"im_no_thread_ts": im_message_no_thread_ts_body,
233+
"message_changed_im": message_changed_event_body,
234+
"channel_user_message": channel_user_message_event_body,
235+
"channel_message_changed": channel_message_changed_event_body,
236+
"bot_message_channel": bot_message_event_payload,
237+
"thread_started": thread_started_event_body,
238+
"thread_context_changed": thread_context_changed_event_body,
239+
"reaction_added": reaction_added_event_body,
240+
"block_actions": block_actions_body,
241+
}
242+
for key, body in positives.items():
243+
assert is_user_message_event_in_assistant_thread(
244+
body
245+
), f"{key} should pass {is_user_message_event_in_assistant_thread.__name__}"
246+
for key, body in negatives.items():
247+
assert not is_user_message_event_in_assistant_thread(
248+
body
249+
), f"{key} should NOT pass {is_user_message_event_in_assistant_thread.__name__}"
250+
251+
def test_is_bot_message_event_in_assistant_thread(self):
252+
# Requires: is_any_im_message_event + subtype is None + thread_ts present + bot_id present
253+
positives = {
254+
"bot_im_thread": bot_im_thread_message_body,
255+
}
256+
negatives = {
257+
"user_message_im": user_message_event_body,
258+
# file_share_im has subtype="file_share" -- the key user/bot asymmetry
259+
"file_share_im": file_share_im_message_body,
260+
"im_no_thread_ts": im_message_no_thread_ts_body,
261+
"message_changed_im": message_changed_event_body,
262+
"channel_user_message": channel_user_message_event_body,
263+
"bot_message_channel": bot_message_event_payload,
264+
"classic_bot_message_channel": classic_bot_message_event_payload,
265+
"thread_started": thread_started_event_body,
266+
"thread_context_changed": thread_context_changed_event_body,
267+
"reaction_added": reaction_added_event_body,
268+
"block_actions": block_actions_body,
269+
}
270+
for key, body in positives.items():
271+
assert is_bot_message_event_in_assistant_thread(
272+
body
273+
), f"{key} should pass {is_bot_message_event_in_assistant_thread.__name__}"
274+
for key, body in negatives.items():
275+
assert not is_bot_message_event_in_assistant_thread(
276+
body
277+
), f"{key} should NOT pass {is_bot_message_event_in_assistant_thread.__name__}"
278+
279+
def test_is_bot_message_user_message_asymmetry(self):
280+
# file_share in IM thread: user predicate accepts it, bot predicate rejects it
281+
assert is_user_message_event_in_assistant_thread(file_share_im_message_body)
282+
assert not is_bot_message_event_in_assistant_thread(file_share_im_message_body)
283+
284+
# bot message in IM thread: bot predicate accepts it, user predicate rejects it
285+
assert is_bot_message_event_in_assistant_thread(bot_im_thread_message_body)
286+
assert not is_user_message_event_in_assistant_thread(bot_im_thread_message_body)
287+
288+
def test_is_other_message_sub_event_in_assistant_thread(self):
289+
positives = {
290+
"message_changed_im": message_changed_event_body,
291+
}
292+
negatives = {
293+
"user_message_im": user_message_event_body,
294+
"bot_im_thread": bot_im_thread_message_body,
295+
"im_no_thread_ts": im_message_no_thread_ts_body,
296+
"channel_message_changed": channel_message_changed_event_body,
297+
"channel_user_message": channel_user_message_event_body,
298+
"message_deleted_channel": message_deleted_channel_body,
299+
"thread_started": thread_started_event_body,
300+
"reaction_added": reaction_added_event_body,
301+
"block_actions": block_actions_body,
302+
}
303+
for key, body in positives.items():
304+
assert is_other_message_sub_event_in_assistant_thread(
305+
body
306+
), f"{key} should pass {is_other_message_sub_event_in_assistant_thread.__name__}"
307+
for key, body in negatives.items():
308+
assert not is_other_message_sub_event_in_assistant_thread(
309+
body
310+
), f"{key} should NOT pass {is_other_message_sub_event_in_assistant_thread.__name__}"
311+
312+
def test_is_assistant_event(self):
313+
positives = {
314+
"thread_started": thread_started_event_body,
315+
"thread_context_changed": thread_context_changed_event_body,
316+
"user_message_im": user_message_event_body,
317+
"user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread,
318+
"file_share_im": file_share_im_message_body,
319+
"bot_im_thread": bot_im_thread_message_body,
320+
}
321+
negatives = {
322+
"message_changed_im": message_changed_event_body,
323+
"im_no_thread_ts": im_message_no_thread_ts_body,
324+
"channel_user_message": channel_user_message_event_body,
325+
"channel_message_changed": channel_message_changed_event_body,
326+
"bot_message_channel": bot_message_event_payload,
327+
"classic_bot_message_channel": classic_bot_message_event_payload,
328+
"message_deleted_channel": message_deleted_channel_body,
329+
"reaction_added": reaction_added_event_body,
330+
"block_actions": block_actions_body,
331+
}
332+
for key, body in positives.items():
333+
assert is_assistant_event(body), f"{key} should pass {is_assistant_event.__name__}"
334+
for key, body in negatives.items():
335+
assert not is_assistant_event(body), f"{key} should NOT pass {is_assistant_event.__name__}"
336+
337+
def test_is_assistant_thread_started_event(self):
338+
assert is_assistant_thread_started_event(thread_started_event_body)
339+
340+
negatives = {
341+
"thread_context_changed": thread_context_changed_event_body,
342+
"user_message_im": user_message_event_body,
343+
"message_changed_im": message_changed_event_body,
344+
"bot_im_thread": bot_im_thread_message_body,
345+
"channel_user_message": channel_user_message_event_body,
346+
"reaction_added": reaction_added_event_body,
347+
"block_actions": block_actions_body,
348+
}
349+
for key, body in negatives.items():
350+
assert not is_assistant_thread_started_event(
351+
body
352+
), f"{key} should NOT pass {is_assistant_thread_started_event.__name__}"
353+
354+
def test_is_assistant_thread_context_changed_event(self):
355+
assert is_assistant_thread_context_changed_event(thread_context_changed_event_body)
356+
357+
negatives = {
358+
"thread_started": thread_started_event_body,
359+
"user_message_im": user_message_event_body,
360+
"message_changed_im": message_changed_event_body,
361+
"bot_im_thread": bot_im_thread_message_body,
362+
"channel_user_message": channel_user_message_event_body,
363+
"reaction_added": reaction_added_event_body,
364+
"block_actions": block_actions_body,
365+
}
366+
for key, body in negatives.items():
367+
assert not is_assistant_thread_context_changed_event(
368+
body
369+
), f"{key} should NOT pass {is_assistant_thread_context_changed_event.__name__}"

0 commit comments

Comments
 (0)