Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions examples/quart/async_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.quart import AsyncSlackRequestHandler

app = AsyncApp()
app_handler = AsyncSlackRequestHandler(app)


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


from quart import Quart, request

api = Quart(__name__)


@api.post("/slack/events")
async def endpoint():
return await app_handler.handle(request)


if __name__ == "__main__":
api.run(host="0.0.0.0", port=int(os.environ.get("PORT", 3000)))


# Requires Python 3.9+
# pip install -r requirements.txt
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# hypercorn async_app:api --reload --bind 0.0.0.0:3000
48 changes: 48 additions & 0 deletions examples/quart/async_oauth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.quart import AsyncSlackRequestHandler

app = AsyncApp()
app_handler = AsyncSlackRequestHandler(app)


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


from quart import Quart, request

api = Quart(__name__)


@api.post("/slack/events")
async def endpoint():
return await app_handler.handle(request)


@api.get("/slack/install")
async def install():
return await app_handler.handle(request)


@api.get("/slack/oauth_redirect")
async def oauth_redirect():
return await app_handler.handle(request)


if __name__ == "__main__":
api.run(host="0.0.0.0", port=int(os.environ.get("PORT", 3000)))


# Requires Python 3.9+
# 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

# hypercorn async_oauth_app:api --reload --bind 0.0.0.0:3000
2 changes: 2 additions & 0 deletions examples/quart/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Requires Python 3.9+
quart>=0.20,<1
1 change: 1 addition & 0 deletions requirements/adapter_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ falcon>=2,<4; python_version<"3.9"
falcon>=4.2.0,<5; python_version>="3.9"
fastapi>=0.70.0,<1
Flask>=1,<4
quart>=0.20,<1; python_version>="3.9"
Werkzeug>=2,<3; python_version<"3.9"
Werkzeug>=3.1.8,<4; python_version>="3.9"
pyramid>=1,<3
Expand Down
5 changes: 5 additions & 0 deletions slack_bolt/adapter/quart/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .async_handler import AsyncSlackRequestHandler

__all__ = [
"AsyncSlackRequestHandler",
]
73 changes: 73 additions & 0 deletions slack_bolt/adapter/quart/async_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Any, Dict, Optional, cast

from quart import Request, Response, make_response

from slack_bolt import BoltResponse
from slack_bolt.async_app import AsyncApp, AsyncBoltRequest
from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow


async def to_async_bolt_request(
req: Request,
addition_context_properties: Optional[Dict[str, Any]] = None,
) -> AsyncBoltRequest:
request = AsyncBoltRequest(
body=cast(str, await req.get_data(as_text=True)),
query=req.query_string.decode("utf-8"),
headers=req.headers, # type: ignore[arg-type]
)

if addition_context_properties is not None:
for k, v in addition_context_properties.items():
request.context[k] = v

return request


async def to_quart_response(bolt_resp: BoltResponse) -> Response:
resp = cast(Response, await make_response(bolt_resp.body, bolt_resp.status))
for k, values in bolt_resp.headers.items():
if k == "set-cookie":
continue
if k.lower() == "content-type" and resp.headers.get("content-type") is not None:
resp.headers.pop("content-type")
for v in values:
resp.headers.add_header(k, v)

for cookie in bolt_resp.cookies():
for name, c in cookie.items():
max_age = int(c["max-age"]) if c.get("max-age") else None
resp.set_cookie(
key=name,
value=c.value,
max_age=max_age,
expires=c.get("expires") or None,
path=c.get("path") or None,
domain=c.get("domain") or None,
secure=True,
httponly=True,
)

return resp


class AsyncSlackRequestHandler:
def __init__(self, app: AsyncApp):
self.app = app

async def handle(self, req: Request, addition_context_properties: Optional[Dict[str, Any]] = None) -> Response:
if req.method == "POST":
bolt_resp = await self.app.async_dispatch(await to_async_bolt_request(req, addition_context_properties))
return await to_quart_response(bolt_resp)

if req.method == "GET" and self.app.oauth_flow is not None:
oauth_flow: AsyncOAuthFlow = self.app.oauth_flow
bolt_req = await to_async_bolt_request(req, addition_context_properties)
if req.path == oauth_flow.install_path:
bolt_resp = await oauth_flow.handle_installation(bolt_req)
return await to_quart_response(bolt_resp)
if req.path == oauth_flow.redirect_uri_path:
bolt_resp = await oauth_flow.handle_callback(bolt_req)
return await to_quart_response(bolt_resp)

return cast(Response, await make_response("Not Found", 404))
Loading