Skip to content

Commit 8014907

Browse files
fix: Caches slack events in order to prevent resends firing (#104)
* tests: Slack Coverage * fix: Multiple slac messages * docs: formatting * docs: Updated README.md * fix: Slack immediately responds * feat: Caches Slack Events * fix: await await await await await await await await await * fix: Body argument in the wrong position * fix: await squared * fix: actually return the correct thing * fix: woops * fix: Adds correct event has (im too tired) * fix: Slack lies to you, and it lied to me too * fix: Removes conflicting check * feat: Implements Slack Signing * fix: Fixes load to loads * fix: Stops Form swallowing Stream
1 parent 3514bef commit 8014907

14 files changed

Lines changed: 342 additions & 106 deletions

File tree

‎.env.template‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CALENDAR_TIMEZONE=
77
WATCHED_CHANNELS=
88
SLACK_API_TOKEN=
99
SLACK_JUMPSTART_MESSAGE=
10+
SLACK_SIGNING_SECRET=
1011

1112
WIKI_API=
1213
WIKIBOT_USER=

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Documentation for the project can be found be appended /docs to the url
1212
All HTML requests that are sent in the project can be seen by appending /swag
1313

1414
This project uses Python, [FastAPI](https://fastapi.tiangolo.com/), HTML/CSS, and Javascript.
15-
See it live [here](http://jumpstart-cubed.cs.house/)!
15+
See it live [here](https://jumpstart.csh.rit.edu)!
1616

1717
## Installing
1818
1. Clone and cd into the repo: git clone https://github.com/WeatherGod3218/jumpstartV2

‎dev-requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pre-commit==4.5.1
22
ruff==0.15.6
3+
pytest==9.0.2

‎docker-compose.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ services:
1515
- WATCHED_CHANNELS=${WATCHED_CHANNELS}
1616
- SLACK_API_TOKEN=${SLACK_API_TOKEN}
1717
- SLACK_JUMPSTART_MESSAGE=${SLACK_JUMPSTART_MESSAGE}
18-
18+
- SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET}
19+
1920
- WIKI_API=${WIKI_API}
2021
- WIKIBOT_USER=${WIKIBOT_USER}
2122
- WIKIBOT_PASSWORD=${WIKIBOT_PASSWORD}

‎docs/endpoints/csh_calendar.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
This API interacts with the CSH Google calendar to pull the number of events set in the .env file to display in the calendar widget on Jumpstart
66

7+
78
---
89

910
### Authentication

‎mkdocs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use_directory_urls: false
3838
nav:
3939
- Home: index.md
4040
- Getting Started: getting-started/getting-started.md
41-
- Backend:
41+
- Backend:
4242
- Calendar: core/csh_calendar.md
4343
- Slack: core/slack.md
4444
- Wikithoughts: core/wikithoughts.md

‎src/api/endpoints.py‎

Lines changed: 25 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
from logging import getLogger, Logger
22

3-
import json
4-
import httpx
5-
63
from fastapi import APIRouter, Request, Form
74
from fastapi.responses import JSONResponse
85

96
from core import slack, wikithoughts, cshcalendar
10-
from config import WATCHED_CHANNELS
7+
import json
8+
import urllib
119

1210
logger: Logger = getLogger(__name__)
1311
router: APIRouter = APIRouter()
1412

15-
ACCEPT_MESSAGE: str = "Posting right now :^)"
16-
DENY_MESSAGE: str = "Okay :( maybe next time"
17-
1813

1914
@router.get("/calendar")
2015
async def get_calendar() -> JSONResponse:
@@ -63,44 +58,25 @@ async def slack_events(request: Request) -> JSONResponse:
6358
JSONResponse: A JSON response indicating the result of the event handling.
6459
"""
6560

66-
try:
67-
logger.debug(f"Received Slack event: {await request.body()}")
61+
raw_body: bytes = await request.body()
6862

69-
body: dict = await request.json()
63+
if not (slack.is_valid_slack_request(request, raw_body)):
64+
logger.warning(f"Received a Fake Slack Event!: {raw_body}")
65+
return JSONResponse({"error": "Invalid signature"}, status_code=403)
7066

71-
if request.headers.get("content-type") == "application/json":
72-
if body.get("type") == "url_verification":
73-
logger.info("SLACK EVENT: Was a challenge!")
74-
return JSONResponse({"challenge": body.get("challenge")})
67+
body: dict = json.loads(raw_body)
7568

76-
if not body:
77-
logger.debug("SLACK EVENT: Was a challenge, with no body")
69+
# Challenge from Bot Authentication
70+
if request.headers.get("content-type") == "application/json":
71+
if body.get("type") == "url_verification":
72+
logger.info("SLACK EVENT: Was a challenge!")
7873
return JSONResponse({"challenge": body.get("challenge")})
7974

80-
event: dict = body.get("event", {})
81-
cleaned_text: str = slack.clean_text(event.get("text", ""))
82-
83-
if event.get("subtype", None) is not None:
84-
logger.info("SLACK EVENT: Had no subtype, ignoring it")
85-
return JSONResponse({"status": "ignored"})
86-
87-
if event.get("channel", None) not in WATCHED_CHANNELS:
88-
logger.info(
89-
"SLACK EVENT: Message was not in a Watched Channel, ignoring it"
90-
)
91-
return JSONResponse({"status": "ignored"})
92-
93-
logger.info("SLACK EVENT: Requesting upload via dm!")
94-
await slack.request_upload_via_dm(event.get("user", ""), cleaned_text)
95-
except Exception as e:
96-
logger.error(f"Error handling Slack event: {e}")
97-
return JSONResponse({"status": "error", "message": str(e)})
98-
99-
return JSONResponse({"status": "success"})
75+
return JSONResponse(await slack.process_slack_events(body))
10076

10177

10278
@router.post("/slack/message_actions")
103-
async def message_actions(payload: str = Form(...)) -> JSONResponse:
79+
async def message_actions(request: Request) -> JSONResponse:
10480
"""
10581
Handles slack message action.
10682
@@ -111,48 +87,20 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse:
11187
JSONResponse: A JSON response indicating the result of the action.
11288
"""
11389

114-
try:
115-
form_json: dict = json.loads(payload)
116-
response_url = form_json.get("response_url")
117-
118-
if form_json.get("type") != "block_actions":
119-
return JSONResponse({}, status_code=200)
120-
121-
if slack.convert_user_response_to_bool(form_json):
122-
logger.info(
123-
"User approved the announcement, Adding it to the announcement list!"
124-
)
125-
126-
message_object: dict[str, dict] = json.loads(
127-
form_json.get("actions", [{}])[0].get("value", '{text:""}')
128-
).get("text", None)
129-
130-
user_id = form_json.get("user", {}).get("id")
131-
132-
username: str = await slack.get_username(user_id=user_id)
133-
username = username[:40]
134-
135-
slack.add_announcement(message_object, username)
136-
137-
if response_url:
138-
async with httpx.AsyncClient() as client:
139-
await client.post(
140-
response_url,
141-
json={"text": ACCEPT_MESSAGE, "replace_original": True},
142-
)
143-
else:
144-
if response_url:
145-
async with httpx.AsyncClient() as client:
146-
await client.post(
147-
response_url,
148-
json={"text": DENY_MESSAGE, "replace_original": True},
149-
)
90+
raw_body: bytes = await request.body()
15091

151-
except Exception as e:
152-
logger.error(f"Error in message_actions: {e}")
153-
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
92+
if not (slack.is_valid_slack_request(request, raw_body)):
93+
logger.warning(f"Received a Fake Slack Message Action! {raw_body}")
94+
return JSONResponse({"error": "Invalid signature"}, status_code=403)
95+
96+
form_data = urllib.parse.parse_qs(raw_body.decode("utf-8"))
97+
payload = form_data.get("payload", [None])[0]
98+
99+
if payload is None:
100+
return JSONResponse({"error": "Missing payload"}, status_code=400)
154101

155-
return JSONResponse({"status": "success"}, status_code=200)
102+
response_dict, status_code = await slack.process_slack_message_actions(payload)
103+
return JSONResponse(response_dict, status_code=status_code)
156104

157105

158106
@router.get("/wikithought")

‎src/config.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None:
3939

4040
SLACK_API_TOKEN: str | None = _get_env_variable("SLACK_API_TOKEN", None)
4141
SLACK_JUMPSTART_MESSAGE: str = "Would you like to post this message to Jumpstart?"
42+
SLACK_SIGNING_SECRET: str = _get_env_variable("SLACK_SIGNING_SECRET", None)
43+
4244
WATCHED_CHANNELS: tuple[str] = tuple(
4345
_get_env_variable("WATCHED_CHANNELS", "").split(",")
4446
)

‎src/core/cshcalendar.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import recurring_ical_events
88
import arrow
99
import re
10+
import asyncio
11+
12+
from modules import taskmanager
1013

1114
from config import (
1215
CALENDAR_CACHE_REFRESH,
@@ -15,7 +18,6 @@
1518
CALENDAR_TIMEZONE,
1619
CALENDAR_URL,
1720
)
18-
import asyncio
1921

2022
calendar_cache: list[CalendarInfo] = [] # The current cache of the calendar
2123
cal_last_update: date | None = (
@@ -136,6 +138,7 @@ def repl(match: re.Match[str]) -> str:
136138

137139
return TIME_PATTERN.sub(repl, unformatted_string)
138140

141+
139142
def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]:
140143
"""
141144
Formats a parsed list of CalendarInfos, and returns the HTML required for front end
@@ -150,7 +153,7 @@ def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]:
150153
current_date: date = datetime.now(ZoneInfo(CALENDAR_TIMEZONE))
151154

152155
if not events:
153-
return {"data": [{"header": ":(", "content": "No Events on the Calendar"}]}
156+
return [{"header": ":(", "content": "No Events on the Calendar"}]
154157

155158
formatted_list: list[dict[str, str]] = []
156159

@@ -291,9 +294,11 @@ async def get_future_events() -> list[CalendarInfo]:
291294

292295
if cal_correct_length:
293296
logger.info("Calendar cache is full length, rebuilding async!")
294-
async with asyncio.TaskGroup() as taskGroup:
295-
taskGroup.create_task(rebuild_calendar())
296-
# Calendar is correct length, we can just run this in the background
297+
298+
taskmanager.create_background_task(
299+
rebuild_calendar()
300+
) # Calendar is correct length, we can just run this in the background
301+
297302
else:
298303
logger.info("Calendar cache is NOT full length, yielding rebuild!")
299304
await rebuild_calendar()

0 commit comments

Comments
 (0)