From 79f8901d7b1f5bd1095221ca06c7a2fdf28509f2 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sun, 12 Dec 2021 23:00:31 +0900 Subject: [PATCH 1/2] Fix #542 Add additional context values for FastAPI apps --- examples/fastapi/async_app_custom_props.py | 41 +++++++++++++++ slack_bolt/adapter/starlette/async_handler.py | 26 +++++++--- .../adapter_tests_async/test_async_fastapi.py | 50 ++++++++++++++++++- 3 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 examples/fastapi/async_app_custom_props.py diff --git a/examples/fastapi/async_app_custom_props.py b/examples/fastapi/async_app_custom_props.py new file mode 100644 index 000000000..497e47aa2 --- /dev/null +++ b/examples/fastapi/async_app_custom_props.py @@ -0,0 +1,41 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +from slack_bolt.async_app import AsyncApp +from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler + +app = AsyncApp() +app_handler = AsyncSlackRequestHandler(app) + + +@app.event("app_mention") +async def handle_app_mentions(context, say, logger): + logger.info(context) + assert context.get("foo") == "FOO" + await say("What's up?") + + +@app.event("message") +async def handle_message(): + pass + + +from fastapi import FastAPI, Request, Depends + +api = FastAPI() + + +def get_foo(): + yield "FOO" + + +@api.post("/slack/events") +async def endpoint(req: Request, foo: str = Depends(get_foo)): + return await app_handler.handle(req, {"foo": foo}) + + +# pip install -r requirements.txt +# export SLACK_SIGNING_SECRET=*** +# export SLACK_BOT_TOKEN=xoxb-*** +# uvicorn async_app_custom_props:api --reload --port 3000 --log-level warning diff --git a/slack_bolt/adapter/starlette/async_handler.py b/slack_bolt/adapter/starlette/async_handler.py index bc38176bf..5fb13ade3 100644 --- a/slack_bolt/adapter/starlette/async_handler.py +++ b/slack_bolt/adapter/starlette/async_handler.py @@ -1,3 +1,5 @@ +from typing import Dict, Any, Optional + from starlette.requests import Request from starlette.responses import Response @@ -6,12 +8,20 @@ from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow -def to_async_bolt_request(req: Request, body: bytes) -> AsyncBoltRequest: - return AsyncBoltRequest( +def to_async_bolt_request( + req: Request, + body: bytes, + addition_context_properties: Optional[Dict[str, Any]] = None, +) -> AsyncBoltRequest: + request = AsyncBoltRequest( body=body.decode("utf-8"), query=req.query_params, headers=req.headers, ) + if addition_context_properties is not None: + for k, v in addition_context_properties.items(): + request.context[k] = v + return request def to_starlette_response(bolt_resp: BoltResponse) -> Response: @@ -39,23 +49,27 @@ class AsyncSlackRequestHandler: def __init__(self, app: AsyncApp): # type: ignore self.app = app - async def handle(self, req: Request) -> Response: + async def handle( + self, req: Request, addition_context_properties: Optional[Dict[str, Any]] = None + ) -> Response: body = await req.body() if req.method == "GET": if self.app.oauth_flow is not None: oauth_flow: AsyncOAuthFlow = self.app.oauth_flow if req.url.path == oauth_flow.install_path: bolt_resp = await oauth_flow.handle_installation( - to_async_bolt_request(req, body) + to_async_bolt_request(req, body, addition_context_properties) ) return to_starlette_response(bolt_resp) elif req.url.path == oauth_flow.redirect_uri_path: bolt_resp = await oauth_flow.handle_callback( - to_async_bolt_request(req, body) + to_async_bolt_request(req, body, addition_context_properties) ) return to_starlette_response(bolt_resp) elif req.method == "POST": - bolt_resp = await self.app.async_dispatch(to_async_bolt_request(req, body)) + bolt_resp = await self.app.async_dispatch( + to_async_bolt_request(req, body, addition_context_properties) + ) return to_starlette_response(bolt_resp) return Response( diff --git a/tests/adapter_tests_async/test_async_fastapi.py b/tests/adapter_tests_async/test_async_fastapi.py index 41e1f4923..5850114d6 100644 --- a/tests/adapter_tests_async/test_async_fastapi.py +++ b/tests/adapter_tests_async/test_async_fastapi.py @@ -2,7 +2,7 @@ from time import time from urllib.parse import quote -from fastapi import FastAPI +from fastapi import FastAPI, Depends from slack_sdk.signature import SignatureVerifier from slack_sdk.web.async_client import AsyncWebClient from starlette.requests import Request @@ -215,3 +215,51 @@ async def endpoint(req: Request): assert response.headers.get("content-type") == "text/html; charset=utf-8" assert response.headers.get("content-length") == "597" assert "https://slack.com/oauth/v2/authorize?state=" in response.text + + def test_custom_props(self): + app = AsyncApp( + client=self.web_client, + signing_secret=self.signing_secret, + ) + + async def shortcut_handler(ack, context): + assert context.get("foo") == "FOO" + await ack() + + app.shortcut("test-shortcut")(shortcut_handler) + + input = { + "type": "shortcut", + "token": "verification_token", + "action_ts": "111.111", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Org Name", + }, + "user": {"id": "W111", "username": "primary-owner", "team_id": "T111"}, + "callback_id": "test-shortcut", + "trigger_id": "111.111.xxxxxx", + } + + timestamp, body = str(int(time())), f"payload={quote(json.dumps(input))}" + + api = FastAPI() + app_handler = AsyncSlackRequestHandler(app) + + def get_foo(): + yield "FOO" + + @api.post("/slack/events") + async def endpoint(req: Request, foo: str = Depends(get_foo)): + return await app_handler.handle(req, {"foo": foo}) + + client = TestClient(api) + response = client.post( + "/slack/events", + data=body, + headers=self.build_headers(timestamp, body), + ) + assert response.status_code == 200 + assert_auth_test_count(self, 1) From 61b4ad7a3c007b53136edacda48a25fc2d640308 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Mon, 13 Dec 2021 07:47:40 +0900 Subject: [PATCH 2/2] Add the same for sync version --- slack_bolt/adapter/starlette/handler.py | 28 ++++++++--- tests/adapter_tests/starlette/test_fastapi.py | 50 ++++++++++++++++++- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/slack_bolt/adapter/starlette/handler.py b/slack_bolt/adapter/starlette/handler.py index c8cea10d4..d1c9a381e 100644 --- a/slack_bolt/adapter/starlette/handler.py +++ b/slack_bolt/adapter/starlette/handler.py @@ -1,3 +1,5 @@ +from typing import Dict, Any, Optional + from starlette.requests import Request from starlette.responses import Response @@ -5,12 +7,20 @@ from slack_bolt.oauth import OAuthFlow -def to_bolt_request(req: Request, body: bytes) -> BoltRequest: - return BoltRequest( +def to_bolt_request( + req: Request, + body: bytes, + addition_context_properties: Optional[Dict[str, Any]] = None, +) -> BoltRequest: + request = BoltRequest( body=body.decode("utf-8"), query=req.query_params, headers=req.headers, ) + if addition_context_properties is not None: + for k, v in addition_context_properties.items(): + request.context[k] = v + return request def to_starlette_response(bolt_resp: BoltResponse) -> Response: @@ -38,21 +48,27 @@ class SlackRequestHandler: def __init__(self, app: App): # type: ignore self.app = app - async def handle(self, req: Request) -> Response: + async def handle( + self, req: Request, addition_context_properties: Optional[Dict[str, Any]] = None + ) -> Response: body = await req.body() if req.method == "GET": if self.app.oauth_flow is not None: oauth_flow: OAuthFlow = self.app.oauth_flow if req.url.path == oauth_flow.install_path: bolt_resp = oauth_flow.handle_installation( - to_bolt_request(req, body) + to_bolt_request(req, body, addition_context_properties) ) return to_starlette_response(bolt_resp) elif req.url.path == oauth_flow.redirect_uri_path: - bolt_resp = oauth_flow.handle_callback(to_bolt_request(req, body)) + bolt_resp = oauth_flow.handle_callback( + to_bolt_request(req, body, addition_context_properties) + ) return to_starlette_response(bolt_resp) elif req.method == "POST": - bolt_resp = self.app.dispatch(to_bolt_request(req, body)) + bolt_resp = self.app.dispatch( + to_bolt_request(req, body, addition_context_properties) + ) return to_starlette_response(bolt_resp) return Response( diff --git a/tests/adapter_tests/starlette/test_fastapi.py b/tests/adapter_tests/starlette/test_fastapi.py index 4b09c618a..20da33a76 100644 --- a/tests/adapter_tests/starlette/test_fastapi.py +++ b/tests/adapter_tests/starlette/test_fastapi.py @@ -2,7 +2,7 @@ from time import time from urllib.parse import quote -from fastapi import FastAPI +from fastapi import FastAPI, Depends from slack_sdk.signature import SignatureVerifier from slack_sdk.web import WebClient from starlette.requests import Request @@ -214,3 +214,51 @@ async def endpoint(req: Request): assert response.status_code == 200 assert response.headers.get("content-type") == "text/html; charset=utf-8" assert "https://slack.com/oauth/v2/authorize?state=" in response.text + + def test_custom_props(self): + app = App( + client=self.web_client, + signing_secret=self.signing_secret, + ) + + def shortcut_handler(ack, context): + assert context.get("foo") == "FOO" + ack() + + app.shortcut("test-shortcut")(shortcut_handler) + + input = { + "type": "shortcut", + "token": "verification_token", + "action_ts": "111.111", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Org Name", + }, + "user": {"id": "W111", "username": "primary-owner", "team_id": "T111"}, + "callback_id": "test-shortcut", + "trigger_id": "111.111.xxxxxx", + } + + timestamp, body = str(int(time())), f"payload={quote(json.dumps(input))}" + + api = FastAPI() + app_handler = SlackRequestHandler(app) + + def get_foo(): + yield "FOO" + + @api.post("/slack/events") + async def endpoint(req: Request, foo: str = Depends(get_foo)): + return await app_handler.handle(req, {"foo": foo}) + + client = TestClient(api) + response = client.post( + "/slack/events", + data=body, + headers=self.build_headers(timestamp, body), + ) + assert response.status_code == 200 + assert_auth_test_count(self, 1)