Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a0c5be5
tests: Slack Coverage
WeatherGod3218 Apr 12, 2026
2fa0f2d
Merge pull request #92 from WeatherGod3218/weather-dev
WeatherGod3218 Apr 15, 2026
1e7b791
fix: Multiple slac messages
WeatherGod3218 Apr 18, 2026
4cf0a50
docs: formatting
WeatherGod3218 Apr 18, 2026
4f93fd6
Merge pull request #94 from WeatherGod3218/weather-dev
WeatherGod3218 Apr 18, 2026
fd41caf
docs: Updated README.md
WeatherGod3218 Apr 20, 2026
c538bdb
Merge pull request #95 from WeatherGod3218/weather-dev
WeatherGod3218 Apr 20, 2026
c6d1e31
fix: Slack immediately responds
WeatherGod3218 Apr 22, 2026
b13c289
Merge pull request #96 from WeatherGod3218/weather-dev
WeatherGod3218 Apr 22, 2026
1b4342c
feat: Caches Slack Events
WeatherGod3218 May 2, 2026
8ce1608
Merge pull request #97 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
ddde2c0
fix: await await await await await await await await await
WeatherGod3218 May 2, 2026
2571390
Merge pull request #98 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
fe4b361
fix: Body argument in the wrong position
WeatherGod3218 May 2, 2026
45b8325
Merge pull request #99 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
59bb73a
fix: await squared
WeatherGod3218 May 2, 2026
ff72c8a
Merge pull request #100 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
59f702a
fix: actually return the correct thing
WeatherGod3218 May 2, 2026
8cc7fe9
fix: woops
WeatherGod3218 May 2, 2026
5b053a2
Merge pull request #101 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
2227abb
fix: Adds correct event has (im too tired)
WeatherGod3218 May 2, 2026
fd282d3
Merge pull request #102 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
296c794
fix: Slack lies to you, and it lied to me too
WeatherGod3218 May 2, 2026
9776bf1
Merge pull request #103 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
91499be
fix: Removes conflicting check
WeatherGod3218 May 2, 2026
638a94e
Merge pull request #107 from ComputerScienceHouse/weather-dev
WeatherGod3218 May 2, 2026
9dada2a
feat: Implements Slack Signing
WeatherGod3218 May 2, 2026
fabf23a
Merge pull request #108 from ComputerScienceHouse/slack-verification
WeatherGod3218 May 2, 2026
718756f
fix: Fixes load to loads
WeatherGod3218 May 2, 2026
4c07aab
Merge pull request #109 from ComputerScienceHouse/slack-verification
WeatherGod3218 May 2, 2026
55f4532
fix: Stops Form swallowing Stream
WeatherGod3218 May 2, 2026
09a5af0
Merge pull request #110 from ComputerScienceHouse/slack-verification
WeatherGod3218 May 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CALENDAR_TIMEZONE=
WATCHED_CHANNELS=
SLACK_API_TOKEN=
SLACK_JUMPSTART_MESSAGE=
SLACK_SIGNING_SECRET=

WIKI_API=
WIKIBOT_USER=
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Documentation for the project can be found be appended /docs to the url
All HTML requests that are sent in the project can be seen by appending /swag

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

## Installing
1. Clone and cd into the repo: git clone https://github.com/WeatherGod3218/jumpstartV2
Expand Down
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pre-commit==4.5.1
ruff==0.15.6
pytest==9.0.2
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ services:
- WATCHED_CHANNELS=${WATCHED_CHANNELS}
- SLACK_API_TOKEN=${SLACK_API_TOKEN}
- SLACK_JUMPSTART_MESSAGE=${SLACK_JUMPSTART_MESSAGE}

- SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET}

- WIKI_API=${WIKI_API}
- WIKIBOT_USER=${WIKIBOT_USER}
- WIKIBOT_PASSWORD=${WIKIBOT_PASSWORD}
Expand Down
1 change: 1 addition & 0 deletions docs/endpoints/csh_calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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


---

### Authentication
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use_directory_urls: false
nav:
- Home: index.md
- Getting Started: getting-started/getting-started.md
- Backend:
- Backend:
- Calendar: core/csh_calendar.md
- Slack: core/slack.md
- Wikithoughts: core/wikithoughts.md
Expand Down
102 changes: 25 additions & 77 deletions src/api/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
from logging import getLogger, Logger

import json
import httpx

from fastapi import APIRouter, Request, Form
from fastapi.responses import JSONResponse

from core import slack, wikithoughts, cshcalendar
from config import WATCHED_CHANNELS
import json
import urllib

logger: Logger = getLogger(__name__)
router: APIRouter = APIRouter()

ACCEPT_MESSAGE: str = "Posting right now :^)"
DENY_MESSAGE: str = "Okay :( maybe next time"


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

try:
logger.debug(f"Received Slack event: {await request.body()}")
raw_body: bytes = await request.body()

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

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

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

event: dict = body.get("event", {})
cleaned_text: str = slack.clean_text(event.get("text", ""))

if event.get("subtype", None) is not None:
logger.info("SLACK EVENT: Had no subtype, ignoring it")
return JSONResponse({"status": "ignored"})

if event.get("channel", None) not in WATCHED_CHANNELS:
logger.info(
"SLACK EVENT: Message was not in a Watched Channel, ignoring it"
)
return JSONResponse({"status": "ignored"})

logger.info("SLACK EVENT: Requesting upload via dm!")
await slack.request_upload_via_dm(event.get("user", ""), cleaned_text)
except Exception as e:
logger.error(f"Error handling Slack event: {e}")
return JSONResponse({"status": "error", "message": str(e)})

return JSONResponse({"status": "success"})
return JSONResponse(await slack.process_slack_events(body))


@router.post("/slack/message_actions")
async def message_actions(payload: str = Form(...)) -> JSONResponse:
async def message_actions(request: Request) -> JSONResponse:
"""
Handles slack message action.

Expand All @@ -111,48 +87,20 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse:
JSONResponse: A JSON response indicating the result of the action.
"""

try:
form_json: dict = json.loads(payload)
response_url = form_json.get("response_url")

if form_json.get("type") != "block_actions":
return JSONResponse({}, status_code=200)

if slack.convert_user_response_to_bool(form_json):
logger.info(
"User approved the announcement, Adding it to the announcement list!"
)

message_object: dict[str, dict] = json.loads(
form_json.get("actions", [{}])[0].get("value", '{text:""}')
).get("text", None)

user_id = form_json.get("user", {}).get("id")

username: str = await slack.get_username(user_id=user_id)
username = username[:40]

slack.add_announcement(message_object, username)

if response_url:
async with httpx.AsyncClient() as client:
await client.post(
response_url,
json={"text": ACCEPT_MESSAGE, "replace_original": True},
)
else:
if response_url:
async with httpx.AsyncClient() as client:
await client.post(
response_url,
json={"text": DENY_MESSAGE, "replace_original": True},
)
raw_body: bytes = await request.body()

except Exception as e:
logger.error(f"Error in message_actions: {e}")
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
if not (slack.is_valid_slack_request(request, raw_body)):
logger.warning(f"Received a Fake Slack Message Action! {raw_body}")
return JSONResponse({"error": "Invalid signature"}, status_code=403)

form_data = urllib.parse.parse_qs(raw_body.decode("utf-8"))
payload = form_data.get("payload", [None])[0]

if payload is None:
return JSONResponse({"error": "Missing payload"}, status_code=400)

return JSONResponse({"status": "success"}, status_code=200)
response_dict, status_code = await slack.process_slack_message_actions(payload)
return JSONResponse(response_dict, status_code=status_code)


@router.get("/wikithought")
Expand Down
2 changes: 2 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None:

SLACK_API_TOKEN: str | None = _get_env_variable("SLACK_API_TOKEN", None)
SLACK_JUMPSTART_MESSAGE: str = "Would you like to post this message to Jumpstart?"
SLACK_SIGNING_SECRET: str = _get_env_variable("SLACK_SIGNING_SECRET", None)

WATCHED_CHANNELS: tuple[str] = tuple(
_get_env_variable("WATCHED_CHANNELS", "").split(",")
)
Expand Down
15 changes: 10 additions & 5 deletions src/core/cshcalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import recurring_ical_events
import arrow
import re
import asyncio

from modules import taskmanager

from config import (
CALENDAR_CACHE_REFRESH,
Expand All @@ -15,7 +18,6 @@
CALENDAR_TIMEZONE,
CALENDAR_URL,
)
import asyncio

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

return TIME_PATTERN.sub(repl, unformatted_string)


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

if not events:
return {"data": [{"header": ":(", "content": "No Events on the Calendar"}]}
return [{"header": ":(", "content": "No Events on the Calendar"}]

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

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

if cal_correct_length:
logger.info("Calendar cache is full length, rebuilding async!")
async with asyncio.TaskGroup() as taskGroup:
taskGroup.create_task(rebuild_calendar())
# Calendar is correct length, we can just run this in the background

taskmanager.create_background_task(
rebuild_calendar()
) # Calendar is correct length, we can just run this in the background

else:
logger.info("Calendar cache is NOT full length, yielding rebuild!")
await rebuild_calendar()
Expand Down
Loading
Loading