Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Falcon (ASGI) adapter #614

Merged
merged 10 commits into from
Mar 15, 2022
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ jobs:
- name: Run tests for HTTP Mode adapters (asyncio-based libraries)
run: |
pip install -e ".[async]"
pip install "falcon>=3,<4"
pytest tests/adapter_tests_async/
100 changes: 100 additions & 0 deletions examples/falcon/async_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import falcon
import logging
import re
from slack_bolt.async_app import AsyncApp, AsyncRespond, AsyncAck
from slack_bolt.adapter.falcon import AsyncSlackAppResource

logging.basicConfig(level=logging.DEBUG)
app = AsyncApp()


# @app.command("/bolt-py-proto", [lambda body: body["team_id"] == "T03E94MJU"])
async def test_command(logger: logging.Logger, body: dict, ack: AsyncAck, respond: AsyncRespond):
logger.info(body)
await ack("thanks!")
await respond(
blocks=[
{
"type": "section",
"block_id": "b",
"text": {
"type": "mrkdwn",
"text": "You can add a button alongside text in your message. ",
},
"accessory": {
"type": "button",
"action_id": "a",
"text": {"type": "plain_text", "text": "Button"},
"value": "click_me_123",
},
}
]
)


app.command(re.compile(r"/hello-bolt-.+"))(test_command)


@app.shortcut("test-shortcut")
async def test_shortcut(ack, client, logger, body):
logger.info(body)
await ack()
res = await client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "view-id",
"title": {
"type": "plain_text",
"text": "My App",
},
"submit": {
"type": "plain_text",
"text": "Submit",
},
"close": {
"type": "plain_text",
"text": "Cancel",
},
"blocks": [
{
"type": "input",
"element": {"type": "plain_text_input"},
"label": {
"type": "plain_text",
"text": "Label",
},
}
],
},
)
logger.info(res)


@app.view("view-id")
async def view_submission(ack, body, logger):
logger.info(body)
await ack()


@app.action("a")
async def button_click(logger, action, ack, respond):
logger.info(action)
await ack()
await respond("Here is my response")


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


api = falcon.asgi.App()
resource = AsyncSlackAppResource(app)
api.add_route("/slack/events", resource)

# pip install -r requirements.txt
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# uvicorn --reload -h 0.0.0.0 -p 3000 async_app:api
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sarayourfriend Can you add unvicorn to requirements.txt in the same directory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing. I'm not sure what version range to pin it to. The project is still <1. Should I just use uvicorn>=0.17.6 (the currently released version)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do the same with fastapi examples

102 changes: 102 additions & 0 deletions examples/falcon/async_oauth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import falcon
import logging
import re
from slack_bolt.async_app import AsyncApp, AsyncRespond, AsyncAck
from slack_bolt.adapter.falcon import AsyncSlackAppResource

logging.basicConfig(level=logging.DEBUG)
app = AsyncApp()


# @app.command("/bolt-py-proto", [lambda body: body["team_id"] == "T03E94MJU"])
async def test_command(logger: logging.Logger, body: dict, ack: AsyncAck, respond: AsyncRespond):
logger.info(body)
await ack("thanks!")
await respond(
blocks=[
{
"type": "section",
"block_id": "b",
"text": {
"type": "mrkdwn",
"text": "You can add a button alongside text in your message. ",
},
"accessory": {
"type": "button",
"action_id": "a",
"text": {"type": "plain_text", "text": "Button"},
"value": "click_me_123",
},
}
]
)


app.command(re.compile(r"/hello-bolt-.+"))(test_command)


@app.shortcut("test-shortcut")
async def test_shortcut(ack, client, logger, body):
logger.info(body)
await ack()
res = await client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "view-id",
"title": {
"type": "plain_text",
"text": "My App",
},
"submit": {
"type": "plain_text",
"text": "Submit",
},
"close": {
"type": "plain_text",
"text": "Cancel",
},
"blocks": [
{
"type": "input",
"element": {"type": "plain_text_input"},
"label": {
"type": "plain_text",
"text": "Label",
},
}
],
},
)
logger.info(res)


@app.view("view-id")
async def view_submission(ack, body, logger):
logger.info(body)
await ack()


@app.action("a")
async def button_click(logger, action, ack, respond):
logger.info(action)
await ack()
await respond("Here is my response")


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


api = falcon.asgi.App()
resource = AsyncSlackAppResource(app)
api.add_route("/slack/events", resource)

# pip install -r requirements.txt
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# uvicorn --reload -h 0.0.0.0 -p 3000 async_oauth_app:api
api.add_route("/slack/install", resource)
api.add_route("/slack/oauth_redirect", resource)
3 changes: 2 additions & 1 deletion examples/falcon/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
falcon>=2,<3
gunicorn>=20,<21
gunicorn>=20,<21
uvicorn
1 change: 1 addition & 0 deletions slack_bolt/adapter/falcon/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# Don't add async module imports here
from .resource import SlackAppResource
84 changes: 84 additions & 0 deletions slack_bolt/adapter/falcon/async_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from datetime import datetime # type: ignore
from http import HTTPStatus

from falcon import version as falcon_version
from falcon.asgi import Request, Response
from slack_bolt import BoltResponse
from slack_bolt.async_app import AsyncApp
from slack_bolt.error import BoltError
from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow
from slack_bolt.request.async_request import AsyncBoltRequest


class AsyncSlackAppResource:
"""
For use with ASGI Falcon Apps.

from slack_bolt.async_app import AsyncApp
app = AsyncApp()

import falcon
app = falcon.asgi.App()
app.add_route("/slack/events", AsyncSlackAppResource(app))
"""

def __init__(self, app: AsyncApp): # type: ignore
if falcon_version.__version__.startswith("2."):
raise BoltError("This ASGI compatible adapter requires Falcon version >= 3.0")

self.app = app

async def on_get(self, req: Request, resp: Response):
if self.app.oauth_flow is not None:
oauth_flow: AsyncOAuthFlow = self.app.oauth_flow
if req.path == oauth_flow.install_path:
bolt_resp = await oauth_flow.handle_installation(
await self._to_bolt_request(req)
)
await self._write_response(bolt_resp, resp)
return
elif req.path == oauth_flow.redirect_uri_path:
bolt_resp = await oauth_flow.handle_callback(
await self._to_bolt_request(req)
)
await self._write_response(bolt_resp, resp)
return

resp.status = "404"
resp.body = "The page is not found..."

async def on_post(self, req: Request, resp: Response):
bolt_req = await self._to_bolt_request(req)
bolt_resp = await self.app.async_dispatch(bolt_req)
await self._write_response(bolt_resp, resp)

async def _to_bolt_request(self, req: Request) -> AsyncBoltRequest:
return AsyncBoltRequest(
body=(await req.stream.read(req.content_length or 0)).decode("utf-8"),
query=req.query_string,
headers={k.lower(): v for k, v in req.headers.items()},
)

async def _write_response(self, bolt_resp: BoltResponse, resp: Response):
resp.text = bolt_resp.body
status = HTTPStatus(bolt_resp.status)
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
resp.status = str(f"{status.value} {status.phrase}")
resp.set_headers(bolt_resp.first_headers_without_set_cookie())
for cookie in bolt_resp.cookies():
for name, c in cookie.items():
expire_value = c.get("expires")
expire = (
datetime.strptime(expire_value, "%a, %d %b %Y %H:%M:%S %Z")
if expire_value
else None
)
resp.set_cookie(
name=name,
value=c.value,
expires=expire,
max_age=c.get("max-age"),
domain=c.get("domain"),
path=c.get("path"),
secure=True,
http_only=True,
)
Loading