Skip to content

Commit

Permalink
feat: add WSGI adapter (slackapi#1085)
Browse files Browse the repository at this point in the history
  • Loading branch information
WilliamBergamin authored May 28, 2024
1 parent dbe2333 commit 549252c
Show file tree
Hide file tree
Showing 11 changed files with 548 additions and 0 deletions.
19 changes: 19 additions & 0 deletions examples/wsgi/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from slack_bolt import App
from slack_bolt.adapter.wsgi import SlackRequestHandler

app = App()


@app.event("app_mention")
def handle_app_mentions(body, say, logger):
logger.info(body)
say("What's up?")


api = SlackRequestHandler(app)

# pip install -r requirements.txt
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# gunicorn app:api -b 0.0.0.0:3000 --log-level debug
# ngrok http 3000
23 changes: 23 additions & 0 deletions examples/wsgi/oauth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from slack_bolt import App
from slack_bolt.adapter.wsgi import SlackRequestHandler

app = App()


@app.event("app_mention")
def handle_app_mentions(body, say, logger):
logger.info(body)
say("What's up?")


api = SlackRequestHandler(app)

# pip install -r requirements.txt

# # -- OAuth flow -- #
# export SLACK_SIGNING_SECRET=***
# export SLACK_CLIENT_ID=111.111
# export SLACK_CLIENT_SECRET=***
# export SLACK_SCOPES=app_mentions:read,channels:history,im:history,chat:write

# gunicorn oauth_app:api -b 0.0.0.0:3000 --log-level debug
1 change: 1 addition & 0 deletions examples/wsgi/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gunicorn<23
3 changes: 3 additions & 0 deletions slack_bolt/adapter/wsgi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .handler import SlackRequestHandler

__all__ = ["SlackRequestHandler"]
85 changes: 85 additions & 0 deletions slack_bolt/adapter/wsgi/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from typing import Any, Callable, Dict, Iterable, List, Tuple

from slack_bolt import App
from slack_bolt.adapter.wsgi.http_request import WsgiHttpRequest
from slack_bolt.adapter.wsgi.http_response import WsgiHttpResponse
from slack_bolt.oauth.oauth_flow import OAuthFlow
from slack_bolt.request import BoltRequest
from slack_bolt.response import BoltResponse


class SlackRequestHandler:
def __init__(self, app: App, path: str = "/slack/events"):
"""Setup Bolt as a WSGI web framework, this will make your application compatible with WSGI web servers.
This can be used for production deployments.
With the default settings, `http://localhost:3000/slack/events`
Run Bolt with [gunicorn](https://gunicorn.org/)
# Python
app = App()
api = SlackRequestHandler(app)
# bash
export SLACK_SIGNING_SECRET=***
export SLACK_BOT_TOKEN=xoxb-***
gunicorn app:api -b 0.0.0.0:3000 --log-level debug
Args:
app: Your bolt application
path: The path to handle request from Slack (Default: `/slack/events`)
"""
self.path = path
self.app = app

def dispatch(self, request: WsgiHttpRequest) -> BoltResponse:
return self.app.dispatch(
BoltRequest(body=request.get_body(), query=request.query_string, headers=request.get_headers())
)

def handle_installation(self, request: WsgiHttpRequest) -> BoltResponse:
oauth_flow: OAuthFlow = self.app.oauth_flow
return oauth_flow.handle_installation(
BoltRequest(body=request.get_body(), query=request.query_string, headers=request.get_headers())
)

def handle_callback(self, request: WsgiHttpRequest) -> BoltResponse:
oauth_flow: OAuthFlow = self.app.oauth_flow
return oauth_flow.handle_callback(
BoltRequest(body=request.get_body(), query=request.query_string, headers=request.get_headers())
)

def _get_http_response(self, request: WsgiHttpRequest) -> WsgiHttpResponse:
if request.method == "GET":
if self.app.oauth_flow is not None:
if request.path == self.app.oauth_flow.install_path:
bolt_response: BoltResponse = self.handle_installation(request)
return WsgiHttpResponse(
status=bolt_response.status, headers=bolt_response.headers, body=bolt_response.body
)
if request.path == self.app.oauth_flow.redirect_uri_path:
bolt_response: BoltResponse = self.handle_callback(request)
return WsgiHttpResponse(
status=bolt_response.status, headers=bolt_response.headers, body=bolt_response.body
)
if request.method == "POST" and request.path == self.path:
bolt_response: BoltResponse = self.dispatch(request)
return WsgiHttpResponse(status=bolt_response.status, headers=bolt_response.headers, body=bolt_response.body)
return WsgiHttpResponse(status=404, headers={"content-type": ["text/plain;charset=utf-8"]}, body="Not Found")

def __call__(
self,
environ: Dict[str, Any],
start_response: Callable[[str, List[Tuple[str, str]]], None],
) -> Iterable[bytes]:
request = WsgiHttpRequest(environ)
if "HTTP" in request.protocol:
response: WsgiHttpResponse = self._get_http_response(
request=request,
)
start_response(response.status, response.get_headers())
return response.get_body()
raise TypeError(f"Unsupported SERVER_PROTOCOL: {request.protocol}")
37 changes: 37 additions & 0 deletions slack_bolt/adapter/wsgi/http_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Any, Dict

from .internals import ENCODING


class WsgiHttpRequest:
"""This Class uses the PEP 3333 standard to extract request information
from the WSGI web server running the application
PEP 3333: https://peps.python.org/pep-3333/
"""

__slots__ = ("method", "path", "query_string", "protocol", "environ")

def __init__(self, environ: Dict[str, Any]):
self.method: str = environ.get("REQUEST_METHOD", "GET")
self.path: str = environ.get("PATH_INFO", "")
self.query_string: str = environ.get("QUERY_STRING", "")
self.protocol: str = environ.get("SERVER_PROTOCOL", "")
self.environ = environ

def get_headers(self) -> Dict[str, str]:
headers = {}
for key, value in self.environ.items():
if key in {"CONTENT_LENGTH", "CONTENT_TYPE"}:
name = key.lower().replace("_", "-")
headers[name] = value
if key.startswith("HTTP_"):
name = key[len("HTTP_"):].lower().replace("_", "-") # fmt: skip
headers[name] = value
return headers

def get_body(self) -> str:
if "wsgi.input" not in self.environ:
return ""
content_length = int(self.environ.get("CONTENT_LENGTH", 0))
return self.environ["wsgi.input"].read(content_length).decode(ENCODING)
33 changes: 33 additions & 0 deletions slack_bolt/adapter/wsgi/http_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from http import HTTPStatus
from typing import Dict, Iterable, List, Sequence, Tuple

from .internals import ENCODING


class WsgiHttpResponse:
"""This Class uses the PEP 3333 standard to adapt bolt response information
for the WSGI web server running the application
PEP 3333: https://peps.python.org/pep-3333/
"""

__slots__ = ("status", "_headers", "_body")

def __init__(self, status: int, headers: Dict[str, Sequence[str]] = {}, body: str = ""):
_status = HTTPStatus(status)
self.status = f"{_status.value} {_status.phrase}"
self._headers = headers
self._body = bytes(body, ENCODING)

def get_headers(self) -> List[Tuple[str, str]]:
headers: List[Tuple[str, str]] = []
for key, value in self._headers.items():
if key.lower() == "content-length":
continue
headers.append((key, value[0]))

headers.append(("content-length", str(len(self._body))))
return headers

def get_body(self) -> Iterable[bytes]:
return [self._body]
1 change: 1 addition & 0 deletions slack_bolt/adapter/wsgi/internals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENCODING = "utf-8" # The content encoding for Slack requests/responses is always utf-8
Empty file.
Loading

0 comments on commit 549252c

Please sign in to comment.