Skip to content

Commit decea76

Browse files
authored
feat: Convert to Flask server (BURG3R5#149)
fixes BURG3R5#148
2 parents 0c43766 + 4b97ca0 commit decea76

File tree

11 files changed

+61
-74
lines changed

11 files changed

+61
-74
lines changed

bot/github/authenticator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import requests
66
import sentry_sdk
7-
from bottle import redirect
7+
from flask import redirect
88

99
from .base import GitHubBase
1010

@@ -35,7 +35,7 @@ def redirect_to_oauth_flow(self, state: str):
3535
f"https://redirect.mdgspace.org/{self.base_url}"
3636
f"/github/auth/redirect",
3737
}
38-
redirect(endpoint + "?" + urllib.parse.urlencode(params))
38+
return redirect(endpoint + "?" + urllib.parse.urlencode(params))
3939

4040
def set_up_webhooks(self, code: str, state: str) -> str:
4141
repository = json.loads(state).get("repository")

bot/github/parser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import Type
1111

1212
import sentry_sdk
13-
from bottle import LocalRequest
13+
from flask.wrappers import Request
1414

1515
from ..models.github import Commit, EventType, Issue, PullRequest, Ref, Repository, User
1616
from ..models.github.event import GitHubEvent
@@ -69,7 +69,7 @@ def parse(self, event_type, raw_json) -> GitHubEvent | None:
6969

7070
return None
7171

72-
def verify(self, request: LocalRequest) -> tuple[bool, str]:
72+
def verify(self, request: Request) -> tuple[bool, str]:
7373
"""
7474
Verifies incoming GitHub event.
7575
@@ -91,7 +91,7 @@ def verify(self, request: LocalRequest) -> tuple[bool, str]:
9191
expected_digest = headers["X-Hub-Signature-256"].split('=', 1)[-1]
9292
digest = hmac.new(
9393
secret.encode(),
94-
request.body.getvalue(),
94+
request.get_data(),
9595
hashlib.sha256,
9696
).hexdigest()
9797
is_valid = hmac.compare_digest(expected_digest, digest)

bot/slack/runner.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import hmac
66
import time
77
import urllib.parse
8-
from io import BytesIO
98
from json import dumps as json_dumps
109
from typing import Any
1110

12-
from bottle import MultiDict, WSGIHeaderDict
1311
from sentry_sdk import capture_message
1412
from slack.errors import SlackApiError
13+
from werkzeug.datastructures import Headers, ImmutableMultiDict
1514

1615
from ..models.github import EventType, convert_keywords_to_events
1716
from ..utils.json import JSON
@@ -44,8 +43,8 @@ def __init__(
4443

4544
def verify(
4645
self,
47-
body: BytesIO,
48-
headers: WSGIHeaderDict,
46+
body: bytes,
47+
headers: Headers,
4948
) -> tuple[bool, str]:
5049
"""
5150
Checks validity of incoming Slack request.
@@ -66,7 +65,7 @@ def verify(
6665
return False, "Request is too old"
6766

6867
expected_digest = headers["X-Slack-Signature"].split('=', 1)[-1]
69-
sig_basestring = ('v0:' + timestamp + ':').encode() + body.getvalue()
68+
sig_basestring = ('v0:' + timestamp + ':').encode() + body
7069
digest = hmac.new(self.secret, sig_basestring,
7170
hashlib.sha256).hexdigest()
7271
is_valid = hmac.compare_digest(expected_digest, digest)
@@ -76,7 +75,7 @@ def verify(
7675

7776
return True, "Request is secure and valid"
7877

79-
def run(self, raw_json: MultiDict) -> dict[str, Any] | None:
78+
def run(self, raw_json: ImmutableMultiDict) -> dict[str, Any] | None:
8079
"""
8180
Runs Slack slash commands sent to the bot.
8281
:param raw_json: Slash command data sent by Slack.

bot/utils/json.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from typing import Any
66

7-
from bottle import MultiDict
7+
from werkzeug.datastructures import ImmutableMultiDict
88

99

1010
class JSON:
@@ -40,10 +40,10 @@ def get(k):
4040
return keys[0].upper()
4141

4242
@staticmethod
43-
def from_multi_dict(multi_dict: MultiDict):
43+
def from_multi_dict(multi_dict: ImmutableMultiDict):
4444
"""
45-
Converts `bottle.MultiDict` to `JSON`.
46-
:param multi_dict: Incoming `MultiDict`.
47-
:return: `JSON` object containing the data from the `MultiDict`.
45+
Converts `werkzeug.datastructures.ImmutableMultiDict` to `JSON`.
46+
:param multi_dict: Incoming `ImmutableMultiDict`.
47+
:return: `JSON` object containing the data from the `ImmutableMultiDict`.
4848
"""
4949
return JSON({key: multi_dict[key] for key in multi_dict.keys()})

bot/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def test_get():
2+
"""
3+
First test endpoint.
4+
:return: Plaintext confirming server status.
5+
"""
6+
return "This server is running!"

main.py

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Execution entrypoint for the project.
33
4-
Sets up a `bottle` server with three endpoints: "/", "/github/events" and "/slack/commands".
4+
Sets up a `Flask` server with three endpoints: "/", "/github/events" and "/slack/commands".
55
66
"/" is used for testing and status checks.
77
@@ -17,45 +17,23 @@
1717
from typing import Any, Optional, Union
1818

1919
import sentry_sdk
20-
from bottle import get, post, request
21-
from bottle import response as http_response
22-
from bottle import run
2320
from dotenv import load_dotenv
24-
from sentry_sdk.integrations.bottle import BottleIntegration
21+
from flask import Flask, make_response, request
22+
from sentry_sdk.integrations.flask import FlaskIntegration
2523

24+
from bot import views
2625
from bot.github import GitHubApp
2726
from bot.models.github.event import GitHubEvent
2827
from bot.slack import SlackBot
2928
from bot.slack.templates import error_message
3029
from bot.utils.log import Logger
3130

31+
app = Flask(__name__)
3232

33-
@get("/")
34-
def test_get() -> str:
35-
"""
36-
First test endpoint.
37-
:return: Plaintext confirming server status.
38-
"""
39-
return "This server is running!"
33+
app.add_url_rule("/", view_func=views.test_get)
4034

4135

42-
@post("/")
43-
def test_post() -> str:
44-
"""
45-
Second test endpoint.
46-
:return: Status confirmation plaintext containing name supplied in request body.
47-
"""
48-
try:
49-
name: str = request.json["name"]
50-
except KeyError:
51-
name: str = "(empty JSON)"
52-
except TypeError:
53-
name: str = "(invalid JSON)"
54-
return (f"This server is working, and to prove it to you, "
55-
f"I'll guess your name!\nYour name is... {name}!")
56-
57-
58-
@post("/github/events")
36+
@app.route("/github/events", methods=['POST'])
5937
def manage_github_events():
6038
"""
6139
Uses `GitHubApp` to verify, parse and cast the payload into a `GitHubEvent`.
@@ -64,8 +42,7 @@ def manage_github_events():
6442

6543
is_valid_request, message = github_app.verify(request)
6644
if not is_valid_request:
67-
http_response.status = "400 Bad Request"
68-
return message
45+
return make_response(message, 400)
6946

7047
event: Optional[GitHubEvent] = github_app.parse(
7148
event_type=request.headers["X-GitHub-Event"],
@@ -74,9 +51,12 @@ def manage_github_events():
7451

7552
if event is not None:
7653
slack_bot.inform(event)
54+
return "Informed appropriate channels"
55+
56+
return "Unrecognized Event"
7757

7858

79-
@post("/slack/commands")
59+
@app.route("/slack/commands", methods=['POST'])
8060
def manage_slack_commands() -> Union[dict, str, None]:
8161
"""
8262
Uses a `SlackBot` instance to run the slash command triggered by the user.
@@ -85,41 +65,40 @@ def manage_slack_commands() -> Union[dict, str, None]:
8565
"""
8666

8767
is_valid_request, message = slack_bot.verify(
88-
body=request.body,
68+
body=request.get_data(),
8969
headers=request.headers,
9070
)
9171
if not is_valid_request:
9272
return error_message(f"⚠️ Couldn't fulfill your request: {message}")
9373

9474
# Unlike GitHub webhooks, Slack does not send the data in `requests.json`.
95-
# Instead, the data is passed in `request.forms`.
96-
response: dict[str, Any] | None = slack_bot.run(raw_json=request.forms)
75+
# Instead, the data is passed in `request.form`.
76+
response: dict[str, Any] | None = slack_bot.run(raw_json=request.form)
9777
return response
9878

9979

100-
@get("/github/auth")
80+
@app.route("/github/auth")
10181
def initiate_auth():
102-
github_app.redirect_to_oauth_flow(request.params.get("state"))
82+
return github_app.redirect_to_oauth_flow(request.args.get("state"))
10383

10484

105-
@get("/github/auth/redirect")
85+
@app.route("/github/auth/redirect")
10686
def complete_auth():
10787
return github_app.set_up_webhooks(
108-
code=request.query.get("code"),
109-
state=request.params.get("state"),
88+
code=request.args.get("code"),
89+
state=request.args.get("state"),
11090
)
11191

11292

11393
if __name__ == "__main__":
11494
load_dotenv(Path(".") / ".env")
11595

116-
debug = os.environ["DEBUG"] == "1"
117-
port = int(os.environ.get("CONTAINER_PORT", 5000))
96+
debug = os.environ["FLASK_DEBUG"] == "True"
11897

11998
if (not debug) and ("SENTRY_DSN" in os.environ):
12099
sentry_sdk.init(
121100
dsn=os.environ["SENTRY_DSN"],
122-
integrations=[BottleIntegration()],
101+
integrations=[FlaskIntegration()],
123102
)
124103

125104
slack_bot = SlackBot(
@@ -136,4 +115,4 @@ def complete_auth():
136115
client_secret=os.environ["GITHUB_APP_CLIENT_SECRET"],
137116
)
138117

139-
run(host="", port=port, debug=debug)
118+
app.run(port=int(os.environ.get("CONTAINER_PORT", 5000)))

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
bottle==0.12.23
1+
Flask==2.2.2
22
peewee==3.15.4
33
python-dotenv==0.21.0
44
requests~=2.28.1
5-
sentry-sdk[bottle]==1.10.1
5+
sentry-sdk[flask]==1.12.1
66
slackclient==2.9.4
7+
Werkzeug~=2.2.2

samples/.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
BASE_URL=subdomain.domain.tld/path1/path2
22
CONTAINER_PORT=5000
3-
DEBUG=1
3+
FLASK_APP=main
4+
FLASK_DEBUG=True
45
GITHUB_APP_CLIENT_ID=0123456789abcdefghij
56
GITHUB_APP_CLIENT_SECRET=e2fbe2fbe2fbe2fbe2fbe2e2fbe2fbe2e2fbe2fb
67
GITHUB_WEBHOOK_SECRET=3dbd2c253813c65b296b7acf67470b7e7bc116e3

samples/.env.dev

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
SLACK_APP_ID=A0101010101
22
BASE_URL=blah-111-22-333-444.ngrok.io
33
CONTAINER_PORT=5000
4-
DEBUG=1
4+
FLASK_APP=main
5+
FLASK_DEBUG=True
56
GITHUB_APP_CLIENT_ID=0123456789abcdefghij
67
GITHUB_APP_CLIENT_SECRET=e2fbe2fbe2fbe2fbe2fbe2e2fbe2fbe2e2fbe2fb
78
GITHUB_WEBHOOK_SECRET=3dbd2c253813c65b296b7acf67470b7e7bc116e3

tests/slack/test_runner.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from unittest import skip
33
from unittest.mock import patch
44

5-
from bottle import MultiDict
5+
from werkzeug.datastructures import ImmutableMultiDict
66

77
from bot.models.github import convert_keywords_to_events
88
from bot.models.slack import Channel
@@ -35,7 +35,7 @@ def setUp(self):
3535
self.runner.storage = MockSubscriptionStorage()
3636

3737
def test_run_calls_subscribe(self):
38-
raw_json = MultiDict(self.data["run|calls_subscribe"][0])
38+
raw_json = ImmutableMultiDict(self.data["run|calls_subscribe"][0])
3939

4040
with patch.object(self.logger, "log_command") as log_command:
4141
with patch.object(
@@ -53,7 +53,7 @@ def test_run_calls_subscribe(self):
5353

5454
@skip('This test is being skipped for the current PR')
5555
def test_run_calls_unsubscribe(self):
56-
raw_json = MultiDict(self.data["run|calls_unsubscribe"][0])
56+
raw_json = ImmutableMultiDict(self.data["run|calls_unsubscribe"][0])
5757
with patch.object(self.logger, "log_command") as mock_logger:
5858
with patch.object(self.runner,
5959
"run_unsubscribe_command") as mock_function:
@@ -67,7 +67,7 @@ def test_run_calls_unsubscribe(self):
6767

6868
@skip('This test is being skipped for the current PR')
6969
def test_run_calls_list(self):
70-
raw_json = MultiDict(self.data["run|calls_list"][0])
70+
raw_json = ImmutableMultiDict(self.data["run|calls_list"][0])
7171
with patch.object(self.logger, "log_command") as mock_logger:
7272
with patch.object(self.runner,
7373
"run_list_command") as mock_function:
@@ -78,7 +78,7 @@ def test_run_calls_list(self):
7878

7979
@skip('This test is being skipped for the current PR')
8080
def test_run_calls_help(self):
81-
raw_json = MultiDict(self.data["run|calls_help"][0])
81+
raw_json = ImmutableMultiDict(self.data["run|calls_help"][0])
8282
with patch.object(self.logger, "log_command") as mock_logger:
8383
with patch.object(self.runner,
8484
"run_help_command") as mock_function:
@@ -90,13 +90,13 @@ def test_run_calls_help(self):
9090
def test_run_doesnt_call(self):
9191
with patch.object(self.logger, "log_command") as mock_logger:
9292
# Wrong command
93-
raw_json = MultiDict(self.data["run|doesnt_call"][0])
93+
raw_json = ImmutableMultiDict(self.data["run|doesnt_call"][0])
9494
self.assertIsNone(self.runner.run(raw_json))
9595

9696
# No args for subscribe or unsubscribe
97-
raw_json = MultiDict(self.data["run|doesnt_call"][1])
97+
raw_json = ImmutableMultiDict(self.data["run|doesnt_call"][1])
9898
self.assertIsNone(self.runner.run(raw_json))
99-
raw_json = MultiDict(self.data["run|doesnt_call"][2])
99+
raw_json = ImmutableMultiDict(self.data["run|doesnt_call"][2])
100100
self.assertIsNone(self.runner.run(raw_json))
101101
mock_logger.assert_not_called()
102102

tests/utils/test_json.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22

3-
from bottle import MultiDict
3+
from werkzeug.datastructures import ImmutableMultiDict
44

55
from bot.utils.json import JSON
66

@@ -32,7 +32,7 @@ def test_getitem_multiple_not_found(self):
3232
self.assertEqual("NAME", json["name", "login"])
3333

3434
def test_from_multi_dict(self):
35-
multi_dict = MultiDict({
35+
multi_dict = ImmutableMultiDict({
3636
"name": "exampleuser",
3737
"login": "example_user"
3838
})

0 commit comments

Comments
 (0)