Skip to content

Commit 549252c

Browse files
feat: add WSGI adapter (#1085)
1 parent dbe2333 commit 549252c

File tree

11 files changed

+548
-0
lines changed

11 files changed

+548
-0
lines changed

examples/wsgi/app.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from slack_bolt import App
2+
from slack_bolt.adapter.wsgi import SlackRequestHandler
3+
4+
app = App()
5+
6+
7+
@app.event("app_mention")
8+
def handle_app_mentions(body, say, logger):
9+
logger.info(body)
10+
say("What's up?")
11+
12+
13+
api = SlackRequestHandler(app)
14+
15+
# pip install -r requirements.txt
16+
# export SLACK_SIGNING_SECRET=***
17+
# export SLACK_BOT_TOKEN=xoxb-***
18+
# gunicorn app:api -b 0.0.0.0:3000 --log-level debug
19+
# ngrok http 3000

examples/wsgi/oauth_app.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from slack_bolt import App
2+
from slack_bolt.adapter.wsgi import SlackRequestHandler
3+
4+
app = App()
5+
6+
7+
@app.event("app_mention")
8+
def handle_app_mentions(body, say, logger):
9+
logger.info(body)
10+
say("What's up?")
11+
12+
13+
api = SlackRequestHandler(app)
14+
15+
# pip install -r requirements.txt
16+
17+
# # -- OAuth flow -- #
18+
# export SLACK_SIGNING_SECRET=***
19+
# export SLACK_CLIENT_ID=111.111
20+
# export SLACK_CLIENT_SECRET=***
21+
# export SLACK_SCOPES=app_mentions:read,channels:history,im:history,chat:write
22+
23+
# gunicorn oauth_app:api -b 0.0.0.0:3000 --log-level debug

examples/wsgi/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gunicorn<23
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .handler import SlackRequestHandler
2+
3+
__all__ = ["SlackRequestHandler"]

slack_bolt/adapter/wsgi/handler.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from typing import Any, Callable, Dict, Iterable, List, Tuple
2+
3+
from slack_bolt import App
4+
from slack_bolt.adapter.wsgi.http_request import WsgiHttpRequest
5+
from slack_bolt.adapter.wsgi.http_response import WsgiHttpResponse
6+
from slack_bolt.oauth.oauth_flow import OAuthFlow
7+
from slack_bolt.request import BoltRequest
8+
from slack_bolt.response import BoltResponse
9+
10+
11+
class SlackRequestHandler:
12+
def __init__(self, app: App, path: str = "/slack/events"):
13+
"""Setup Bolt as a WSGI web framework, this will make your application compatible with WSGI web servers.
14+
This can be used for production deployments.
15+
16+
With the default settings, `http://localhost:3000/slack/events`
17+
Run Bolt with [gunicorn](https://gunicorn.org/)
18+
19+
# Python
20+
app = App()
21+
22+
api = SlackRequestHandler(app)
23+
24+
# bash
25+
export SLACK_SIGNING_SECRET=***
26+
27+
export SLACK_BOT_TOKEN=xoxb-***
28+
29+
gunicorn app:api -b 0.0.0.0:3000 --log-level debug
30+
31+
Args:
32+
app: Your bolt application
33+
path: The path to handle request from Slack (Default: `/slack/events`)
34+
"""
35+
self.path = path
36+
self.app = app
37+
38+
def dispatch(self, request: WsgiHttpRequest) -> BoltResponse:
39+
return self.app.dispatch(
40+
BoltRequest(body=request.get_body(), query=request.query_string, headers=request.get_headers())
41+
)
42+
43+
def handle_installation(self, request: WsgiHttpRequest) -> BoltResponse:
44+
oauth_flow: OAuthFlow = self.app.oauth_flow
45+
return oauth_flow.handle_installation(
46+
BoltRequest(body=request.get_body(), query=request.query_string, headers=request.get_headers())
47+
)
48+
49+
def handle_callback(self, request: WsgiHttpRequest) -> BoltResponse:
50+
oauth_flow: OAuthFlow = self.app.oauth_flow
51+
return oauth_flow.handle_callback(
52+
BoltRequest(body=request.get_body(), query=request.query_string, headers=request.get_headers())
53+
)
54+
55+
def _get_http_response(self, request: WsgiHttpRequest) -> WsgiHttpResponse:
56+
if request.method == "GET":
57+
if self.app.oauth_flow is not None:
58+
if request.path == self.app.oauth_flow.install_path:
59+
bolt_response: BoltResponse = self.handle_installation(request)
60+
return WsgiHttpResponse(
61+
status=bolt_response.status, headers=bolt_response.headers, body=bolt_response.body
62+
)
63+
if request.path == self.app.oauth_flow.redirect_uri_path:
64+
bolt_response: BoltResponse = self.handle_callback(request)
65+
return WsgiHttpResponse(
66+
status=bolt_response.status, headers=bolt_response.headers, body=bolt_response.body
67+
)
68+
if request.method == "POST" and request.path == self.path:
69+
bolt_response: BoltResponse = self.dispatch(request)
70+
return WsgiHttpResponse(status=bolt_response.status, headers=bolt_response.headers, body=bolt_response.body)
71+
return WsgiHttpResponse(status=404, headers={"content-type": ["text/plain;charset=utf-8"]}, body="Not Found")
72+
73+
def __call__(
74+
self,
75+
environ: Dict[str, Any],
76+
start_response: Callable[[str, List[Tuple[str, str]]], None],
77+
) -> Iterable[bytes]:
78+
request = WsgiHttpRequest(environ)
79+
if "HTTP" in request.protocol:
80+
response: WsgiHttpResponse = self._get_http_response(
81+
request=request,
82+
)
83+
start_response(response.status, response.get_headers())
84+
return response.get_body()
85+
raise TypeError(f"Unsupported SERVER_PROTOCOL: {request.protocol}")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import Any, Dict
2+
3+
from .internals import ENCODING
4+
5+
6+
class WsgiHttpRequest:
7+
"""This Class uses the PEP 3333 standard to extract request information
8+
from the WSGI web server running the application
9+
10+
PEP 3333: https://peps.python.org/pep-3333/
11+
"""
12+
13+
__slots__ = ("method", "path", "query_string", "protocol", "environ")
14+
15+
def __init__(self, environ: Dict[str, Any]):
16+
self.method: str = environ.get("REQUEST_METHOD", "GET")
17+
self.path: str = environ.get("PATH_INFO", "")
18+
self.query_string: str = environ.get("QUERY_STRING", "")
19+
self.protocol: str = environ.get("SERVER_PROTOCOL", "")
20+
self.environ = environ
21+
22+
def get_headers(self) -> Dict[str, str]:
23+
headers = {}
24+
for key, value in self.environ.items():
25+
if key in {"CONTENT_LENGTH", "CONTENT_TYPE"}:
26+
name = key.lower().replace("_", "-")
27+
headers[name] = value
28+
if key.startswith("HTTP_"):
29+
name = key[len("HTTP_"):].lower().replace("_", "-") # fmt: skip
30+
headers[name] = value
31+
return headers
32+
33+
def get_body(self) -> str:
34+
if "wsgi.input" not in self.environ:
35+
return ""
36+
content_length = int(self.environ.get("CONTENT_LENGTH", 0))
37+
return self.environ["wsgi.input"].read(content_length).decode(ENCODING)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from http import HTTPStatus
2+
from typing import Dict, Iterable, List, Sequence, Tuple
3+
4+
from .internals import ENCODING
5+
6+
7+
class WsgiHttpResponse:
8+
"""This Class uses the PEP 3333 standard to adapt bolt response information
9+
for the WSGI web server running the application
10+
11+
PEP 3333: https://peps.python.org/pep-3333/
12+
"""
13+
14+
__slots__ = ("status", "_headers", "_body")
15+
16+
def __init__(self, status: int, headers: Dict[str, Sequence[str]] = {}, body: str = ""):
17+
_status = HTTPStatus(status)
18+
self.status = f"{_status.value} {_status.phrase}"
19+
self._headers = headers
20+
self._body = bytes(body, ENCODING)
21+
22+
def get_headers(self) -> List[Tuple[str, str]]:
23+
headers: List[Tuple[str, str]] = []
24+
for key, value in self._headers.items():
25+
if key.lower() == "content-length":
26+
continue
27+
headers.append((key, value[0]))
28+
29+
headers.append(("content-length", str(len(self._body))))
30+
return headers
31+
32+
def get_body(self) -> Iterable[bytes]:
33+
return [self._body]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ENCODING = "utf-8" # The content encoding for Slack requests/responses is always utf-8

tests/adapter_tests/wsgi/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)