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鈥檒l 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
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 app:api
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
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 app:api
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
api.add_route("/slack/install", resource)
api.add_route("/slack/oauth_redirect", resource)
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 @@
from .resource import SlackAppResource
from .async_resource import AsyncSlackAppResource
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
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.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
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):
if falcon_version.__version__.startswith("2."):
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError("ASGI Falcon requires version >= 3.0")
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
else:
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