Skip to content

Commit

Permalink
Fix #754 by adding the async version of Tornado adapter (#758)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Nov 8, 2022
1 parent 3cdb1dc commit cbf2383
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 4 deletions.
4 changes: 2 additions & 2 deletions examples/tornado/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@


@app.middleware # or app.use(log_request)
def log_request(logger, body, next):
def log_request(logger, body, next_):
logger.debug(body)
return next()
next_()


@app.event("app_mention")
Expand Down
35 changes: 35 additions & 0 deletions examples/tornado/async_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

logging.basicConfig(level=logging.DEBUG)

from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.tornado.async_handler import AsyncSlackEventsHandler

app = AsyncApp()


@app.middleware # or app.use(log_request)
async def log_request(logger, body, next_):
logger.debug(body)
await next_()


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


from tornado.web import Application
from tornado.ioloop import IOLoop

api = Application([("/slack/events", AsyncSlackEventsHandler, dict(app=app))])

if __name__ == "__main__":
api.listen(3000)
IOLoop.current().start()

# pip install -r requirements.txt
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# python async_app.py
45 changes: 45 additions & 0 deletions examples/tornado/async_oauth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import logging
from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.tornado.async_handler import AsyncSlackEventsHandler, AsyncSlackOAuthHandler

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


@app.middleware # or app.use(log_request)
async def log_request(logger, body, next_):
logger.debug(body)
await next_()


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


from tornado.web import Application
from tornado.ioloop import IOLoop

api = Application(
[
("/slack/events", AsyncSlackEventsHandler, dict(app=app)),
("/slack/install", AsyncSlackOAuthHandler, dict(app=app)),
("/slack/oauth_redirect", AsyncSlackOAuthHandler, dict(app=app)),
]
)

if __name__ == "__main__":
api.listen(3000)
IOLoop.current().start()

# pip install -r requirements.txt

# # -- OAuth flow -- #
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# export SLACK_CLIENT_ID=111.111
# export SLACK_CLIENT_SECRET=***
# export SLACK_SCOPES=app_mentions:read,chat:write

# python async_oauth_app.py
4 changes: 2 additions & 2 deletions examples/tornado/oauth_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
@app.middleware # or app.use(log_request)
def log_request(logger, body, next):
logger.debug(body)
return next()
next()


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

Expand Down
1 change: 1 addition & 0 deletions slack_bolt/adapter/tornado/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Don't add async module imports here
from .handler import SlackEventsHandler, SlackOAuthHandler

__all__ = [
Expand Down
44 changes: 44 additions & 0 deletions slack_bolt/adapter/tornado/async_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from tornado.httputil import HTTPServerRequest
from tornado.web import RequestHandler

from slack_bolt.async_app import AsyncApp
from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow
from slack_bolt.request.async_request import AsyncBoltRequest
from slack_bolt.response import BoltResponse
from .handler import set_response


class AsyncSlackEventsHandler(RequestHandler):
def initialize(self, app: AsyncApp): # type: ignore
self.app = app

async def post(self):
bolt_resp: BoltResponse = await self.app.async_dispatch(to_async_bolt_request(self.request))
set_response(self, bolt_resp)
return


class AsyncSlackOAuthHandler(RequestHandler):
def initialize(self, app: AsyncApp): # type: ignore
self.app = app

async def get(self):
if self.app.oauth_flow is not None: # type: ignore
oauth_flow: AsyncOAuthFlow = self.app.oauth_flow # type: ignore
if self.request.path == oauth_flow.install_path:
bolt_resp = await oauth_flow.handle_installation(to_async_bolt_request(self.request))
set_response(self, bolt_resp)
return
elif self.request.path == oauth_flow.redirect_uri_path:
bolt_resp = await oauth_flow.handle_callback(to_async_bolt_request(self.request))
set_response(self, bolt_resp)
return
self.set_status(404)


def to_async_bolt_request(req: HTTPServerRequest) -> AsyncBoltRequest:
return AsyncBoltRequest(
body=req.body.decode("utf-8") if req.body else "",
query=req.query,
headers=req.headers,
)
170 changes: 170 additions & 0 deletions tests/adapter_tests_async/test_tornado.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import json
from time import time
from urllib.parse import quote

from slack_sdk.signature import SignatureVerifier
from slack_sdk.web.async_client import AsyncWebClient
from tornado.httpclient import HTTPRequest, HTTPResponse
from tornado.testing import AsyncHTTPTestCase, gen_test
from tornado.web import Application

from slack_bolt.adapter.tornado.async_handler import AsyncSlackEventsHandler
from slack_bolt.async_app import AsyncApp
from tests.mock_web_api_server import (
setup_mock_web_api_server,
cleanup_mock_web_api_server,
assert_auth_test_count,
)
from tests.utils import remove_os_env_temporarily, restore_os_env

signing_secret = "secret"
valid_token = "xoxb-valid"
mock_api_server_base_url = "http://localhost:8888"


async def event_handler():
pass


async def shortcut_handler(ack):
await ack()


async def command_handler(ack):
await ack()


class TestTornado(AsyncHTTPTestCase):
signature_verifier = SignatureVerifier(signing_secret)

def setUp(self):
self.old_os_env = remove_os_env_temporarily()
setup_mock_web_api_server(self)

web_client = AsyncWebClient(
token=valid_token,
base_url=mock_api_server_base_url,
)
self.app = AsyncApp(
client=web_client,
signing_secret=signing_secret,
)
self.app.event("app_mention")(event_handler)
self.app.shortcut("test-shortcut")(shortcut_handler)
self.app.command("/hello-world")(command_handler)

AsyncHTTPTestCase.setUp(self)

def tearDown(self):
AsyncHTTPTestCase.tearDown(self)
cleanup_mock_web_api_server(self)
restore_os_env(self.old_os_env)

def get_app(self):
return Application([("/slack/events", AsyncSlackEventsHandler, dict(app=self.app))])

def generate_signature(self, body: str, timestamp: str):
return self.signature_verifier.generate_signature(
body=body,
timestamp=timestamp,
)

def build_headers(self, timestamp: str, body: str):
content_type = "application/json" if body.startswith("{") else "application/x-www-form-urlencoded"
return {
"content-type": content_type,
"x-slack-signature": self.generate_signature(body, timestamp),
"x-slack-request-timestamp": timestamp,
}

@gen_test
async def test_events(self):
input = {
"token": "verification_token",
"team_id": "T111",
"enterprise_id": "E111",
"api_app_id": "A111",
"event": {
"client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63",
"type": "app_mention",
"text": "<@W111> Hi there!",
"user": "W222",
"ts": "1595926230.009600",
"team": "T111",
"channel": "C111",
"event_ts": "1595926230.009600",
},
"type": "event_callback",
"event_id": "Ev111",
"event_time": 1595926230,
"authed_users": ["W111"],
}
timestamp, body = str(int(time())), json.dumps(input)

request = HTTPRequest(
url=self.get_url("/slack/events"),
method="POST",
body=body,
headers=self.build_headers(timestamp, body),
)
response: HTTPResponse = await self.http_client.fetch(request)
assert response.code == 200
assert_auth_test_count(self, 1)

@gen_test
async def test_shortcuts(self):
input = {
"type": "shortcut",
"token": "verification_token",
"action_ts": "111.111",
"team": {
"id": "T111",
"domain": "workspace-domain",
"enterprise_id": "E111",
"enterprise_name": "Org Name",
},
"user": {"id": "W111", "username": "primary-owner", "team_id": "T111"},
"callback_id": "test-shortcut",
"trigger_id": "111.111.xxxxxx",
}

timestamp, body = str(int(time())), f"payload={quote(json.dumps(input))}"

request = HTTPRequest(
url=self.get_url("/slack/events"),
method="POST",
body=body,
headers=self.build_headers(timestamp, body),
)
response: HTTPResponse = await self.http_client.fetch(request)
assert response.code == 200
assert_auth_test_count(self, 1)

@gen_test
async def test_commands(self):
input = (
"token=verification_token"
"&team_id=T111"
"&team_domain=test-domain"
"&channel_id=C111"
"&channel_name=random"
"&user_id=W111"
"&user_name=primary-owner"
"&command=%2Fhello-world"
"&text=Hi"
"&enterprise_id=E111"
"&enterprise_name=Org+Name"
"&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT111%2F111%2Fxxxxx"
"&trigger_id=111.111.xxx"
)
timestamp, body = str(int(time())), input

request = HTTPRequest(
url=self.get_url("/slack/events"),
method="POST",
body=body,
headers=self.build_headers(timestamp, body),
)
response: HTTPResponse = await self.http_client.fetch(request)
assert response.code == 200
assert_auth_test_count(self, 1)
41 changes: 41 additions & 0 deletions tests/adapter_tests_async/test_tornado_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPClientError
from tornado.testing import AsyncHTTPTestCase, gen_test
from tornado.web import Application

from slack_bolt.adapter.tornado.async_handler import AsyncSlackOAuthHandler
from slack_bolt.async_app import AsyncApp
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
from tests.utils import remove_os_env_temporarily, restore_os_env

signing_secret = "secret"

app = AsyncApp(
signing_secret=signing_secret,
oauth_settings=AsyncOAuthSettings(
client_id="111.111",
client_secret="xxx",
scopes=["chat:write", "commands"],
),
)


class TestTornado(AsyncHTTPTestCase):
def get_app(self):
return Application([("/slack/install", AsyncSlackOAuthHandler, dict(app=app))])

def setUp(self):
AsyncHTTPTestCase.setUp(self)
self.old_os_env = remove_os_env_temporarily()

def tearDown(self):
AsyncHTTPTestCase.tearDown(self)
restore_os_env(self.old_os_env)

@gen_test
async def test_oauth(self):
request = HTTPRequest(url=self.get_url("/slack/install"), method="GET", follow_redirects=False)
try:
response: HTTPResponse = await self.http_client.fetch(request)
assert response.code == 200
except HTTPClientError as e:
assert e.code == 200

0 comments on commit cbf2383

Please sign in to comment.