Skip to content

Commit 4df6859

Browse files
committed
Refactor Slack interaction handling to a Celery task
1 parent 4185555 commit 4df6859

4 files changed

Lines changed: 121 additions & 109 deletions

File tree

‎.vscode/launch.json‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,22 @@
2323
"autoStartBrowser": false
2424
},
2525
{
26-
"name": "Migrate",
26+
"name": "Celery",
2727
"type": "debugpy",
2828
"request": "launch",
29-
"module": "migrate",
30-
"env": {},
29+
"module": "celery",
3130
"envFile": "${workspaceFolder}/.env",
32-
"args": [],
31+
"args": [
32+
"--app",
33+
"checkpoint.celery_app",
34+
"worker",
35+
"--loglevel",
36+
"DEBUG",
37+
"--pool",
38+
"solo"
39+
],
3340
"jinja": true,
3441
"autoStartBrowser": false
35-
}
42+
},
3643
]
37-
}
44+
}

‎checkpoint.py‎

Lines changed: 88 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from authlib.integrations.flask_client import OAuth
2222
from authlib.integrations.requests_client import OAuth2Session
2323

24+
from celery import Celery, Task, shared_task
25+
2426
from flask import Flask, g, render_template, request, session
2527
from flask.helpers import get_debug_flag, redirect, url_for
2628

@@ -77,6 +79,29 @@ def traces_sampler(sampling_context: Dict[str, Dict[str, str]]) -> bool:
7779
return request_uri != "/ping"
7880

7981

82+
def init_celery(flask: Flask) -> Celery:
83+
"""
84+
Initialize Celery
85+
"""
86+
87+
class FlaskTask(Task): # type: ignore # pylint: disable=abstract-method
88+
"""
89+
Extend default Task class to have Flask context available
90+
91+
https://flask.palletsprojects.com/en/stable/patterns/celery/
92+
"""
93+
94+
def __call__(self, *args, **kwargs): # type: ignore
95+
with flask.app_context():
96+
return self.run(*args, **kwargs)
97+
98+
new_celery_app = Celery("checkpoint", task_cls=FlaskTask)
99+
new_celery_app.config_from_object(flask.config, namespace="CELERY")
100+
new_celery_app.set_default()
101+
flask.extensions["celery"] = new_celery_app
102+
return new_celery_app # type: ignore
103+
104+
80105
sentry_sdk.init(
81106
debug=get_debug_flag(),
82107
integrations=[
@@ -95,6 +120,8 @@ def traces_sampler(sampling_context: Dict[str, Dict[str, str]]) -> bool:
95120
app = Flask(__name__)
96121
app.config.from_prefixed_env()
97122

123+
celery_app = init_celery(app)
124+
98125
oauth = OAuth(app) # type: ignore
99126
oauth.register( # type: ignore
100127
name="keycloak",
@@ -3561,35 +3588,12 @@ def handle_event_callback(body: Dict[str, Any]) -> Dict[str, str]:
35613588
return {"status": "ok"}
35623589

35633590

3564-
@app.post("/slack")
3565-
def handle_slack_event() -> Dict[str, str]:
3591+
@cache.memoize()
3592+
def slack_user_has_access(user_id: str) -> bool:
35663593
"""
3567-
Handle an event from Slack
3594+
Check if a Slack user has access to Checkpoint
35683595
"""
3569-
verifier = SignatureVerifier(app.config["SLACK_SIGNING_SECRET"])
3570-
3571-
if not verifier.is_valid_request(request.get_data(), request.headers): # type: ignore
3572-
raise Unauthorized("Slack signature verification failed")
3573-
3574-
if request.content_type is not None and "application/json" in request.content_type:
3575-
body = request.get_json()
3576-
if body is not None and body.get("type") == "url_verification":
3577-
return {"challenge": body["challenge"]}
3578-
3579-
if request.content_type is not None and "application/json" in request.content_type:
3580-
body = request.get_json()
3581-
if body is not None and body.get("type") == "event_callback":
3582-
return handle_event_callback(body)
3583-
3584-
payload = loads(request.form.get("payload")) # type: ignore
3585-
3586-
if payload.get("type") == "block_actions":
3587-
return {"status": "ok"}
3588-
3589-
if payload.get("type") != "message_action":
3590-
raise BadRequest("Unsupported payload type")
3591-
3592-
triggering_user_info = slack.users_info(user=payload["user"]["id"])
3596+
triggering_user_info = slack.users_info(user=user_id)
35933597

35943598
triggering_user_email: Union[str, None] = None
35953599

@@ -3598,69 +3602,21 @@ def handle_slack_event() -> Dict[str, str]:
35983602
triggering_user_email = triggering_user_profile.get("profile", {}).get("email")
35993603

36003604
if triggering_user_email is None:
3601-
slack.views_open(
3602-
trigger_id=payload["trigger_id"],
3603-
view={
3604-
"type": "modal",
3605-
"title": {"type": "plain_text", "text": "Checkpoint"},
3606-
"blocks": [
3607-
{
3608-
"type": "section",
3609-
"text": {
3610-
"type": "mrkdwn",
3611-
"text": "Could not determine your email address from Slack.",
3612-
},
3613-
}
3614-
],
3615-
},
3616-
)
3617-
return {"status": "ok"}
3605+
return False
36183606

36193607
triggering_user_results = search_by_email(
36203608
Address(addr_spec=triggering_user_email), with_gted=False, with_title_and_organization=False
36213609
)
36223610

36233611
if len(triggering_user_results["results"]) == 0:
3624-
slack.views_open(
3625-
trigger_id=payload["trigger_id"],
3626-
view={
3627-
"type": "modal",
3628-
"title": {"type": "plain_text", "text": "Checkpoint"},
3629-
"blocks": [
3630-
{
3631-
"type": "section",
3632-
"text": {
3633-
"type": "mrkdwn",
3634-
"text": "No Georgia Tech account found for your email address.",
3635-
},
3636-
}
3637-
],
3638-
},
3639-
)
3640-
return {"status": "ok"}
3612+
return False
36413613

36423614
keycloak_account = get_keycloak_account(
36433615
triggering_user_results["results"][0]["directoryId"], is_frontend_request=False
36443616
)
36453617

36463618
if keycloak_account is None or "id" not in keycloak_account or keycloak_account["id"] is None:
3647-
slack.views_open(
3648-
trigger_id=payload["trigger_id"],
3649-
view={
3650-
"type": "modal",
3651-
"title": {"type": "plain_text", "text": "Checkpoint"},
3652-
"blocks": [
3653-
{
3654-
"type": "section",
3655-
"text": {
3656-
"type": "mrkdwn",
3657-
"text": "Could not find your Keycloak account.",
3658-
},
3659-
}
3660-
],
3661-
},
3662-
)
3663-
return {"status": "ok"}
3619+
return False
36643620

36653621
keycloak_user_id = keycloak_account["id"]
36663622

@@ -3700,10 +3656,17 @@ def handle_slack_event() -> Dict[str, str]:
37003656
"mappings"
37013657
]:
37023658
if mapping.get("name") == "access":
3703-
has_access = True
3704-
break
3659+
return True
37053660

3706-
if not has_access:
3661+
return False
3662+
3663+
3664+
@shared_task
3665+
def handle_slack_search_request(payload: Dict[str, Any]) -> None:
3666+
"""
3667+
Handle a Slack interaction event
3668+
"""
3669+
if slack_user_has_access(payload["user"]["id"]) is False:
37073670
slack.views_open(
37083671
trigger_id=payload["trigger_id"],
37093672
view={
@@ -3720,7 +3683,6 @@ def handle_slack_event() -> Dict[str, str]:
37203683
],
37213684
},
37223685
)
3723-
return {"status": "ok"}
37243686

37253687
mentioned_users = re.findall(r"<@(U[A-Z0-9]+)>", payload["message"].get("text", ""))
37263688

@@ -3751,7 +3713,6 @@ def handle_slack_event() -> Dict[str, str]:
37513713
],
37523714
},
37533715
)
3754-
return {"status": "ok"}
37553716

37563717
lookup_results = search_by_email(Address(addr_spec=lookup_email))
37573718

@@ -3764,9 +3725,53 @@ def handle_slack_event() -> Dict[str, str]:
37643725
},
37653726
)
37663727

3728+
3729+
@app.post("/slack/interaction")
3730+
def handle_slack_interaction() -> Dict[str, str]:
3731+
"""
3732+
Handle a Slack interaction event
3733+
"""
3734+
verifier = SignatureVerifier(app.config["SLACK_SIGNING_SECRET"])
3735+
3736+
if not verifier.is_valid_request(request.get_data(), request.headers): # type: ignore
3737+
raise Unauthorized("Slack signature verification failed")
3738+
3739+
payload = loads(request.form.get("payload")) # type: ignore
3740+
3741+
if payload.get("type") == "block_actions":
3742+
return {"status": "ok"}
3743+
3744+
if payload.get("type") != "message_action":
3745+
raise BadRequest("Unsupported payload type")
3746+
3747+
handle_slack_search_request.delay(payload)
3748+
37673749
return {"status": "ok"}
37683750

37693751

3752+
@app.post("/slack")
3753+
def handle_slack_event() -> Dict[str, str]:
3754+
"""
3755+
Handle an event from Slack
3756+
"""
3757+
verifier = SignatureVerifier(app.config["SLACK_SIGNING_SECRET"])
3758+
3759+
if not verifier.is_valid_request(request.get_data(), request.headers): # type: ignore
3760+
raise Unauthorized("Slack signature verification failed")
3761+
3762+
if request.content_type is not None and "application/json" in request.content_type:
3763+
body = request.get_json()
3764+
if body is not None and body.get("type") == "url_verification":
3765+
return {"challenge": body["challenge"]}
3766+
3767+
if request.content_type is not None and "application/json" in request.content_type:
3768+
body = request.get_json()
3769+
if body is not None and body.get("type") == "event_callback":
3770+
return handle_event_callback(body)
3771+
3772+
raise BadRequest("Unsupported payload type")
3773+
3774+
37703775
@app.get("/ping")
37713776
def ping() -> Dict[str, str]:
37723777
"""

‎poetry.lock‎

Lines changed: 17 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = [
77
]
88
license = {text = "MIT"}
99
readme = "README.md"
10-
requires-python = ">=3.12"
10+
requires-python = "^3.12"
1111
dependencies = [
1212
"flask (>=3.1.2,<4.0.0)",
1313
"authlib (>=1.6.5,<2.0.0)",
@@ -36,7 +36,8 @@ dev = [
3636
"pylint (>=4.0.3,<5.0.0)",
3737
"types-requests (>=2.32.4.20250913,<3.0.0.0)",
3838
"types-ldap3 (>=2.9.13.20251121,<3.0.0.0)",
39-
"types-authlib (>=1.6.5.20251005,<2.0.0.0)"
39+
"types-authlib (>=1.6.5.20251005,<2.0.0.0)",
40+
"celery-types (>=0.26.0,<0.27.0)"
4041
]
4142

4243

0 commit comments

Comments
 (0)