X Tutup
Skip to content

Commit becee21

Browse files
authored
Add CherryPy adapter (slackapi#47)
1 parent e93c62f commit becee21

File tree

9 files changed

+415
-1
lines changed

9 files changed

+415
-1
lines changed

samples/bottle/oauth_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ def oauth_redirect():
5757
# export SLACK_CLIENT_SECRET=***
5858
# export SLACK_SCOPES=app_mentions:read,chat:write
5959

60-
# FLASK_APP=oauth_app.py FLASK_ENV=development flask run -p 3000
60+
# python oauth_app.py

samples/cherrypy/app.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ------------------------------------------------
2+
# instead of slack_bolt in requirements.txt
3+
import sys
4+
5+
sys.path.insert(1, "../..")
6+
# ------------------------------------------------
7+
8+
import logging
9+
10+
logging.basicConfig(level=logging.DEBUG)
11+
12+
from slack_bolt import App
13+
from slack_bolt.adapter.cherrypy import SlackRequestHandler
14+
15+
app = App()
16+
17+
18+
@app.middleware # or app.use(log_request)
19+
def log_request(logger, payload, next):
20+
logger.debug(payload)
21+
return next()
22+
23+
24+
@app.command("/hello-bolt-python")
25+
def hello_command(ack):
26+
ack("Hi from CherryPy")
27+
28+
29+
@app.event("app_mention")
30+
def event_test(payload, say, logger):
31+
logger.info(payload)
32+
say("What's up?")
33+
34+
35+
import cherrypy
36+
37+
handler = SlackRequestHandler(app)
38+
39+
40+
class SlackApp(object):
41+
@cherrypy.expose
42+
@cherrypy.tools.slack_in()
43+
def events(self, **kwargs):
44+
return handler.handle()
45+
46+
47+
if __name__ == "__main__":
48+
cherrypy.config.update({"server.socket_port": 3000})
49+
cherrypy.quickstart(SlackApp(), "/slack")
50+
51+
# pip install -r requirements.txt
52+
# export SLACK_SIGNING_SECRET=***
53+
# export SLACK_BOT_TOKEN=xoxb-***
54+
# python app.py

samples/cherrypy/oauth_app.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ------------------------------------------------
2+
# instead of slack_bolt in requirements.txt
3+
import sys
4+
5+
sys.path.insert(1, "../../src")
6+
# ------------------------------------------------
7+
8+
import logging
9+
from slack_bolt import App
10+
from slack_bolt.adapter.cherrypy import SlackRequestHandler
11+
12+
logging.basicConfig(level=logging.DEBUG)
13+
app = App()
14+
15+
16+
@app.middleware # or app.use(log_request)
17+
def log_request(logger, payload, next):
18+
logger.debug(payload)
19+
return next()
20+
21+
22+
@app.command("/hello-bolt-python")
23+
def hello_command(ack):
24+
ack("Hi from CherryPy")
25+
26+
27+
@app.event("app_mention")
28+
def event_test(payload, say, logger):
29+
logger.info(payload)
30+
say("What's up?")
31+
32+
33+
import cherrypy
34+
35+
handler = SlackRequestHandler(app)
36+
37+
38+
class SlackApp(object):
39+
@cherrypy.expose
40+
@cherrypy.tools.slack_in()
41+
def events(self, **kwargs):
42+
return handler.handle()
43+
44+
@cherrypy.expose
45+
@cherrypy.tools.slack_in()
46+
def install(self, **kwargs):
47+
return handler.handle()
48+
49+
@cherrypy.expose
50+
@cherrypy.tools.slack_in()
51+
def oauth_redirect(self, **kwargs):
52+
return handler.handle()
53+
54+
55+
if __name__ == "__main__":
56+
cherrypy.config.update({"server.socket_port": 3000})
57+
cherrypy.quickstart(SlackApp(), "/slack")
58+
59+
# pip install -r requirements.txt
60+
61+
# # -- OAuth flow -- #
62+
# export SLACK_SIGNING_SECRET=***
63+
# export SLACK_BOT_TOKEN=xoxb-***
64+
# export SLACK_CLIENT_ID=111.111
65+
# export SLACK_CLIENT_SECRET=***
66+
# export SLACK_SCOPES=app_mentions:read,chat:write
67+
68+
# python oauth_app.py

samples/cherrypy/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CherryPy>=18,<19

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"bottle>=0.12,<1",
5454
"chalice>=1,<2",
5555
"click>=7,<8", # for chalice
56+
"CherryPy>=18,<19",
5657
"Django>=3,<4",
5758
"falcon>=2,<3",
5859
"fastapi<1",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .handler import SlackRequestHandler
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from typing import Optional
2+
3+
import cherrypy
4+
5+
from slack_bolt.app import App
6+
from slack_bolt.oauth import OAuthFlow
7+
from slack_bolt.request import BoltRequest
8+
from slack_bolt.response import BoltResponse
9+
10+
11+
def build_bolt_request() -> BoltRequest:
12+
req = cherrypy.request
13+
body = req.raw_body if hasattr(req, "raw_body") else ""
14+
return BoltRequest(body=body, query=req.query_string, headers=req.headers,)
15+
16+
17+
def set_response_status_and_headers(bolt_resp: BoltResponse) -> None:
18+
cherrypy.response.status = bolt_resp.status
19+
for k, v in bolt_resp.first_headers_without_set_cookie().items():
20+
cherrypy.response.headers[k] = v
21+
for cookie in bolt_resp.cookies():
22+
for name, c in cookie.items():
23+
str_max_age: Optional[str] = c.get("max-age", None)
24+
max_age: Optional[int] = int(str_max_age) if str_max_age else None
25+
cherrypy_cookie = cherrypy.response.cookie
26+
cherrypy_cookie[name] = c.value
27+
cherrypy_cookie[name]["expires"] = c.get("expires", None)
28+
cherrypy_cookie[name]["max-age"] = max_age
29+
cherrypy_cookie[name]["domain"] = c.get("domain", None)
30+
cherrypy_cookie[name]["path"] = c.get("path", None)
31+
cherrypy_cookie[name]["secure"] = True
32+
cherrypy_cookie[name]["httponly"] = True
33+
34+
35+
@cherrypy.tools.register("on_start_resource")
36+
def slack_in():
37+
request = cherrypy.serving.request
38+
39+
def slack_processor(entity):
40+
try:
41+
if request.process_request_body:
42+
body = entity.fp.read()
43+
body = body.decode("utf-8") if isinstance(body, bytes) else ""
44+
request.raw_body = body
45+
except ValueError:
46+
raise cherrypy.HTTPError(400, "Invalid request body")
47+
48+
request.body.processors.clear()
49+
request.body.processors["application/json"] = slack_processor
50+
request.body.processors["application/x-www-form-urlencoded"] = slack_processor
51+
52+
53+
class SlackRequestHandler:
54+
def __init__(self, app: App): # type: ignore
55+
self.app = app
56+
57+
def handle(self) -> bytes:
58+
req = cherrypy.request
59+
if req.method == "GET":
60+
if self.app.oauth_flow is not None:
61+
oauth_flow: OAuthFlow = self.app.oauth_flow
62+
request_path = req.wsgi_environ["REQUEST_URI"].split("?")[0]
63+
if request_path == oauth_flow.install_path:
64+
bolt_resp = oauth_flow.handle_installation(build_bolt_request())
65+
set_response_status_and_headers(bolt_resp)
66+
return (bolt_resp.body or "").encode("utf-8")
67+
elif request_path == oauth_flow.redirect_uri_path:
68+
bolt_resp = oauth_flow.handle_callback(build_bolt_request())
69+
set_response_status_and_headers(bolt_resp)
70+
return (bolt_resp.body or "").encode("utf-8")
71+
elif req.method == "POST":
72+
bolt_resp: BoltResponse = self.app.dispatch(build_bolt_request())
73+
set_response_status_and_headers(bolt_resp)
74+
return (bolt_resp.body or "").encode("utf-8")
75+
76+
cherrypy.response.status = 404
77+
return "Not Found".encode("utf-8")
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import json
2+
from time import time
3+
4+
import cherrypy
5+
from cherrypy.test import helper
6+
from slack_sdk.signature import SignatureVerifier
7+
from slack_sdk.web import WebClient
8+
9+
from slack_bolt.adapter.cherrypy import SlackRequestHandler
10+
from slack_bolt.app import App
11+
from tests.mock_web_api_server import (
12+
setup_mock_web_api_server,
13+
cleanup_mock_web_api_server,
14+
)
15+
from tests.utils import remove_os_env_temporarily, restore_os_env
16+
17+
18+
class TestCherryPy(helper.CPWebCase):
19+
helper.CPWebCase.interactive = False
20+
signing_secret = "secret"
21+
signature_verifier = SignatureVerifier(signing_secret)
22+
23+
@classmethod
24+
def setup_server(cls):
25+
cls.old_os_env = remove_os_env_temporarily()
26+
setup_mock_web_api_server(cls)
27+
28+
signing_secret = "secret"
29+
valid_token = "xoxb-valid"
30+
mock_api_server_base_url = "http://localhost:8888"
31+
web_client = WebClient(token=valid_token, base_url=mock_api_server_base_url,)
32+
app = App(client=web_client, signing_secret=signing_secret,)
33+
34+
def event_handler():
35+
pass
36+
37+
def shortcut_handler(ack):
38+
ack()
39+
40+
def command_handler(ack):
41+
ack()
42+
43+
app.event("app_mention")(event_handler)
44+
app.shortcut("test-shortcut")(shortcut_handler)
45+
app.command("/hello-world")(command_handler)
46+
47+
handler = SlackRequestHandler(app)
48+
49+
class SlackApp(object):
50+
@cherrypy.expose
51+
@cherrypy.tools.slack_in()
52+
def events(self, **kwargs):
53+
return handler.handle()
54+
55+
cherrypy.tree.mount(SlackApp(), "/slack")
56+
57+
@classmethod
58+
def teardown_class(cls):
59+
cls.supervisor.stop()
60+
cleanup_mock_web_api_server(cls)
61+
restore_os_env(cls.old_os_env)
62+
63+
def generate_signature(self, body: str, timestamp: str):
64+
return self.signature_verifier.generate_signature(
65+
body=body, timestamp=timestamp,
66+
)
67+
68+
def build_headers(self, timestamp: str, body: str):
69+
return [
70+
("content-length", str(len(body))),
71+
("x-slack-signature", self.generate_signature(body, timestamp)),
72+
("x-slack-request-timestamp", timestamp),
73+
]
74+
75+
def test_events(self):
76+
payload = {
77+
"token": "verification_token",
78+
"team_id": "T111",
79+
"enterprise_id": "E111",
80+
"api_app_id": "A111",
81+
"event": {
82+
"client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63",
83+
"type": "app_mention",
84+
"text": "<@W111> Hi there!",
85+
"user": "W222",
86+
"ts": "1595926230.009600",
87+
"team": "T111",
88+
"channel": "C111",
89+
"event_ts": "1595926230.009600",
90+
},
91+
"type": "event_callback",
92+
"event_id": "Ev111",
93+
"event_time": 1595926230,
94+
"authed_users": ["W111"],
95+
}
96+
timestamp, body = str(int(time())), json.dumps(payload)
97+
cherrypy.request.process_request_body = True
98+
self.getPage(
99+
"/slack/events",
100+
method="POST",
101+
body=body,
102+
headers=self.build_headers(timestamp, body),
103+
)
104+
self.assertStatus("200 OK")
105+
self.assertBody("")
106+
107+
def test_shortcuts(self):
108+
payload = {
109+
"type": "shortcut",
110+
"token": "verification_token",
111+
"action_ts": "111.111",
112+
"team": {
113+
"id": "T111",
114+
"domain": "workspace-domain",
115+
"enterprise_id": "E111",
116+
"enterprise_name": "Org Name",
117+
},
118+
"user": {"id": "W111", "username": "primary-owner", "team_id": "T111"},
119+
"callback_id": "test-shortcut",
120+
"trigger_id": "111.111.xxxxxx",
121+
}
122+
123+
timestamp, body = str(int(time())), json.dumps(payload)
124+
cherrypy.request.process_request_body = True
125+
self.getPage(
126+
"/slack/events",
127+
method="POST",
128+
body=body,
129+
headers=self.build_headers(timestamp, body),
130+
)
131+
self.assertStatus("200 OK")
132+
self.assertBody("")
133+
134+
def test_commands(self):
135+
payload = (
136+
"token=verification_token"
137+
"&team_id=T111"
138+
"&team_domain=test-domain"
139+
"&channel_id=C111"
140+
"&channel_name=random"
141+
"&user_id=W111"
142+
"&user_name=primary-owner"
143+
"&command=%2Fhello-world"
144+
"&text=Hi"
145+
"&enterprise_id=E111"
146+
"&enterprise_name=Org+Name"
147+
"&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT111%2F111%2Fxxxxx"
148+
"&trigger_id=111.111.xxx"
149+
)
150+
timestamp, body = str(int(time())), json.dumps(payload)
151+
cherrypy.request.process_request_body = True
152+
self.getPage(
153+
"/slack/events",
154+
method="POST",
155+
body=body,
156+
headers=self.build_headers(timestamp, body),
157+
)
158+
self.assertStatus("200 OK")
159+
self.assertBody("")

0 commit comments

Comments
 (0)
X Tutup