Skip to content

Commit fd10ebd

Browse files
committed
Fix #260 Enable to use respond utility in app.view listeners (only when response_urls exists)
1 parent 8786783 commit fd10ebd

File tree

5 files changed

+323
-149
lines changed

5 files changed

+323
-149
lines changed

slack_bolt/request/async_internals.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,34 @@
66
extract_team_id,
77
extract_user_id,
88
extract_channel_id,
9+
debug_multiple_response_urls_detected,
910
)
1011

1112

1213
def build_async_context(
1314
context: AsyncBoltContext,
14-
payload: Dict[str, Any],
15+
body: Dict[str, Any],
1516
) -> AsyncBoltContext:
16-
enterprise_id = extract_enterprise_id(payload)
17+
enterprise_id = extract_enterprise_id(body)
1718
if enterprise_id:
1819
context["enterprise_id"] = enterprise_id
19-
team_id = extract_team_id(payload)
20+
team_id = extract_team_id(body)
2021
if team_id:
2122
context["team_id"] = team_id
22-
user_id = extract_user_id(payload)
23+
user_id = extract_user_id(body)
2324
if user_id:
2425
context["user_id"] = user_id
25-
channel_id = extract_channel_id(payload)
26+
channel_id = extract_channel_id(body)
2627
if channel_id:
2728
context["channel_id"] = channel_id
28-
if "response_url" in payload:
29-
context["response_url"] = payload["response_url"]
29+
if "response_url" in body:
30+
context["response_url"] = body["response_url"]
31+
elif "response_urls" in body:
32+
# In the case where response_url_enabled: true in a modal exists
33+
response_urls = body["response_urls"]
34+
if len(response_urls) >= 1:
35+
if len(response_urls) > 1:
36+
context.logger.debug(debug_multiple_response_urls_detected())
37+
response_url = response_urls[0].get("response_url")
38+
context["response_url"] = response_url
3039
return context

slack_bolt/request/internals.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ def build_context(context: BoltContext, body: Dict[str, Any]) -> BoltContext:
143143
context["channel_id"] = channel_id
144144
if "response_url" in body:
145145
context["response_url"] = body["response_url"]
146+
elif "response_urls" in body:
147+
# In the case where response_url_enabled: true in a modal exists
148+
response_urls = body["response_urls"]
149+
if len(response_urls) >= 1:
150+
if len(response_urls) > 1:
151+
context.logger.debug(debug_multiple_response_urls_detected())
152+
response_url = response_urls[0].get("response_url")
153+
context["response_url"] = response_url
146154
return context
147155

148156

@@ -177,3 +185,11 @@ def error_message_raw_body_required_in_http_mode() -> str:
177185

178186
def error_message_unknown_request_body_type() -> str:
179187
return "`body` must be either str or dict"
188+
189+
190+
def debug_multiple_response_urls_detected() -> str:
191+
return (
192+
"`response_urls` in the body has multiple URLs in it. "
193+
"If you would like to use non-primary one, "
194+
"please manually extract the one from body['response_urls']."
195+
)

tests/mock_web_api_server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def set_common_headers(self):
9292
def _handle(self):
9393
self.received_requests[self.path] = self.received_requests.get(self.path, 0) + 1
9494
try:
95+
if self.path == "/webhook":
96+
self.send_response(200)
97+
self.set_common_headers()
98+
self.wfile.write("OK".encode("utf-8"))
99+
return
100+
95101
if self.path == "/received_requests.json":
96102
self.send_response(200)
97103
self.set_common_headers()

tests/scenario_tests/test_view_submission.py

Lines changed: 142 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,133 @@
1515
from tests.utils import remove_os_env_temporarily, restore_os_env
1616

1717

18+
body = {
19+
"type": "view_submission",
20+
"team": {
21+
"id": "T111",
22+
"domain": "workspace-domain",
23+
"enterprise_id": "E111",
24+
"enterprise_name": "Sandbox Org",
25+
},
26+
"user": {
27+
"id": "W111",
28+
"username": "primary-owner",
29+
"name": "primary-owner",
30+
"team_id": "T111",
31+
},
32+
"api_app_id": "A111",
33+
"token": "verification_token",
34+
"trigger_id": "111.222.valid",
35+
"view": {
36+
"id": "V111",
37+
"team_id": "T111",
38+
"type": "modal",
39+
"blocks": [
40+
{
41+
"type": "input",
42+
"block_id": "hspI",
43+
"label": {
44+
"type": "plain_text",
45+
"text": "Label",
46+
},
47+
"optional": False,
48+
"element": {"type": "plain_text_input", "action_id": "maBWU"},
49+
}
50+
],
51+
"private_metadata": "This is for you!",
52+
"callback_id": "view-id",
53+
"state": {
54+
"values": {"hspI": {"maBWU": {"type": "plain_text_input", "value": "test"}}}
55+
},
56+
"hash": "1596530361.3wRYuk3R",
57+
"title": {
58+
"type": "plain_text",
59+
"text": "My App",
60+
},
61+
"clear_on_close": False,
62+
"notify_on_close": False,
63+
"close": {
64+
"type": "plain_text",
65+
"text": "Cancel",
66+
},
67+
"submit": {
68+
"type": "plain_text",
69+
"text": "Submit",
70+
},
71+
"previous_view_id": None,
72+
"root_view_id": "V111",
73+
"app_id": "A111",
74+
"external_id": "",
75+
"app_installed_team_id": "T111",
76+
"bot_id": "B111",
77+
},
78+
"response_urls": [],
79+
}
80+
81+
raw_body = f"payload={quote(json.dumps(body))}"
82+
83+
84+
def simple_listener(ack, body, payload, view):
85+
assert body["trigger_id"] == "111.222.valid"
86+
assert body["view"] == payload
87+
assert payload == view
88+
assert view["private_metadata"] == "This is for you!"
89+
ack()
90+
91+
92+
response_url_payload_body = {
93+
"type": "view_submission",
94+
"team": {"id": "T111", "domain": "test-test-test"},
95+
"user": {
96+
"id": "U111",
97+
"username": "test-test-test",
98+
"name": "test-test-test",
99+
"team_id": "T111",
100+
},
101+
"api_app_id": "A111",
102+
"token": "verification-token",
103+
"trigger_id": "111.222.xxx",
104+
"view": {
105+
"id": "V111",
106+
"team_id": "T111",
107+
"type": "modal",
108+
"blocks": [],
109+
"callback_id": "view-id",
110+
"state": {},
111+
"title": {
112+
"type": "plain_text",
113+
"text": "My App",
114+
},
115+
"close": {
116+
"type": "plain_text",
117+
"text": "Cancel",
118+
},
119+
"submit": {
120+
"type": "plain_text",
121+
"text": "Submit",
122+
},
123+
"previous_view_id": None,
124+
"root_view_id": "V111",
125+
"app_id": "A111",
126+
"external_id": "",
127+
"app_installed_team_id": "T111",
128+
"bot_id": "B111",
129+
},
130+
"response_urls": [
131+
{
132+
"block_id": "b",
133+
"action_id": "a",
134+
"channel_id": "C111",
135+
"response_url": "http://localhost:8888/webhook",
136+
}
137+
],
138+
"is_enterprise_install": False,
139+
}
140+
141+
142+
raw_response_url_body = f"payload={quote(json.dumps(response_url_payload_body))}"
143+
144+
18145
class TestViewSubmission:
19146
signing_secret = "secret"
20147
valid_token = "xoxb-valid"
@@ -46,11 +173,9 @@ def build_headers(self, timestamp: str, body: str):
46173
"x-slack-request-timestamp": [timestamp],
47174
}
48175

49-
def build_valid_request(self) -> BoltRequest:
176+
def build_valid_request(self, body: str = raw_body) -> BoltRequest:
50177
timestamp = str(int(time()))
51-
return BoltRequest(
52-
body=raw_body, headers=self.build_headers(timestamp, raw_body)
53-
)
178+
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))
54179

55180
def test_mock_server_is_running(self):
56181
resp = self.web_client.api_test()
@@ -123,76 +248,18 @@ def test_failure_2(self):
123248
assert response.status == 404
124249
assert_auth_test_count(self, 1)
125250

251+
def test_response_urls(self):
252+
app = App(
253+
client=self.web_client,
254+
signing_secret=self.signing_secret,
255+
)
126256

127-
body = {
128-
"type": "view_submission",
129-
"team": {
130-
"id": "T111",
131-
"domain": "workspace-domain",
132-
"enterprise_id": "E111",
133-
"enterprise_name": "Sandbox Org",
134-
},
135-
"user": {
136-
"id": "W111",
137-
"username": "primary-owner",
138-
"name": "primary-owner",
139-
"team_id": "T111",
140-
},
141-
"api_app_id": "A111",
142-
"token": "verification_token",
143-
"trigger_id": "111.222.valid",
144-
"view": {
145-
"id": "V111",
146-
"team_id": "T111",
147-
"type": "modal",
148-
"blocks": [
149-
{
150-
"type": "input",
151-
"block_id": "hspI",
152-
"label": {
153-
"type": "plain_text",
154-
"text": "Label",
155-
},
156-
"optional": False,
157-
"element": {"type": "plain_text_input", "action_id": "maBWU"},
158-
}
159-
],
160-
"private_metadata": "This is for you!",
161-
"callback_id": "view-id",
162-
"state": {
163-
"values": {"hspI": {"maBWU": {"type": "plain_text_input", "value": "test"}}}
164-
},
165-
"hash": "1596530361.3wRYuk3R",
166-
"title": {
167-
"type": "plain_text",
168-
"text": "My App",
169-
},
170-
"clear_on_close": False,
171-
"notify_on_close": False,
172-
"close": {
173-
"type": "plain_text",
174-
"text": "Cancel",
175-
},
176-
"submit": {
177-
"type": "plain_text",
178-
"text": "Submit",
179-
},
180-
"previous_view_id": None,
181-
"root_view_id": "V111",
182-
"app_id": "A111",
183-
"external_id": "",
184-
"app_installed_team_id": "T111",
185-
"bot_id": "B111",
186-
},
187-
"response_urls": [],
188-
}
189-
190-
raw_body = f"payload={quote(json.dumps(body))}"
191-
257+
@app.view("view-id")
258+
def check(ack, respond):
259+
respond("Hi")
260+
ack()
192261

193-
def simple_listener(ack, body, payload, view):
194-
assert body["trigger_id"] == "111.222.valid"
195-
assert body["view"] == payload
196-
assert payload == view
197-
assert view["private_metadata"] == "This is for you!"
198-
ack()
262+
request = self.build_valid_request(raw_response_url_body)
263+
response = app.dispatch(request)
264+
assert response.status == 200
265+
assert_auth_test_count(self, 1)

0 commit comments

Comments
 (0)