Skip to content

Commit 68c43d8

Browse files
latest changes
1 parent c30b6fb commit 68c43d8

File tree

7 files changed

+226
-36
lines changed

7 files changed

+226
-36
lines changed

slack_bolt/context/base_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class BaseContext(dict):
3838
"set_status",
3939
"set_title",
4040
"set_suggested_prompts",
41+
"say_stream",
4142
]
4243
# Note that these items are not copyable, so when you add new items to this list,
4344
# you must modify ThreadListenerRunner/AsyncioListenerRunner's _build_lazy_request method to pass the values.

slack_bolt/context/say_stream/async_say_stream.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ def __init__(
1515
self,
1616
client: AsyncWebClient,
1717
channel_id: Optional[str],
18-
thread_ts: Optional[str] = None,
19-
team_id: Optional[str] = None,
20-
user_id: Optional[str] = None,
18+
thread_ts: Optional[str],
19+
team_id: Optional[str],
20+
user_id: Optional[str],
2121
):
2222
self.client = client
2323
self.channel_id = channel_id
@@ -39,15 +39,16 @@ async def __call__(
3939
raise ValueError(
4040
"Either provide all of channel, thread_ts, recipient_team_id, and recipient_user_id, or none of them"
4141
)
42-
resolved_channel = channel or self.channel_id
43-
resolved_thread_ts = thread_ts or self.thread_ts
44-
if resolved_channel is None:
42+
channel = channel or self.channel_id
43+
thread_ts = thread_ts or self.thread_ts
44+
if channel is None:
4545
raise ValueError("say_stream is unsupported here as there is no channel_id")
46-
if resolved_thread_ts is None:
46+
if thread_ts is None:
4747
raise ValueError("say_stream is unsupported here as there is no thread_ts")
48+
4849
return await self.client.chat_stream(
49-
channel=resolved_channel,
50-
thread_ts=resolved_thread_ts,
50+
channel=channel,
51+
thread_ts=thread_ts,
5152
recipient_team_id=recipient_team_id or self.team_id,
5253
recipient_user_id=recipient_user_id or self.user_id,
5354
**kwargs,

slack_bolt/context/say_stream/say_stream.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ def __init__(
1515
self,
1616
client: WebClient,
1717
channel_id: Optional[str],
18-
thread_ts: Optional[str] = None,
19-
team_id: Optional[str] = None,
20-
user_id: Optional[str] = None,
18+
thread_ts: Optional[str],
19+
team_id: Optional[str],
20+
user_id: Optional[str],
2121
):
2222
self.client = client
2323
self.channel_id = channel_id
@@ -39,16 +39,16 @@ def __call__(
3939
raise ValueError(
4040
"Either provide all of channel, thread_ts, recipient_team_id, and recipient_user_id, or none of them"
4141
)
42-
resolved_channel = channel or self.channel_id
43-
resolved_thread_ts = thread_ts or self.thread_ts
44-
if resolved_channel is None:
42+
channel = channel or self.channel_id
43+
thread_ts = thread_ts or self.thread_ts
44+
if channel is None:
4545
raise ValueError("say_stream is unsupported here as there is no channel_id")
46-
if resolved_thread_ts is None:
46+
if thread_ts is None:
4747
raise ValueError("say_stream is unsupported here as there is no thread_ts")
4848

4949
return self.client.chat_stream(
50-
channel=resolved_channel,
51-
thread_ts=resolved_thread_ts,
50+
channel=channel,
51+
thread_ts=thread_ts,
5252
recipient_team_id=recipient_team_id or self.team_id,
5353
recipient_user_id=recipient_user_id or self.user_id,
5454
**kwargs,

slack_bolt/middleware/attaching_agent_kwargs/async_attaching_agent_kwargs.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,20 @@ async def async_process(
2121
channel_id = req.context.channel_id
2222
# TODO: improve the logic around extracting thread_ts and event ts
2323
event = req.body.get("event", {})
24+
req.context.thread_ts
2425
thread_ts = event.get("thread_ts") or event.get("ts")
2526

2627
if channel_id and thread_ts:
2728
client = req.context.client
2829
req.context["set_status"] = AsyncSetStatus(client, channel_id, thread_ts)
2930
req.context["set_title"] = AsyncSetTitle(client, channel_id, thread_ts)
3031
req.context["set_suggested_prompts"] = AsyncSetSuggestedPrompts(client, channel_id, thread_ts)
31-
32-
req.context["say_stream"] = AsyncSayStream(
33-
client=req.context.client,
34-
channel_id=channel_id,
35-
thread_ts=thread_ts,
36-
team_id=req.context.team_id,
37-
user_id=req.context.user_id,
38-
)
32+
req.context["say_stream"] = AsyncSayStream(
33+
client=req.context.client,
34+
channel_id=channel_id,
35+
thread_ts=thread_ts,
36+
team_id=req.context.team_id,
37+
user_id=req.context.user_id,
38+
)
3939

4040
return await next()

slack_bolt/middleware/attaching_agent_kwargs/attaching_agent_kwargs.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ def process(
2828
req.context["set_status"] = SetStatus(client, channel_id, thread_ts)
2929
req.context["set_title"] = SetTitle(client, channel_id, thread_ts)
3030
req.context["set_suggested_prompts"] = SetSuggestedPrompts(client, channel_id, thread_ts)
31-
32-
req.context["say_stream"] = SayStream(
33-
client=req.context.client,
34-
channel_id=channel_id,
35-
thread_ts=thread_ts,
36-
team_id=req.context.team_id,
37-
user_id=req.context.user_id,
38-
)
31+
req.context["say_stream"] = SayStream(
32+
client=req.context.client,
33+
channel_id=channel_id,
34+
thread_ts=thread_ts,
35+
team_id=req.context.team_id,
36+
user_id=req.context.user_id,
37+
)
3938

4039
return next()

slack_bolt/request/internals.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,16 @@ def extract_thread_ts(payload: Dict[str, Any]) -> Optional[str]:
240240
elif event.get("previous_message", {}).get("thread_ts") is not None:
241241
# message_deleted
242242
return event["previous_message"]["thread_ts"]
243+
return None
244+
thread_ts = payload.get("thread_ts")
245+
if thread_ts is not None:
246+
return thread_ts
247+
if payload.get("event") is not None:
248+
return extract_thread_ts(payload["event"])
249+
if isinstance(payload.get("message"), dict):
250+
return extract_thread_ts(payload["message"])
251+
if isinstance(payload.get("previous_message"), dict):
252+
return extract_thread_ts(payload["previous_message"])
243253
return None
244254

245255

tests/slack_bolt/request/test_internals.py

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
extract_actor_team_id,
1414
extract_actor_user_id,
1515
extract_function_execution_id,
16+
extract_thread_ts,
1617
)
1718

1819

@@ -111,6 +112,143 @@ def teardown_method(self):
111112
},
112113
]
113114

115+
thread_ts_event_requests = [
116+
{
117+
"event": {
118+
"type": "app_mention",
119+
"channel": "C111",
120+
"user": "U111",
121+
"ts": "123.420",
122+
"thread_ts": "123.456",
123+
},
124+
},
125+
{
126+
"event": {
127+
"type": "message",
128+
"channel": "C111",
129+
"user": "U111",
130+
"ts": "123.420",
131+
"thread_ts": "123.456",
132+
},
133+
},
134+
{
135+
"event": {
136+
"type": "message",
137+
"subtype": "bot_message",
138+
"channel": "C111",
139+
"bot_id": "B111",
140+
"ts": "123.420",
141+
"thread_ts": "123.456",
142+
},
143+
},
144+
{
145+
"event": {
146+
"type": "message",
147+
"subtype": "file_share",
148+
"channel": "C111",
149+
"user": "U111",
150+
"ts": "123.420",
151+
"thread_ts": "123.456",
152+
},
153+
},
154+
{
155+
"event": {
156+
"type": "message",
157+
"subtype": "thread_broadcast",
158+
"channel": "C111",
159+
"user": "U111",
160+
"ts": "123.420",
161+
"thread_ts": "123.456",
162+
"root": {"thread_ts": "123.420"},
163+
},
164+
},
165+
{
166+
"event": {
167+
"type": "link_shared",
168+
"channel": "C111",
169+
"user": "U111",
170+
"thread_ts": "123.456",
171+
"links": [{"url": "https://example.com"}],
172+
},
173+
},
174+
{
175+
"event": {
176+
"type": "message",
177+
"subtype": "message_changed",
178+
"channel": "C111",
179+
"message": {
180+
"type": "message",
181+
"user": "U111",
182+
"text": "edited",
183+
"ts": "123.420",
184+
"thread_ts": "123.456",
185+
},
186+
},
187+
},
188+
{
189+
"event": {
190+
"type": "message",
191+
"subtype": "message_changed",
192+
"channel": "C111",
193+
"message": {
194+
"type": "message",
195+
"user": "U111",
196+
"text": "edited",
197+
"ts": "123.420",
198+
"thread_ts": "123.456",
199+
},
200+
"previous_message": {
201+
"type": "message",
202+
"user": "U111",
203+
"text": "deleted",
204+
"ts": "123.420",
205+
"thread_ts": "123.420",
206+
},
207+
},
208+
},
209+
{
210+
"event": {
211+
"type": "message",
212+
"subtype": "message_deleted",
213+
"channel": "C111",
214+
"previous_message": {
215+
"type": "message",
216+
"user": "U111",
217+
"text": "deleted",
218+
"ts": "123.420",
219+
"thread_ts": "123.456",
220+
},
221+
},
222+
},
223+
]
224+
225+
no_thread_ts_requests = [
226+
{
227+
"event": {
228+
"type": "reaction_added",
229+
"user": "U111",
230+
"reaction": "thumbsup",
231+
"item": {"type": "message", "channel": "C111", "ts": "123.420"},
232+
},
233+
},
234+
{
235+
"event": {
236+
"type": "channel_created",
237+
"channel": {"id": "C222", "name": "test", "created": 1678455198},
238+
},
239+
},
240+
{
241+
"event": {
242+
"type": "message",
243+
"channel": "C111",
244+
"user": "U111",
245+
"text": "hello",
246+
"ts": "123.420",
247+
},
248+
},
249+
{},
250+
]
251+
114252
slack_connect_authorizations = [
115253
{
116254
"enterprise_id": "INSTALLED_ENTERPRISE_ID",
@@ -223,10 +361,10 @@ def teardown_method(self):
223361
"type": "message",
224362
"text": "<@INSTALLED_BOT_USER_ID> Hey!",
225363
"user": "USER_ID_ACTOR",
226-
"ts": "1678455198.838499",
364+
"ts": "123.456",
227365
"team": "TEAM_ID_ACTOR",
228366
"channel": "C111",
229-
"event_ts": "1678455198.838499",
367+
"event_ts": "123.456",
230368
"channel_type": "channel",
231369
},
232370
"type": "event_callback",
@@ -337,6 +475,47 @@ def test_function_inputs_extraction(self):
337475
inputs = extract_function_inputs(req)
338476
assert inputs == {"customer_id": "Ux111"}
339477

478+
def test_extract_thread_ts(self):
479+
for req in self.thread_ts_event_requests:
480+
thread_ts = extract_thread_ts(req)
481+
assert thread_ts == "123.456", f"Expected thread_ts for {req}"
482+
483+
def test_extract_thread_ts_fail(self):
484+
for req in self.no_thread_ts_requests:
485+
thread_ts = extract_thread_ts(req)
486+
assert thread_ts is None, f"Expected None for {req}"
487+
488+
def test_extract_thread_ts_edge_cases(self):
489+
# message_changed where only previous_message has thread_ts (no message key)
490+
req = {
491+
"event": {
492+
"type": "message",
493+
"subtype": "message_deleted",
494+
"channel": "C111",
495+
"previous_message": {
496+
"type": "message",
497+
"ts": "1678455205.000000",
498+
"thread_ts": "123.456",
499+
},
500+
},
501+
}
502+
assert extract_thread_ts(req) == "123.456"
503+
504+
# Payload with thread_ts directly at root level (non-event payload)
505+
req = {"thread_ts": "123.456"}
506+
assert extract_thread_ts(req) == "123.456"
507+
508+
# Event with thread_ts as empty string (truthy check: empty string is falsy)
509+
req = {
510+
"event": {
511+
"type": "message",
512+
"channel": "C111",
513+
"thread_ts": "",
514+
},
515+
}
516+
# Empty string is falsy, so .get() returns "" but `is not None` is True
517+
assert extract_thread_ts(req) == ""
518+
340519
def test_is_enterprise_install_extraction(self):
341520
for req in self.requests:
342521
should_be_false = extract_is_enterprise_install(req)

0 commit comments

Comments
 (0)