Skip to content

Commit 47063fa

Browse files
committed
Fix #197 by supporting classic bot messages in app.message
1 parent 512a53c commit 47063fa

File tree

5 files changed

+450
-9
lines changed

5 files changed

+450
-9
lines changed

slack_bolt/app/app.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,9 @@ def error(
466466

467467
def event(
468468
self,
469-
event: Union[str, Pattern, Dict[str, str]],
469+
event: Union[
470+
str, Pattern, Dict[str, Union[str, Sequence[Optional[Union[str, Pattern]]]]]
471+
],
470472
matchers: Optional[Sequence[Callable[..., bool]]] = None,
471473
middleware: Optional[Sequence[Union[Callable, Middleware]]] = None,
472474
) -> Optional[Callable[..., Optional[BoltResponse]]]:
@@ -501,9 +503,10 @@ def message(
501503

502504
def __call__(*args, **kwargs):
503505
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
504-
primary_matcher = builtin_matchers.event(
505-
{"type": "message", "subtype": None}
506-
)
506+
# As of Jan 2021, most bot messages no longer have the subtype bot_message.
507+
# By contrast, messages posted using class app's bot token still have the subtype.
508+
constraints = {"type": "message", "subtype": (None, "bot_message")}
509+
primary_matcher = builtin_matchers.event(constraints=constraints)
507510
middleware.append(MessageListenerMatches(keyword))
508511
return self._register_listener(
509512
list(functions), primary_matcher, matchers, middleware, True

slack_bolt/app/async_app.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,9 @@ def error(
517517

518518
def event(
519519
self,
520-
event: Union[str, Pattern, Dict[str, str]],
520+
event: Union[
521+
str, Pattern, Dict[str, Union[str, Sequence[Optional[Union[str, Pattern]]]]]
522+
],
521523
matchers: Optional[Sequence[Callable[..., Awaitable[bool]]]] = None,
522524
middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None,
523525
) -> Optional[Callable[..., Awaitable[Optional[BoltResponse]]]]:
@@ -550,8 +552,11 @@ def message(
550552

551553
def __call__(*args, **kwargs):
552554
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
555+
# As of Jan 2021, most bot messages no longer have the subtype bot_message.
556+
# By contrast, messages posted using class app's bot token still have the subtype.
557+
constraints = {"type": "message", "subtype": (None, "bot_message")}
553558
primary_matcher = builtin_matchers.event(
554-
{"type": "message", "subtype": None}, True
559+
constraints=constraints, asyncio=True
555560
)
556561
middleware.append(AsyncMessageListenerMatches(keyword))
557562
return self._register_listener(

slack_bolt/listener_matcher/builtins.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from re import _pattern_type as Pattern
2727
else:
2828
from re import Pattern
29-
from typing import Callable, Awaitable, Any
29+
from typing import Callable, Awaitable, Any, Sequence, Optional, Union
3030
from typing import Union, Optional, Dict
3131

3232
from slack_bolt.kwargs_injection import build_required_kwargs
@@ -74,7 +74,9 @@ async def async_fun(body: Dict[str, Any]) -> bool:
7474

7575

7676
def event(
77-
constraints: Union[str, Pattern, Dict[str, str]],
77+
constraints: Union[
78+
str, Pattern, Dict[str, Union[str, Sequence[Optional[Union[str, Pattern]]]]]
79+
],
7880
asyncio: bool = False,
7981
) -> Union[ListenerMatcher, "AsyncListenerMatcher"]:
8082
if isinstance(constraints, (str, Pattern)):
@@ -93,10 +95,24 @@ def func(body: Dict[str, Any]) -> bool:
9395
if not _matches(constraints["type"], event["type"]):
9496
return False
9597
if "subtype" in constraints:
96-
expected_subtype = constraints["subtype"]
98+
expected_subtype: Union[
99+
str, Sequence[Optional[Union[str, Pattern]]]
100+
] = constraints["subtype"]
97101
if expected_subtype is None:
98102
# "subtype" in constraints is intentionally None for this pattern
99103
return "subtype" not in event
104+
elif isinstance(expected_subtype, Sequence):
105+
subtypes: Sequence[
106+
Optional[Union[str, Pattern]]
107+
] = expected_subtype
108+
for expected in subtypes:
109+
actual: Optional[str] = event.get("subtype")
110+
if expected is None:
111+
if actual is None:
112+
return True
113+
elif actual is not None and _matches(expected, actual):
114+
return True
115+
return False
100116
else:
101117
return "subtype" in event and _matches(
102118
expected_subtype, event["subtype"]
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import json
2+
import re
3+
import time
4+
5+
from slack_sdk.signature import SignatureVerifier
6+
from slack_sdk.web import WebClient
7+
8+
from slack_bolt.app import App
9+
from slack_bolt.request import BoltRequest
10+
from tests.mock_web_api_server import (
11+
setup_mock_web_api_server,
12+
cleanup_mock_web_api_server,
13+
)
14+
from tests.utils import remove_os_env_temporarily, restore_os_env
15+
16+
17+
class TestBotMessage:
18+
signing_secret = "secret"
19+
valid_token = "xoxb-valid"
20+
mock_api_server_base_url = "http://localhost:8888"
21+
signature_verifier = SignatureVerifier(signing_secret)
22+
web_client = WebClient(
23+
token=valid_token,
24+
base_url=mock_api_server_base_url,
25+
)
26+
27+
def setup_method(self):
28+
self.old_os_env = remove_os_env_temporarily()
29+
setup_mock_web_api_server(self)
30+
31+
def teardown_method(self):
32+
cleanup_mock_web_api_server(self)
33+
restore_os_env(self.old_os_env)
34+
35+
def generate_signature(self, body: str, timestamp: str):
36+
return self.signature_verifier.generate_signature(
37+
body=body,
38+
timestamp=timestamp,
39+
)
40+
41+
def build_headers(self, timestamp: str, body: str):
42+
return {
43+
"content-type": ["application/json"],
44+
"x-slack-signature": [self.generate_signature(body, timestamp)],
45+
"x-slack-request-timestamp": [timestamp],
46+
}
47+
48+
def build_request(self, event_payload: dict) -> BoltRequest:
49+
timestamp, body = str(int(time.time())), json.dumps(event_payload)
50+
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))
51+
52+
def test_message_handler(self):
53+
app = App(
54+
client=self.web_client,
55+
signing_secret=self.signing_secret,
56+
)
57+
58+
result = {"call_count": 0}
59+
60+
@app.message("Hi there!")
61+
def handle_messages(event, logger):
62+
logger.info(event)
63+
result["call_count"] = result["call_count"] + 1
64+
65+
request = self.build_request(user_message_event_payload)
66+
response = app.dispatch(request)
67+
assert response.status == 200
68+
69+
request = self.build_request(bot_message_event_payload)
70+
response = app.dispatch(request)
71+
assert response.status == 200
72+
73+
request = self.build_request(classic_bot_message_event_payload)
74+
response = app.dispatch(request)
75+
assert response.status == 200
76+
77+
assert self.mock_received_requests["/auth.test"] == 1
78+
time.sleep(1) # wait a bit after auto ack()
79+
assert result["call_count"] == 3
80+
81+
82+
user_message_event_payload = {
83+
"token": "verification-token",
84+
"team_id": "T111",
85+
"enterprise_id": "E111",
86+
"api_app_id": "A111",
87+
"event": {
88+
"client_msg_id": "968c94da-c271-4f2a-8ec9-12a9985e5df4",
89+
"type": "message",
90+
"text": "Hi there! Thanks for sharing the info!",
91+
"user": "W111",
92+
"ts": "1610261659.001400",
93+
"team": "T111",
94+
"blocks": [
95+
{
96+
"type": "rich_text",
97+
"block_id": "bN8",
98+
"elements": [
99+
{
100+
"type": "rich_text_section",
101+
"elements": [
102+
{
103+
"type": "text",
104+
"text": "Hi there! Thanks for sharing the info!",
105+
}
106+
],
107+
}
108+
],
109+
}
110+
],
111+
"channel": "C111",
112+
"event_ts": "1610261659.001400",
113+
"channel_type": "channel",
114+
},
115+
"type": "event_callback",
116+
"event_id": "Ev111",
117+
"event_time": 1610261659,
118+
"authorizations": [
119+
{
120+
"enterprise_id": "E111",
121+
"team_id": "T111",
122+
"user_id": "W111",
123+
"is_bot": True,
124+
"is_enterprise_install": False,
125+
}
126+
],
127+
"is_ext_shared_channel": False,
128+
"event_context": "1-message-T111-C111",
129+
}
130+
131+
bot_message_event_payload = {
132+
"token": "verification-token",
133+
"team_id": "T111",
134+
"enterprise_id": "E111",
135+
"api_app_id": "A111",
136+
"event": {
137+
"bot_id": "B999",
138+
"type": "message",
139+
"text": "Hi there! Thanks for sharing the info!",
140+
"user": "UB111",
141+
"ts": "1610261539.000900",
142+
"team": "T111",
143+
"bot_profile": {
144+
"id": "B999",
145+
"deleted": False,
146+
"name": "other-app",
147+
"updated": 1607307935,
148+
"app_id": "A222",
149+
"icons": {
150+
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
151+
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
152+
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png",
153+
},
154+
"team_id": "T111",
155+
},
156+
"channel": "C111",
157+
"event_ts": "1610261539.000900",
158+
"channel_type": "channel",
159+
},
160+
"type": "event_callback",
161+
"event_id": "Ev222",
162+
"event_time": 1610261539,
163+
"authorizations": [
164+
{
165+
"enterprise_id": "E111",
166+
"team_id": "T111",
167+
"user_id": "UB111",
168+
"is_bot": True,
169+
"is_enterprise_install": False,
170+
}
171+
],
172+
"is_ext_shared_channel": False,
173+
"event_context": "1-message-T111-C111",
174+
}
175+
176+
classic_bot_message_event_payload = {
177+
"token": "verification-token",
178+
"team_id": "T111",
179+
"enterprise_id": "E111",
180+
"api_app_id": "A111",
181+
"event": {
182+
"type": "message",
183+
"subtype": "bot_message",
184+
"text": "Hi there! Thanks for sharing the info!",
185+
"ts": "1610262363.001600",
186+
"username": "classic-bot",
187+
"bot_id": "B888",
188+
"channel": "C111",
189+
"event_ts": "1610262363.001600",
190+
"channel_type": "channel",
191+
},
192+
"type": "event_callback",
193+
"event_id": "Ev333",
194+
"event_time": 1610262363,
195+
"authorizations": [
196+
{
197+
"enterprise_id": "E111",
198+
"team_id": "T111",
199+
"user_id": "UB222",
200+
"is_bot": True,
201+
"is_enterprise_install": False,
202+
}
203+
],
204+
"is_ext_shared_channel": False,
205+
"event_context": "1-message-T111-C111",
206+
}

0 commit comments

Comments
 (0)