-
Notifications
You must be signed in to change notification settings - Fork 245
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
seratch
merged 10 commits into
slackapi:main
from
sarayourfriend:add/async-falcon-adapter
Mar 15, 2022
Merged
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
667d89f
Add ASGI Falcon App support
sarayourfriend 7bd69cf
Add examples for ASGI Falcon adapter
sarayourfriend 937fddc
Don't accidentally force async into non-async apps
sarayourfriend 515345c
Move version check early in the execution
sarayourfriend ae20975
Update examples/falcon/async_app.py
sarayourfriend dc59647
Update examples/falcon/async_oauth_app.py
sarayourfriend 19fa7f5
Indicate uvicorn as a dependency of the asgi app
sarayourfriend 95bc0ba
Manually install falcon >=3 for async tests
sarayourfriend 241a749
Fix response text not being set
sarayourfriend 5a9cb0a
Revert unnecessary falcon version bump
sarayourfriend File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
# Don't add async module imports here | ||
from .resource import SlackAppResource |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
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): | ||
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, | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)?There was a problem hiding this comment.
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