From 0ec65f2671de9a15a89bd674abe7b849d2191a3b Mon Sep 17 00:00:00 2001 From: Weather Date: Sat, 28 Mar 2026 03:13:44 -0400 Subject: [PATCH 01/48] feat: implements css themes and 50th start --- Dockerfile | 2 +- src/core/cshcalendar.py | 2 +- src/static/css/style.css | 91 ++++++++++++++++++++++++++++++---------- src/static/js/main.js | 58 +++++++++++++------------ src/templates/index.html | 4 +- 5 files changed, 105 insertions(+), 52 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36e221c..c1a8241 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ RUN addgroup -g 2000 jumpgroup && adduser -S -u 1001 -G jumpgroup jumpstart && \ USER jumpstart -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "/jumpstart/logging_config.yaml", "--proxy-headers","--forwarded-allow-ips","*"] +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "/jumpstart/logging_config.yaml", "--proxy-headers", "--forwarded-allow-ips", "*"] diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 3434ff8..dcaabc5 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -54,7 +54,7 @@ (WEEK - DAY): f"In %{DAY}% Days", } -BORDER_STRING: str = "
" +BORDER_STRING: str = '
' TIME_PATTERN = re.compile(r"%([^%]+)%") diff --git a/src/static/css/style.css b/src/static/css/style.css index 47b7d0a..3f70c71 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -1,3 +1,40 @@ +/* Creates a default theme, but can get overwritten */ +:root { + --panel-header-color: #B0197E; /* Standard color for panels header*/ + --panel-body-color: #FFFFFF; /* text body */ + --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ +} + +.theme-dark { + --panel-header-color: #B0197E; /* Standard color for panels header*/ + --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-color: #000000; /* text body */ + --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgba(255, 0, 170, 0.705); /* Used for the shadow in the calendar */ +} + +.theme-light { + --panel-header-color: #B0197E; /* Standard color for panels header*/ + --panel-body-color: #FFFFFF; /* text body */ + --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-text-color: #000000; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ +} + +.theme-golden { + --panel-header-color: #CC9318; /* Standard color for panels header*/ + --panel-header-text-color: #000000; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-color: #000000; /* text body */ + --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgba(206, 191, 149, 0.767); /* Used for the shadow in the calendar */ +} + body{ background-image: url('/static/img/darkmodeF.png'); overflow: hidden; @@ -10,14 +47,21 @@ body{ } .panel-primary{ - background: #34495e; - border: 8px #34495e solid ; + background: var(--border-color); + border: 8px var(--border-color) solid ; border-radius: 10px; } +.panel-heading{ + background-color: var(--panel-header-color) !important; +} + +.panel-body{ + background-color: var(--panel-body-color) !important; +} #top-bar{ - border: 5px #B0197E solid; - background-color: #34495e; + border: 5px var(--panel-header-color) solid; + background-color: var(--border-color); } #timewidget-fake{ @@ -25,19 +69,18 @@ body{ } .file-title{ - color: #fcf6fa; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 28px; - border-left: 5px #B0197E solid; + border-left: 5px var(--panel-header-color) solid; float: right; width: 29%; } .js-title{ - color: #fcf6fa; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 30px; - border-left: 5px #B0197E solid; float: right; } @@ -49,18 +92,18 @@ body{ .weatherwidget{ float: right; width: 49%; - border: 8px #34495e solid; + border: 8px var(--border-color) solid; border-radius: 10px; - background-color: #34495e; + background-color: var(--border-color); margin-bottom: 1.5%; } .datadog{ margin-top: 20px; overflow: auto; - border: 8px #34495e solid; + border: 8px var(--border-color) solid; border-radius: 10px; - background-color: #34495e; + background-color: var(--border-color); width: 760px; height: 580px; float: right; @@ -78,14 +121,14 @@ body{ } .announcements-text-header{ - color: white; + color: var(--panel-header-text-color); font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; } .announcements-text-body{ - color: black; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 18px; text-align: center; @@ -98,22 +141,22 @@ body{ } .wikithoughts-text-header{ - color: white; + color: var(--panel-header-text-color); font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; } .wikithoughts-text-body{ - color: black; + color:var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 18px; text-align: center; } .calendar-frame-lvl1{ - background-color: #fcf6fa; - border: 8px #34495e solid; + background-color: var(--panel-body-color); + border: 8px var(--border-color) solid; border-radius: 10px; margin-top: 20px; height: 950px; @@ -127,19 +170,25 @@ body{ } .calendar-text-date{ - color: #4c4c4c; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 20px; - text-shadow: 2px 2px 3px rgb(176,25,126, 0.5); + text-shadow: 2px 2px 3px var(--shadow-color); text-align: center; } .calendar-text{ - color: black; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 19px; } +.calendar-border{ + border: 0px solid; + border-top: 2px solid var(--panel-header-color); + margin: 10px 0; +} + @media screen and (max-width: 1899px) { .wikithoughts{ width: 49%; diff --git a/src/static/js/main.js b/src/static/js/main.js index 92f0f18..cc2f2e9 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -1,22 +1,45 @@ +let current_theme = ""; + +function resetAllThemes(){ + document.body.classList.forEach(cls => { + if (cls.startsWith('theme-')) { + document.body.classList.remove(cls); + }}); +} + +function setNewTheme(newTheme) { + if (newTheme === current_theme) { + return; + } + resetAllThemes(); + document.body.classList.toggle(newTheme); + current_theme = newTheme; +} + async function longUpdate() { const date = new Date(); const hour = date.getHours(); const month = date.getMonth() + 1; const day = date.getDate(); + const isDay = (hour > 9 && hour < 18); + let is_golden = true; let bgImage = "url(../static/img/darkmodeF.png)"; + if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { bgImage = "url(../static/img/jumpstartbang.png)"; + } else if (month === 4 && [10, 11, 12].includes(day)) { + is_golden = true; } else if (month === 10 && [29, 30, 31].includes(day)) { bgImage = "url(../static/img/spookymode.png)"; } else if (month === 11 && day === 2) { bgImage = "url(../static/img/duckymode2.png)"; } else if ([11, 12].includes(month)) { bgImage = "url(../static/img/wintermode.png)"; - } else if (hour > 9 && hour < 18) { + } else if (isDay) { bgImage = "url(../static/img/lightmodeF.png)"; } $("body").css("background-image", bgImage); @@ -26,34 +49,15 @@ async function longUpdate() { const data = await res.json(); $("#calendar").html(data.data); - const isDay = hour > 9 && hour < 18; - const panelBody = $(".panel-body"); - const plugBody = $(".plug-body"); - const wikiPages = $(".wikithoughts-text-body"); - const announcementsBody = $(".announcements-text-body"); - const calendarFrame = $(".calendar-frame-lvl1"); - const calendarTextDate = $(".calendar-text-date"); - const calendarText = $(".calendar-text"); - - if (isDay) { - panelBody.css("background-color", "white"); - plugBody.css("background-color", "white"); - wikiPages.css({ "background-color": "white", "color": "black" }); - announcementsBody.css("color", "black"); - calendarFrame.css("background-color", "white"); - calendarTextDate.css("color", "black"); - calendarText.css("color", "black"); - } else { - panelBody.css("background-color", "black"); - plugBody.css("background-color", "black"); - wikiPages.css({ "background-color": "black", "color": "white" }); - announcementsBody.css("color", "white"); - calendarFrame.css("background-color", "black"); - calendarTextDate.css("color", "white"); - calendarText.css("color", "white"); + if (is_golden){ + setNewTheme("theme-golden") + } else if (isDay) { + setNewTheme("theme-light") + } else{ + setNewTheme("theme-dark") } - $("#datadog").attr('src', ddog_dashboard + new Date().now()); + $("#datadog").attr('src', ddog_dashboard + Date().now()); } catch (err) { console.log(err); diff --git a/src/templates/index.html b/src/templates/index.html index 1c48aa3..676ff41 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -15,7 +15,7 @@
- +
@@ -37,7 +37,7 @@ data-font="Fira Sans" data-icons="Climacons Animated" data-days="100" - data-theme="kitty"> + data-theme="dark"> ROCHESTER WEATHER
- {% block head %}{% endblock %} diff --git a/src/templates/index.html b/src/templates/index.html index 676ff41..a2158f2 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -73,4 +73,5 @@
+ {% endblock %} \ No newline at end of file From 6f8f8bcb0f8b5b181db59318adfe435fd510cb68 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 30 Mar 2026 17:30:15 -0400 Subject: [PATCH 05/48] fix: 50th only being set for 50th --- src/static/js/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index 4b9bf6d..3ea0e45 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -75,7 +75,6 @@ async function longUpdate() { const isDay = (hour > 9 && hour < 18); let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); - is_golden = true; let bgImage = "url(../static/img/darkmodeF.png)"; if (month === 2 && [12, 13, 14].includes(day)) { From 7e972839ea6d8b575666d53f6c7fd1768f28cc85 Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 01:03:47 -0400 Subject: [PATCH 06/48] fix: Weather widget not updating properly --- mkdocs.yml | 6 +- src/api/endpoints.py | 20 ++++--- src/core/cshcalendar.py | 7 +-- src/core/wikithoughts.py | 121 +++++++++++++++++++++++---------------- src/main.py | 4 +- src/requirements.txt | 2 - src/static/css/style.css | 36 ++++++------ src/static/js/main.js | 49 ++++++++++------ 8 files changed, 141 insertions(+), 104 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index acafb5b..234a459 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,10 +38,10 @@ use_directory_urls: false nav: - Home: index.md - Getting Started: getting-started/getting-started.md - - Backend: - - Wikithoughts: core/Wiki-thoughts.md + - Backend: - Calendar: core/CSH Calendar.md - - Slack: core/Slack.md + - Slack Bot: core/Slack.md + - Wiki Thoughts: core/Wiki-thoughts.md - Endpoints: - Calendar: endpoints/calendar_endpoint.md - Announcements: endpoints/announcements.md diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 887d28d..28de1ea 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -14,6 +14,8 @@ logger: Logger = getLogger(__name__) router: APIRouter = APIRouter() +ACCEPT_MESSAGE: str = "Posting right now :^)" +DENY_MESSAGE: str = "Okay :( maybe next time" @router.get("/api/calendar") async def get_calendar() -> JSONResponse: @@ -121,16 +123,18 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: slack.add_announcement(message_object) if response_url: - await httpx.post( - response_url, - json={"text": "Posting right now :^)", "replace_original": True}, - ) + async with httpx.AsyncClient() as client: + await client.post( + response_url, + json={"text": ACCEPT_MESSAGE, "replace_original": True}, + ) else: if response_url: - await httpx.post( - response_url, - json={"text": "Okay :( maybe next time", "replace_original": True}, - ) + async with httpx.AsyncClient() as client: + await client.post( + response_url, + json={"text": DENY_MESSAGE, "replace_original": True}, + ) except Exception as e: logger.error(f"Error in message_actions: {e}") diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index dcaabc5..a9bb7d9 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -314,10 +314,9 @@ async def get_future_events() -> list[CalendarInfo]: if cal_correct_length: logger.info("Calendar cache is full length, rebuilding async!") - asyncio.create_task( - rebuild_calendar() - ) # Calendar is correct length, we can just run this in the background - # Make it a variable for GC purposes? idk sonarqube told me to do it + async with asyncio.TaskGroup() as taskGroup: + taskGroup.create_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() diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 4173fab..3856464 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -219,6 +219,64 @@ def process_category_page(r_json: dict[str, str]) -> tuple[list[str], bool | str return (titles, False) + +async def fetch_category_pages(response: httpx.Response) -> list[str]: + """ + Loops through and gets the list of every page for Jumpstart Curated + + Args: + response (httpx.Response): The response to be converted and searched through + + Returns: + list[str]: The list of titles to be fetched. + """ + + params: dict[str, str] = { + "action": "query", + "list": "categorymembers", + "cmtitle": f"Category:{WIKI_CATEGORY}", + "cmlimit": "500", + "format": "json", + } + + titles_found: list[str] = [] + + while True: + r_json: dict[str, str] = response.json() + + if "error" in r_json and r_json["error"].get("code") in ( + "readapidenied", + "notloggedin", + ): + if failed_authentication_attempts > REAUTHENTICATE_ATTEMPTS: + logger.warning( + "Reauthenticating the wikithought bot failed, sending empty response" + ) + return [] + + logger.info("Bot was unauthenticated, attempting to reauthenticate!") + await auth_bot() + if not (bot_authenticated): + logger.warning( + f"Failed to reauthenticate the bot! Attempt: {failed_authentication_attempts}" + ) + + failed_authentication_attempts += 1 + continue + + added, repeat_req = process_category_page(r_json) + titles_found += added + + if repeat_req not in (None, False, ""): + params["cmcontinue"] = repeat_req + + response = await client.get( + WIKI_API, params=params + ) + continue + break + return titles_found + async def refresh_category_pages() -> list[str]: """ Refreshes all pages of the category @@ -227,7 +285,8 @@ async def refresh_category_pages() -> list[str]: category (str): The name of the category to search through Returns: - list[str]: All the page titles found in this category, None if the bot is not authorized + list[str] + : All the page titles found in this category, None if the bot is not authorized """ global page_title_cache, last_updated_time, queued_pages, shown_pages time_now: datetime = datetime.now() @@ -245,54 +304,19 @@ async def refresh_category_pages() -> list[str]: } headers: dict[str, str] = headers_formatting() - # This needs to loop due to mediawiki limitations - - failed_authentication_attempts: int = 0 + response: httpx.Response = await client.get( + WIKI_API, params=params, headers=headers + ) - while True: - response: httpx.Response = await client.get( - WIKI_API, params=params, headers=headers - ) - - if response.status_code == 304: - last_updated_time = time_now - return page_title_cache - - elif response.status_code == 200: - headers_formatting(etag, last_modifed) - r_json: dict[str, str] = response.json() - - if "error" in r_json and r_json["error"].get("code") in ( - "readapidenied", - "notloggedin", - ): - if failed_authentication_attempts > REAUTHENTICATE_ATTEMPTS: - logger.warning( - "Reauthenticating the wikithought bot failed, sending empty response" - ) - return [] - - logger.info(f"Both was unauthenticated, attempting to reauthenticate!") - await auth_bot() - if not (bot_authenticated): - logger.warning( - "Failed to reauthenticate the bot! Attempt: " - + failed_authentication_attempts - ) - - failed_authentication_attempts += 1 - continue - - added, repeat_req = process_category_page(r_json) - titles += added - - if repeat_req not in (None, False, ""): - params["cmcontinue"] = repeat_req - continue - break - else: - logger.warning("Failed to update the CSH wiki page!") - return page_title_cache + if response.status_code == 304: + last_updated_time = time_now + return page_title_cache + + elif response.status_code == 200: + titles = await fetch_category_pages(response=response) + else: + logger.warning("Failed to update the CSH wiki page!") + return page_title_cache last_updated_time = time_now page_title_cache = titles @@ -335,7 +359,6 @@ async def refresh_page_dictionary() -> None: if "query" in r_json: for page in r_json["query"]["pages"].values(): wikitext = page["revisions"][0]["slots"]["main"]["*"] - cleaned_text = clean_wikitext(wikitext) # unfuck the text paragraphs = cleaned_text.split("\n\n") # Cut the first line diff --git a/src/main.py b/src/main.py index 43b7f13..cd14e90 100644 --- a/src/main.py +++ b/src/main.py @@ -27,8 +27,10 @@ @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Starting up the Jumpstart application!") - asyncio.create_task(cshcalendar.rebuild_calendar()) + async with asyncio.TaskGroup() as tg: + tg.create_task(cshcalendar.rebuild_calendar()) await wikithoughts.auth_bot() + yield logger.info("Shutting down the Jumpstart application!") await cshcalendar.close_client() diff --git a/src/requirements.txt b/src/requirements.txt index 0b1faf8..305684d 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -19,8 +19,6 @@ icalendar==7.0.3 recurring-ical-events==3.8.1 arrow==1.4.0 -# For Wikithoughts - # For the docker to run uvicorn==0.41.0 pyyaml==6.0.3 diff --git a/src/static/css/style.css b/src/static/css/style.css index ef5a5a5..750eb83 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -9,30 +9,30 @@ } .theme-dark { - --panel-header-color: #B0197E; /* Standard color for panels header*/ - --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ - --panel-body-color: #000000; /* text body */ - --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ - --border-color: #34495E; /* Used for borders around elements */ - --shadow-color: rgba(255, 0, 170, 0.705); /* Used for the shadow in the calendar */ + --panel-header-color: #B0197E; + --panel-header-text-color: #FFFFFF; + --panel-body-color: #000000; + --panel-body-text-color: #FFFFFF; + --border-color: #34495E; + --shadow-color: rgba(255, 0, 170, 0.705); } .theme-light { - --panel-header-color: #B0197E; /* Standard color for panels header*/ - --panel-body-color: #FFFFFF; /* text body */ - --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ - --panel-body-text-color: #000000; /* Used for the text within said panels */ - --border-color: #34495E; /* Used for borders around elements */ - --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ + --panel-header-color: #B0197E; + --panel-body-color: #FFFFFF; + --panel-header-text-color: #FFFFFF; + --panel-body-text-color: #000000; + --border-color: #34495E; + --shadow-color: rgb(176,25,126, 0.5); } .theme-golden { - --panel-header-color: #FFC64A; /* Standard color for panels header*/ - --panel-header-text-color: #000000; /* Used for headers in panels, like Page - csh/Wikithoughts */ - --panel-body-color: #000000; /* text body */ - --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ - --border-color: #34495E; /* Used for borders around elements */ - --shadow-color: rgba(206, 191, 149, 0.767); /* Used for the shadow in the calendar */ + --panel-header-color: #FFC64A; + --panel-header-text-color: #000000; + --panel-body-color: #000000; + --panel-body-text-color: #FFFFFF; + --border-color: #34495E; + --shadow-color: rgb(255, 212, 94); } body{ diff --git a/src/static/js/main.js b/src/static/js/main.js index 3ea0e45..6f22885 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -2,6 +2,7 @@ let currentPageTheme = ""; let currentDatadogTheme = ""; let currentWeatherTheme = ""; +let mode_enabled = false; /* The struct of all the different themes. - Page: The .css page theme to be loaded (make sure they start with theme-)! @@ -28,9 +29,7 @@ const allThemes = { function setDatadogTheme(newTheme) { - if (newTheme === currentDatadogTheme) { - return; - } + if (newTheme === currentDatadogTheme) return; currentDatadogTheme = newTheme; const iframe = document.getElementById("datadog"); @@ -40,23 +39,35 @@ function setDatadogTheme(newTheme) { } function setWeatherTheme(newTheme) { - if (newTheme === currentWeatherTheme) { - return; - } + if (newTheme === currentWeatherTheme) return; currentWeatherTheme = newTheme; - const widget = document.getElementById("weather-image"); - widget.setAttribute("data-theme", newTheme); - - if (globalThis.weatherWidget) { - widget.innerHTML = widget.innerHTML; - weatherWidget.init(); - } + + const oldWidget = document.getElementById("weather-image"); + + const newWidget = document.createElement("a"); + newWidget.className = "weatherwidget-io"; + newWidget.id = "weather-image"; + newWidget.href = "https://forecast7.com/en/43d16n77d61/rochester/?unit=us"; + + newWidget.setAttribute("data-label_1", "ROCHESTER"); + newWidget.setAttribute("data-label_2", "WEATHER"); + newWidget.setAttribute("data-font", "Fira Sans"); + newWidget.setAttribute("data-icons", "Climacons Animated"); + newWidget.setAttribute("data-days", "100"); + newWidget.setAttribute("data-theme", newTheme); + + newWidget.textContent = "ROCHESTER WEATHER"; + oldWidget.replaceWith(newWidget); + + setTimeout(() => { + if (globalThis.__weatherwidget_init) { + globalThis.__weatherwidget_init(); + } + }, 100); } function setNewPageTheme(newTheme) { - if (newTheme === currentPageTheme) { - return; - } + if (newTheme === currentPageTheme) return; currentPageTheme = newTheme; document.body.classList.forEach(cls => { @@ -77,6 +88,9 @@ async function longUpdate() { let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); let bgImage = "url(../static/img/darkmodeF.png)"; + is_golden = mode_enabled; + mode_enabled = !mode_enabled; + if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { @@ -111,9 +125,6 @@ async function longUpdate() { const res = await fetch('/api/calendar', { method: 'GET', mode: 'cors' }); const data = await res.json(); $("#calendar").html(data.data); - - $("#datadog").attr('src', ddog_dashboard + Date().now()); - } catch (err) { console.log(err); } From d05df1ef535eec6817a543b80bdf4057a2547f5a Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 01:19:27 -0400 Subject: [PATCH 07/48] fix: formatted --- src/api/endpoints.py | 1 + src/core/cshcalendar.py | 2 +- src/core/wikithoughts.py | 20 +++++++++----------- src/main.py | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 28de1ea..af56e1d 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -17,6 +17,7 @@ ACCEPT_MESSAGE: str = "Posting right now :^)" DENY_MESSAGE: str = "Okay :( maybe next time" + @router.get("/api/calendar") async def get_calendar() -> JSONResponse: """ diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index a9bb7d9..8b44d53 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -316,7 +316,7 @@ async def get_future_events() -> list[CalendarInfo]: 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 + # 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() diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 3856464..81a8c49 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -219,16 +219,15 @@ def process_category_page(r_json: dict[str, str]) -> tuple[list[str], bool | str return (titles, False) - async def fetch_category_pages(response: httpx.Response) -> list[str]: """ - Loops through and gets the list of every page for Jumpstart Curated - - Args: - response (httpx.Response): The response to be converted and searched through + Loops through and gets the list of every page for Jumpstart Curated + + Args: + response (httpx.Response): The response to be converted and searched through - Returns: - list[str]: The list of titles to be fetched. + Returns: + list[str]: The list of titles to be fetched. """ params: dict[str, str] = { @@ -270,13 +269,12 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: if repeat_req not in (None, False, ""): params["cmcontinue"] = repeat_req - response = await client.get( - WIKI_API, params=params - ) + response = await client.get(WIKI_API, params=params) continue break return titles_found + async def refresh_category_pages() -> list[str]: """ Refreshes all pages of the category @@ -311,7 +309,7 @@ async def refresh_category_pages() -> list[str]: if response.status_code == 304: last_updated_time = time_now return page_title_cache - + elif response.status_code == 200: titles = await fetch_category_pages(response=response) else: diff --git a/src/main.py b/src/main.py index cd14e90..43bc793 100644 --- a/src/main.py +++ b/src/main.py @@ -30,7 +30,7 @@ async def lifespan(app: FastAPI): async with asyncio.TaskGroup() as tg: tg.create_task(cshcalendar.rebuild_calendar()) await wikithoughts.auth_bot() - + yield logger.info("Shutting down the Jumpstart application!") await cshcalendar.close_client() From db0305192915b232168f38b5da270f9c21197fc6 Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Thu, 2 Apr 2026 02:12:37 -0400 Subject: [PATCH 08/48] Update keylime-dev and apply stash --- .github/workflows/sonarqube.yml | 2 +- .gitignore | 4 +- .pre-commit-config.yaml | 15 ++++++ .python-version | 2 +- README.md | 9 +++- dev-requirements.txt | 2 + docs/core/CSH Calendar.md | 2 +- docs/core/Slack.md | 2 +- docs/core/Wiki-thoughts.md | 2 +- docs/endpoints/announcements.md | 2 +- docs/endpoints/calendar_endpoint.md | 2 +- docs/endpoints/slack_bot.md | 2 +- docs/endpoints/wikithoughts.md | 2 +- docs/getting-started/getting-started.md | 6 +-- docs/index.md | 6 +-- docs/requirements.txt | 2 +- pytest.ini | 4 ++ ruff.toml | 2 +- sonar-project.properties | 2 +- src/__init__.py | 1 - src/core/slack.py | 1 - src/logging_config.yaml | 2 +- src/static/js/main.js | 20 ++++---- src/static/slack/dm_request_template.json | 2 +- src/templates/base.html | 2 +- src/templates/index.html | 18 +++---- tests/conftest.py | 44 +++++++++++++++++ tests/requirements.txt | 2 + tests/src/test_config.py | 58 +++++++++++++++++++++++ 29 files changed, 175 insertions(+), 45 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 dev-requirements.txt create mode 100644 pytest.ini create mode 100644 tests/conftest.py create mode 100644 tests/requirements.txt create mode 100644 tests/src/test_config.py diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 52e4245..7d77948 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -25,4 +25,4 @@ jobs: - uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} \ No newline at end of file + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/.gitignore b/.gitignore index cac8df1..4e1b3ca 100644 --- a/.gitignore +++ b/.gitignore @@ -185,9 +185,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..81e2bac --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.6 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json diff --git a/.python-version b/.python-version index 3767b4b..6324d40 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14 \ No newline at end of file +3.14 diff --git a/README.md b/README.md index 21ec2b2..d25e5a2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ All information displayed has been authorized to been shown. 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. +This project uses Python, [FastAPI](https://fastapi.tiangolo.com/), HTML/CSS, and Javascript. See it live [here](http://jumpstart-cubed.cs.house/)! ## Installing @@ -46,3 +46,10 @@ Jumpstart also has support for Docker Compose, a extended version of docker that docker compose up ``` +## Development +1. Install uv on your system if not already on it (this just makes it easy) +2. Run: `uv venv .venv --python 3.14` +3. Activate the virtual environment +4. Run: `uv pip install -r dev-requirements.txt`, `uv pip install -r src/requirements.txt`, `uv pip install -r tests/requirements,txt`, and `uv pip install -r docs/requirements.txt` +5. Run: `pre-commit install` +6. You're all set! diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..89303c8 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,2 @@ +pre-commit==4.5.1 +ruff==0.15.6 diff --git a/docs/core/CSH Calendar.md b/docs/core/CSH Calendar.md index 986b1d9..80eb328 100644 --- a/docs/core/CSH Calendar.md +++ b/docs/core/CSH Calendar.md @@ -11,4 +11,4 @@ During a fetch of the calendar, if any other clients attempt to connect, they wi --- ### Documentation Overview -::: core.cshcalendar \ No newline at end of file +::: core.cshcalendar diff --git a/docs/core/Slack.md b/docs/core/Slack.md index f2f2e20..3c238f8 100644 --- a/docs/core/Slack.md +++ b/docs/core/Slack.md @@ -5,4 +5,4 @@ This component handles the Slack Bot and it's related functions. Such as respond --- ### Documentation Overview -::: core.slack \ No newline at end of file +::: core.slack diff --git a/docs/core/Wiki-thoughts.md b/docs/core/Wiki-thoughts.md index 8842c2f..b38acf5 100644 --- a/docs/core/Wiki-thoughts.md +++ b/docs/core/Wiki-thoughts.md @@ -11,4 +11,4 @@ This component handles the fetching, caching and cycling of the CSH Wikithoughts --- ### Documentation Overview -::: core.wikithoughts \ No newline at end of file +::: core.wikithoughts diff --git a/docs/endpoints/announcements.md b/docs/endpoints/announcements.md index e37ec0d..c9c7e35 100644 --- a/docs/endpoints/announcements.md +++ b/docs/endpoints/announcements.md @@ -1,3 +1,3 @@ -::: api.endpoints.get_announcement \ No newline at end of file +::: api.endpoints.get_announcement diff --git a/docs/endpoints/calendar_endpoint.md b/docs/endpoints/calendar_endpoint.md index 34704cc..e9d8ff0 100644 --- a/docs/endpoints/calendar_endpoint.md +++ b/docs/endpoints/calendar_endpoint.md @@ -25,4 +25,4 @@ Example error response: ``` ### Endpoint Overview -::: api.endpoints.get_calendar \ No newline at end of file +::: api.endpoints.get_calendar diff --git a/docs/endpoints/slack_bot.md b/docs/endpoints/slack_bot.md index 5766bbc..063cbf9 100644 --- a/docs/endpoints/slack_bot.md +++ b/docs/endpoints/slack_bot.md @@ -1,4 +1,4 @@ ::: api.endpoints.slack_events -::: api.endpoints.message_actions \ No newline at end of file +::: api.endpoints.message_actions diff --git a/docs/endpoints/wikithoughts.md b/docs/endpoints/wikithoughts.md index fb48abd..f6f46b6 100644 --- a/docs/endpoints/wikithoughts.md +++ b/docs/endpoints/wikithoughts.md @@ -18,4 +18,4 @@ In the event of an Error, the API will log an error and display default text } ``` -::: api.endpoints.wikithought \ No newline at end of file +::: api.endpoints.wikithought diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index 4cd1770..0805d77 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -2,7 +2,7 @@ ## Installing 1. Clone and cd into the repo: git clone https://github.com/WeatherGod3218/jumpstartV2 >> Make another branch if you are working on a large PR -2. +2. ## Setup 1. Make sure you have docker installed @@ -10,7 +10,7 @@ 2. Copy the .env.template file, rename it to .env and place it in the root folder 3. Ask an RTP for jumpstart secrets, add them to the .env accordingly -## Run +## Run 1. Build the docker file ``` docker build -t Jumpstart . @@ -23,4 +23,4 @@ ### Alternatively, you can run the docker compose file as well ``` docker compose up -``` \ No newline at end of file +``` diff --git a/docs/index.md b/docs/index.md index e32af29..64481aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,10 +5,10 @@ ![Static Badge](https://img.shields.io/badge/%40gravy-made_by?style=flat-square&logo=github&labelColor=%230d1117&color=%23E11C52&link=https%3A%2F%2Fgithub.com%2FNikolaiStrong) -A graphical interface that displays information in the elevator lobby of Computer Science House. +A graphical interface that displays information in the elevator lobby of Computer Science House. All information displayed has been authorized to been shown. -This project uses Python, [FastAPI](https://fastapi.tiangolo.com/), HTML/CSS, and Javascript. +This project uses Python, [FastAPI](https://fastapi.tiangolo.com/), HTML/CSS, and Javascript. See it live [here](http://jumpstart-squared.cs.house/)! The application has multiple features: @@ -23,7 +23,7 @@ The application has multiple features: 5. An informational that displays real-time status information from CSH’s server room. 6. Calendar module that uses the Python ICalendar to display a countdown to the next 10 events from the CSH calendar. - + 7. Displays a daily forecast. ### AUTHORS: diff --git a/docs/requirements.txt b/docs/requirements.txt index 0eff80d..9044f17 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,4 @@ mkdocstrings mkdocstrings-python mkdocs-minify-plugin mkdocs-git-revision-date-localized-plugin -pymdown-extensions \ No newline at end of file +pymdown-extensions diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c152b9f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = -ra +python_files = test_*.py +pythonpath = src diff --git a/ruff.toml b/ruff.toml index d141539..aa66445 100644 --- a/ruff.toml +++ b/ruff.toml @@ -10,4 +10,4 @@ line-length = 88 [format] quote-style = "double" indent-style = "tab" -skip-magic-trailing-comma = false \ No newline at end of file +skip-magic-trailing-comma = false diff --git a/sonar-project.properties b/sonar-project.properties index 995bea0..7f80d2e 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1 +1 @@ -sonar.projectKey=jumpstart-v2 \ No newline at end of file +sonar.projectKey=jumpstart-v2 diff --git a/src/__init__.py b/src/__init__.py index 8b13789..e69de29 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +0,0 @@ - diff --git a/src/core/slack.py b/src/core/slack.py index b7fda91..4e7ef20 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -5,7 +5,6 @@ from logging import getLogger, Logger from slack_sdk.web.async_client import AsyncWebClient -from slack_sdk.errors import SlackApiError from config import SLACK_API_TOKEN, SLACK_JUMPSTART_MESSAGE, SLACK_DM_TEMPLATE diff --git a/src/logging_config.yaml b/src/logging_config.yaml index 14be8f4..f58de81 100644 --- a/src/logging_config.yaml +++ b/src/logging_config.yaml @@ -23,4 +23,4 @@ loggers: propagate: no root: handlers: [console] - level: INFO \ No newline at end of file + level: INFO diff --git a/src/static/js/main.js b/src/static/js/main.js index 3ea0e45..abd4fc8 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -2,8 +2,8 @@ let currentPageTheme = ""; let currentDatadogTheme = ""; let currentWeatherTheme = ""; -/* - The struct of all the different themes. +/* + The struct of all the different themes. - Page: The .css page theme to be loaded (make sure they start with theme-)! - Datadog: The datadog theme to be loaded, can either be light or dark - Weather: The theme of the weather widget, all the styles can be found here: https://weatherwidget.io/ @@ -46,7 +46,7 @@ function setWeatherTheme(newTheme) { currentWeatherTheme = newTheme; const widget = document.getElementById("weather-image"); widget.setAttribute("data-theme", newTheme); - + if (globalThis.weatherWidget) { widget.innerHTML = widget.innerHTML; weatherWidget.init(); @@ -64,7 +64,7 @@ function setNewPageTheme(newTheme) { document.body.classList.remove(cls); }}); - document.body.classList.toggle(newTheme); + document.body.classList.toggle(newTheme); } async function longUpdate() { @@ -76,7 +76,7 @@ async function longUpdate() { let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); let bgImage = "url(../static/img/darkmodeF.png)"; - + if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { @@ -111,7 +111,7 @@ async function longUpdate() { const res = await fetch('/api/calendar', { method: 'GET', mode: 'cors' }); const data = await res.json(); $("#calendar").html(data.data); - + $("#datadog").attr('src', ddog_dashboard + Date().now()); } catch (err) { @@ -142,7 +142,7 @@ longUpdate(); setInterval(longUpdate, 60000); setInterval(mediumUpdate, 22000); -setInterval(() => { - if (globalThis.__weatherwidget_init) - globalThis.__weatherwidget_init(); -}, 1800000); \ No newline at end of file +setInterval(() => { + if (globalThis.__weatherwidget_init) + globalThis.__weatherwidget_init(); +}, 1800000); diff --git a/src/static/slack/dm_request_template.json b/src/static/slack/dm_request_template.json index 998b881..dc10391 100644 --- a/src/static/slack/dm_request_template.json +++ b/src/static/slack/dm_request_template.json @@ -25,4 +25,4 @@ } ] } -] \ No newline at end of file +] diff --git a/src/templates/base.html b/src/templates/base.html index 182aab0..29cf96f 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -15,4 +15,4 @@ {% block body %}{% endblock %} - \ No newline at end of file + diff --git a/src/templates/index.html b/src/templates/index.html index a2158f2..39a7810 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -29,14 +29,14 @@
- ROCHESTER WEATHER
@@ -74,4 +74,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..96dcc6a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,44 @@ +import coverage +import os + +# Get the absolute path to the src directory +src_path = os.path.join(os.path.dirname((os.path.abspath(__file__))), "..", "src") + +cov: coverage.Coverage = coverage.Coverage(source=[src_path], omit=["__init__.py"]) + + +def pytest_sessionstart(session) -> None: + """ + Start the coverage session. + + Args: + session: The pytest session. + + Returns: + None + """ + + cov.start() + + +def pytest_sessionfinish(session, exitstatus) -> None: + """ + Finish the coverage session. + + Args: + session: The pytest session. + exitstatus: The exit status of the session. + + Returns: + None + """ + + cov.stop() + cov.save() + + try: + cov.html_report(directory="htmlcov") + cov.xml_report(outfile="coverage.xml") + cov.report() + except coverage.exceptions.NoDataError: + print("Warning: No coverage data collected") diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..a6c6634 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +pytest==9.0.2 +coverage==7.13.4 diff --git a/tests/src/test_config.py b/tests/src/test_config.py new file mode 100644 index 0000000..0b33b5c --- /dev/null +++ b/tests/src/test_config.py @@ -0,0 +1,58 @@ +import sys +import importlib + +import pytest + + +def import_config_module() -> object: + """ + Helper function to import the config module after modifying environment variables. + """ + + if "config" in sys.modules: + del sys.modules["config"] + + return importlib.import_module("config") + + +def test_missing_slack_token_raises(monkeypatch) -> None: + """ + Test that if SLACK_API_TOKEN is missing, an exception is raised. + + Args: + monkeypatch: The pytest monkeypatch fixture. + """ + + # Ensure SLACK_API_TOKEN is not set + monkeypatch.delenv("SLACK_API_TOKEN", raising=False) + # Prevent leftover module from interfering + sys.modules.pop("config", None) + + with pytest.raises(Exception, match="Missing SLACK_API_TOKEN"): + import_config_module() + + +def test_with_slack_token_parses_values(monkeypatch) -> None: + """ + Test that with a valid SLACK_API_TOKEN, the config values are parsed correctly. + + Args: + monkeypatch: The pytest monkeypatch fixture. + """ + + monkeypatch.setenv("SLACK_API_TOKEN", "test-token") + monkeypatch.setenv("WATCHED_CHANNELS", "chan1,chan2") + monkeypatch.setenv("CALENDAR_OUTLOOK_DAYS", "5") + monkeypatch.setenv("CALENDAR_EVENT_MAXIMUM", "20") + monkeypatch.setenv("CALENDAR_TIMEZONE", "UTC") + monkeypatch.setenv("CALENDAR_CACHE_REFRESH", "15") + + sys.modules.pop("config", None) + cfg = import_config_module() + + assert cfg.SLACK_API_TOKEN == "test-token" + assert cfg.WATCHED_CHANNELS == ("chan1", "chan2") + assert cfg.CALENDAR_OUTLOOK_DAYS == 5 + assert cfg.CALENDAR_EVENT_MAXIMUM == 20 + assert cfg.CALENDAR_TIMEZONE == "UTC" + assert isinstance(cfg.SLACK_DM_TEMPLATE, dict) From 0ac617fc1a3e694c0667bbef0b97d1944d291ddd Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Thu, 2 Apr 2026 03:11:35 -0400 Subject: [PATCH 09/48] Add checking before assuming creds are present and crashing Add pytest tests --- src/api/endpoints.py | 50 ++++++++---- src/config.py | 57 +++++++++---- src/core/cshcalendar.py | 5 ++ src/core/wikithoughts.py | 60 ++++++++++++-- src/requirements.txt | 1 + tests/src/core/test_slack.py | 154 +++++++++++++++++++++++++++++++++++ tests/src/test_config.py | 20 ++--- 7 files changed, 295 insertions(+), 52 deletions(-) create mode 100644 tests/src/core/test_slack.py diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 887d28d..6c7324c 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -2,8 +2,6 @@ import json import httpx -import random -import textwrap from fastapi import APIRouter, Request, Form from fastapi.responses import JSONResponse @@ -24,12 +22,18 @@ async def get_calendar() -> JSONResponse: JSONResponse: A JSON response containing the calendar data. """ - get_future_events_ical: tuple[ - cshcalendar.CalendarInfo - ] = await cshcalendar.get_future_events() - formatted_events: dict = cshcalendar.format_events(get_future_events_ical) + events: dict[str, str] = {} - return JSONResponse(formatted_events) + try: + get_future_events_ical: tuple[ + cshcalendar.CalendarInfo + ] = await cshcalendar.get_future_events() + events = cshcalendar.format_events(get_future_events_ical) + except Exception as e: + logger.error(f"Error fetching calendar events: {e}") + return JSONResponse({"status": "error", "message": str(e)}, status_code=500) + + return JSONResponse(events) @router.get("/api/announcement") @@ -57,18 +61,17 @@ async def slack_events(request: Request) -> JSONResponse: """ try: - logger.info("Received Slack event!") + logger.debug(f"Received Slack event: {await request.body()}") body: dict = await request.json() - logger.info(body) - logger.info("\n") + 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")}) if not body: - logger.info("SLACK EVENT: Was a challenge, with no body") + logger.debug("SLACK EVENT: Was a challenge, with no body") return JSONResponse({"challenge": body.get("challenge")}) event: dict = body.get("event", {}) @@ -79,8 +82,9 @@ async def slack_events(request: Request) -> JSONResponse: return JSONResponse({"status": "ignored"}) if event.get("channel", None) not in WATCHED_CHANNELS: - logger.info("SLACK EVENT: Message was not in a Watched Channel, returning!") - logger.info(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!") @@ -112,12 +116,14 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: return JSONResponse({}, status_code=200) if slack.convert_user_response_to_bool(form_json): - logger.info("User approved the announcement!") - logger.info(f"{form_json}\n\n") + 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) - logger.info(f"Display Object {message_object}") + slack.add_announcement(message_object) if response_url: @@ -147,5 +153,13 @@ async def wikithought() -> JSONResponse: Returns: JSONResponse: A JSON response containing a random Wiki thought. """ - returned_page_data: dict[str, str] = await wikithoughts.get_next_display() - return JSONResponse(returned_page_data) + + page_data: dict | None = None + + try: + page_data = await wikithoughts.get_next_display() + except Exception as e: + logger.error(f"Error fetching wiki thought: {e}") + return JSONResponse({"status": "error", "message": str(e)}, status_code=500) + + return JSONResponse(page_data) diff --git a/src/config.py b/src/config.py index b641377..9dd7e22 100644 --- a/src/config.py +++ b/src/config.py @@ -1,30 +1,55 @@ import os import json +import logging from dotenv import load_dotenv - load_dotenv() +logger: logging.Logger = logging.getLogger(__name__) + + +def _get_env_variable(name: str, default: str | None = None) -> str | None: + """ + Retrieves an environment variable, with an optional default value. + + Args: + name (str): The name of the environment variable to retrieve. + default (str | None): An optional default value to return if the environment variable is not set. + + Returns: + str | None: The value of the environment variable, or the default value if it is not set. + """ + + value: str = os.getenv(name, default) + + if value in (None, ""): + logger.warning( + f"Environment variable '{name}' is not set, using default value: '{default if default is not None else 'None'}'" + ) + return default + + return value + + BASE_DIR: str = os.path.dirname(os.path.abspath(__file__)) -SLACK_API_TOKEN: str | None = os.getenv("SLACK_API_TOKEN", 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?" -WATCHED_CHANNELS: tuple[str] = tuple(os.getenv("WATCHED_CHANNELS", "").split(",")) +WATCHED_CHANNELS: tuple[str] = tuple( + _get_env_variable("WATCHED_CHANNELS", "").split(",") +) SLACK_DM_TEMPLATE: dict | None = None -CALENDAR_URL: str | None = os.getenv("CALENDAR_URL", None) -CALENDAR_OUTLOOK_DAYS: int = int(os.getenv("CALENDAR_OUTLOOK_DAYS", "7")) -CALENDAR_EVENT_MAXIMUM: int = int(os.getenv("CALENDAR_EVENT_MAXIMUM", "10")) -CALENDAR_TIMEZONE: str = os.getenv("CALENDAR_TIMEZONE", "America/New_York") -CALENDAR_CACHE_REFRESH: int = int(os.getenv("CALENDAR_CACHE_REFRESH", "10")) - -WIKI_API: str | None = os.getenv("WIKI_API", None) -WIKIBOT_USER: str | None = os.getenv("WIKIBOT_USER", None) -WIKIBOT_PASSWORD: str | None = os.getenv("WIKIBOT_PASSWORD", None) -WIKI_CATEGORY: str = os.getenv("WIKI_CATEGORY", "JobAdvice") +CALENDAR_URL: str | None = _get_env_variable("CALENDAR_URL", None) +CALENDAR_OUTLOOK_DAYS: int = int(_get_env_variable("CALENDAR_OUTLOOK_DAYS", "7")) +CALENDAR_EVENT_MAXIMUM: int = int(_get_env_variable("CALENDAR_EVENT_MAXIMUM", "10")) +CALENDAR_TIMEZONE: str = _get_env_variable("CALENDAR_TIMEZONE", "America/New_York") +CALENDAR_CACHE_REFRESH: int = int(_get_env_variable("CALENDAR_CACHE_REFRESH", "10")) -if SLACK_API_TOKEN in (None, ""): - raise ValueError("Missing SLACK_API_TOKEN") +WIKI_API: str | None = _get_env_variable("WIKI_API", None) +WIKIBOT_USER: str | None = _get_env_variable("WIKIBOT_USER", None) +WIKIBOT_PASSWORD: str | None = _get_env_variable("WIKIBOT_PASSWORD", None) +WIKI_CATEGORY: str = _get_env_variable("WIKI_CATEGORY", "JobAdvice") with open(os.path.join(BASE_DIR, "static", "slack", "dm_request_template.json")) as f: - SLACK_DM_TEMPLATE = json.load(f) + SLACK_DM_TEMPLATE = dict(json.load(f)) diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index dcaabc5..3012585 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -89,6 +89,7 @@ def ceil_division(num: int, den: int) -> int: Returns: int: the result of the operation """ + return (num + den - 1) // den @@ -113,6 +114,7 @@ def repl(match: re.Match[str]) -> str: Returns: str: The newly formatted string """ + num = int(match.group(1)) return str(round(time_before_event / num)) @@ -146,6 +148,7 @@ def calendar_to_html(seg_header: str, seg_content: str) -> str: Returns: str: """ + ret_string: str = ( """
""" + seg_header @@ -202,6 +205,7 @@ async def rebuild_calendar() -> None: """ Fetches and rebuilds the global calendar cache. This does NOT return a new cache, but changes the global calendar cache """ + global calendar_cache, cal_last_update, cal_constructed_event try: cal_constructed_event.clear() @@ -333,6 +337,7 @@ async def close_client() -> None: Closes the calendar's HTTPX client, logs if the event loops has been closed prior to the function being called """ + global cshcal_client try: await cshcal_client.aclose() diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 4173fab..337880c 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -1,12 +1,15 @@ +import re import httpx +import random import asyncio -from datetime import datetime, timedelta -from itertools import islice +import logging + from typing import Pattern +from itertools import islice +from datetime import datetime, timedelta + from config import WIKIBOT_PASSWORD, WIKIBOT_USER, WIKI_CATEGORY, WIKI_API -import logging -import random -import re + CYCLE_DEBOUNCE_TIME: int = 12 # How long it takes to resfresh wiki titles BATCH_SIZE: int = 50 # max titles per request @@ -17,10 +20,15 @@ HEADERS: dict[str, str] = {"User-Agent": "JumpstartFetcher/1.0"} AUTH: tuple[str] = (WIKIBOT_USER, WIKIBOT_PASSWORD) -client: httpx.AsyncClient = httpx.AsyncClient(headers=HEADERS, auth=AUTH) - logger: logging.Logger = logging.getLogger(__name__) +client: httpx.AsyncClient | None = None + +try: + client = httpx.AsyncClient(headers=HEADERS, auth=AUTH) +except Exception as e: + logger.warning(f"Failed to initialize HTTP client for wiki: {e}") + bot_authenticated: bool = False last_updated_time: datetime | None = None @@ -96,10 +104,13 @@ def batch_iterable(iterable: list, size: int): Args: iterable (list): The iterable to be split up for batches size (int): the size of the batches + Yields: A batch split by the requested size """ + it = iter(iterable) + while True: batch = list(islice(it, size)) if not batch: @@ -111,6 +122,17 @@ async def auth_bot() -> None: """ Authenticates the CSH Wiki bot, logging if it was successful or not """ + + if not WIKI_API or not WIKIBOT_USER or not WIKIBOT_PASSWORD: + logger.warning("Missing wiki API credentials, unable to authenticate the bot!") + return + + if not client: + logger.warning( + "HTTP client for wiki is not initialized, unable to authenticate the bot!" + ) + return + token_req: httpx.Response = await client.get( WIKI_API, params={"action": "query", "meta": "tokens", "type": "login", "format": "json"}, @@ -154,6 +176,7 @@ def headers_formatting( Returns: dict[str,str]: The new headers to be applied """ + global etag, last_modifed headers: dict[str, str] = {} @@ -166,6 +189,7 @@ def headers_formatting( if etag: headers["If-None-Match"] = etag + if last_modifed: headers["If-Modified-Since"] = last_modifed @@ -204,7 +228,9 @@ def process_category_page(r_json: dict[str, str]) -> tuple[list[str], bool | str Returns: tuple[list[str], bool | str]: The list of titles from the request, along with a possible continutation if needed """ + titles: list[str] = [] + if "query" in r_json: for page in r_json["query"]["categorymembers"]: titles.append(page["title"]) @@ -229,6 +255,13 @@ async def refresh_category_pages() -> list[str]: Returns: list[str]: All the page titles found in this category, None if the bot is not authorized """ + + if not client: + logger.warning( + "HTTP client for wiki is not initialized, unable to refresh category pages!" + ) + return [] + global page_title_cache, last_updated_time, queued_pages, shown_pages time_now: datetime = datetime.now() @@ -272,7 +305,7 @@ async def refresh_category_pages() -> list[str]: ) return [] - logger.info(f"Both was unauthenticated, attempting to reauthenticate!") + logger.info("Both was unauthenticated, attempting to reauthenticate!") await auth_bot() if not (bot_authenticated): logger.warning( @@ -309,6 +342,13 @@ async def refresh_page_dictionary() -> None: """ Fetches the pages based off the cache of page titles, and updates the page cache accordingly """ + + if not client: + logger.warning( + "HTTP client for wiki is not initialized, unable to refresh page dictionary!" + ) + return + global page_dict_cache, page_title_cache if not page_title_cache: @@ -357,8 +397,11 @@ def reset_queues() -> None: """ Swaps Queued and Shown pages queued """ + global queued_pages, shown_pages + logger.info("RESETING QUEUES FOR WIKITHOUGHTS") + if len(queued_pages) > 0: return @@ -375,6 +418,7 @@ async def get_next_display() -> dict[str, str]: Returns: dict["page": str,"content": str]: The JSON of the page name and the first paragraph """ + global queued_pages, shown_pages, page_last_updated, current_page if page_last_updated and ( diff --git a/src/requirements.txt b/src/requirements.txt index 0b1faf8..b345008 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -20,6 +20,7 @@ recurring-ical-events==3.8.1 arrow==1.4.0 # For Wikithoughts +httpx==0.28.1 # For the docker to run uvicorn==0.41.0 diff --git a/tests/src/core/test_slack.py b/tests/src/core/test_slack.py new file mode 100644 index 0000000..d410aff --- /dev/null +++ b/tests/src/core/test_slack.py @@ -0,0 +1,154 @@ +import sys +import asyncio +import importlib + + +def import_slack_module(monkeypatch) -> object: + """ + Helper function to import the slack module after setting the SLACK_API_TOKEN environment variable. + + Args: + monkeypatch: The pytest monkeypatch fixture. + + Returns: + object: The imported config module. + """ + + monkeypatch.setenv("SLACK_API_TOKEN", "test-token") + + sys.modules.pop("config", None) + sys.modules.pop("core.slack", None) + + importlib.import_module("config") + + return importlib.import_module("core.slack") + + +def test_clean_text_and_convert_response(monkeypatch): + """ + Test the clean_text and convert_user_response_to_bool functions in the slack module. + + Args: + monkeypatch: The pytest monkeypatch fixture. + """ + + slack = import_slack_module(monkeypatch) + + raw = "Hello *world* _there_ `code` <skip>" + cleaned = slack.clean_text(raw) + assert "<" not in cleaned + assert "*" not in cleaned + assert "_" not in cleaned + assert "`" not in cleaned + + yes_payload = {"actions": [{"action_id": "yes_j"}]} + no_payload = {"actions": [{"action_id": "no_j"}]} + assert slack.convert_user_response_to_bool(yes_payload) is True + assert slack.convert_user_response_to_bool(no_payload) is False + # malformed payload + assert slack.convert_user_response_to_bool({}) is False + + +def test_gather_emojis_success_and_failure(monkeypatch): + """ + Test the gather_emojis function in the slack module. + + Args: + monkeypatch: The pytest monkeypatch fixture. + """ + + slack = import_slack_module(monkeypatch) + + class FakeClientSuccess: + async def emoji_list(self): + return {"ok": True, "emoji": {"smile": "url"}} + + monkeypatch.setattr(slack, "client", FakeClientSuccess()) + emojis = asyncio.run(slack.gather_emojis()) + assert emojis == {"smile": "url"} + + class FakeClientFail: + async def emoji_list(self): + raise RuntimeError("boom") + + monkeypatch.setattr(slack, "client", FakeClientFail()) + emojis = asyncio.run(slack.gather_emojis()) + assert emojis == {} + + monkeypatch.setattr(slack, "client", None) + emojis = asyncio.run(slack.gather_emojis()) + assert emojis == {} + + +def test_request_upload_via_dm_success_and_exception(monkeypatch): + """ + Test the request_upload_via_dm function in the slack module. + + Args: + monkeypatch: The pytest monkeypatch fixture. + """ + + slack = import_slack_module(monkeypatch) + + monkeypatch.setattr( + slack, + "SLACK_DM_TEMPLATE", + [{"text": {"text": ""}}, {"elements": [{"value": ""}]}], + ) + + recorded = {} + + class FakeClient: + async def chat_postMessage(self, *, channel, text, blocks): + recorded["channel"] = channel + recorded["text"] = text + recorded["blocks"] = blocks + + monkeypatch.setattr(slack, "client", FakeClient()) + + asyncio.run(slack.request_upload_via_dm("U123", "Announcement!")) + assert recorded["channel"] == "U123" + assert recorded["text"] == slack.SLACK_JUMPSTART_MESSAGE + + assert isinstance(recorded["blocks"], list) + assert "Announcement!" in recorded["blocks"][0]["text"]["text"] + + class BrokenClient: + async def chat_postMessage(self, **_): + raise RuntimeError("nope") + + monkeypatch.setattr(slack, "client", BrokenClient()) + + asyncio.run(slack.request_upload_via_dm("U123", "Announcement!")) + + +def test_get_and_add_announcement(monkeypatch): + """ + Test the get_announcement and add_announcement functions in the slack module. + + Args: + monkeypatch: The pytest monkeypatch fixture. + """ + + slack = import_slack_module(monkeypatch) + + slack.announcements.clear() + assert slack.get_announcement() is None + + skip_announcements: list[str | None] = [None, "", " "] + for ann in skip_announcements: + slack.add_announcement(ann) + + assert slack.get_announcement() is None + + test_announcements: list[str] = ["First", "Second", "Third"] + + for ann in test_announcements: + slack.add_announcement(ann) + + for ann in test_announcements: + assert slack.get_announcement() == ann + + assert ( + slack.get_announcement() == test_announcements[-1] + ) # should return last announcement when queue is empty diff --git a/tests/src/test_config.py b/tests/src/test_config.py index 0b33b5c..49c16e0 100644 --- a/tests/src/test_config.py +++ b/tests/src/test_config.py @@ -1,12 +1,13 @@ import sys import importlib -import pytest - def import_config_module() -> object: """ Helper function to import the config module after modifying environment variables. + + Returns: + object: The imported config module. """ if "config" in sys.modules: @@ -15,21 +16,20 @@ def import_config_module() -> object: return importlib.import_module("config") -def test_missing_slack_token_raises(monkeypatch) -> None: +def test_get_env_variable(monkeypatch) -> None: """ - Test that if SLACK_API_TOKEN is missing, an exception is raised. + Test the _get_env_variable function to ensure it retrieves environment variables correctly. Args: monkeypatch: The pytest monkeypatch fixture. """ - # Ensure SLACK_API_TOKEN is not set - monkeypatch.delenv("SLACK_API_TOKEN", raising=False) - # Prevent leftover module from interfering - sys.modules.pop("config", None) + monkeypatch.setenv("TEST_ENV_VAR", "test_value") + cfg = import_config_module() - with pytest.raises(Exception, match="Missing SLACK_API_TOKEN"): - import_config_module() + assert cfg._get_env_variable("TEST_ENV_VAR", None) == "test_value" + assert cfg._get_env_variable("NON_EXISTENT_VAR", "default_value") == "default_value" + assert cfg._get_env_variable("NON_EXISTENT_VAR", None) is None def test_with_slack_token_parses_values(monkeypatch) -> None: From dedbaeaa900406dfd1bfb08da47a6b5a8d05e4a8 Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 11:14:33 -0400 Subject: [PATCH 10/48] fix: Oops, It isin't 50th yet --- src/static/js/main.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index 6f22885..3181218 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -2,7 +2,6 @@ let currentPageTheme = ""; let currentDatadogTheme = ""; let currentWeatherTheme = ""; -let mode_enabled = false; /* The struct of all the different themes. - Page: The .css page theme to be loaded (make sure they start with theme-)! @@ -88,9 +87,6 @@ async function longUpdate() { let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); let bgImage = "url(../static/img/darkmodeF.png)"; - is_golden = mode_enabled; - mode_enabled = !mode_enabled; - if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { From 2aa2d5c8a5ba35919543ee0bdce30a2dd945c2dc Mon Sep 17 00:00:00 2001 From: Weather Date: Sat, 28 Mar 2026 03:13:44 -0400 Subject: [PATCH 11/48] feat: implements css themes and 50th start --- Dockerfile | 2 +- src/core/cshcalendar.py | 2 +- src/static/css/style.css | 91 ++++++++++++++++++++++++++++++---------- src/static/js/main.js | 58 +++++++++++++------------ src/templates/index.html | 4 +- 5 files changed, 105 insertions(+), 52 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36e221c..c1a8241 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ RUN addgroup -g 2000 jumpgroup && adduser -S -u 1001 -G jumpgroup jumpstart && \ USER jumpstart -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "/jumpstart/logging_config.yaml", "--proxy-headers","--forwarded-allow-ips","*"] +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "/jumpstart/logging_config.yaml", "--proxy-headers", "--forwarded-allow-ips", "*"] diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 3434ff8..dcaabc5 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -54,7 +54,7 @@ (WEEK - DAY): f"In %{DAY}% Days", } -BORDER_STRING: str = "
" +BORDER_STRING: str = '
' TIME_PATTERN = re.compile(r"%([^%]+)%") diff --git a/src/static/css/style.css b/src/static/css/style.css index 47b7d0a..3f70c71 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -1,3 +1,40 @@ +/* Creates a default theme, but can get overwritten */ +:root { + --panel-header-color: #B0197E; /* Standard color for panels header*/ + --panel-body-color: #FFFFFF; /* text body */ + --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ +} + +.theme-dark { + --panel-header-color: #B0197E; /* Standard color for panels header*/ + --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-color: #000000; /* text body */ + --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgba(255, 0, 170, 0.705); /* Used for the shadow in the calendar */ +} + +.theme-light { + --panel-header-color: #B0197E; /* Standard color for panels header*/ + --panel-body-color: #FFFFFF; /* text body */ + --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-text-color: #000000; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ +} + +.theme-golden { + --panel-header-color: #CC9318; /* Standard color for panels header*/ + --panel-header-text-color: #000000; /* Used for headers in panels, like Page - csh/Wikithoughts */ + --panel-body-color: #000000; /* text body */ + --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ + --border-color: #34495E; /* Used for borders around elements */ + --shadow-color: rgba(206, 191, 149, 0.767); /* Used for the shadow in the calendar */ +} + body{ background-image: url('/static/img/darkmodeF.png'); overflow: hidden; @@ -10,14 +47,21 @@ body{ } .panel-primary{ - background: #34495e; - border: 8px #34495e solid ; + background: var(--border-color); + border: 8px var(--border-color) solid ; border-radius: 10px; } +.panel-heading{ + background-color: var(--panel-header-color) !important; +} + +.panel-body{ + background-color: var(--panel-body-color) !important; +} #top-bar{ - border: 5px #B0197E solid; - background-color: #34495e; + border: 5px var(--panel-header-color) solid; + background-color: var(--border-color); } #timewidget-fake{ @@ -25,19 +69,18 @@ body{ } .file-title{ - color: #fcf6fa; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 28px; - border-left: 5px #B0197E solid; + border-left: 5px var(--panel-header-color) solid; float: right; width: 29%; } .js-title{ - color: #fcf6fa; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 30px; - border-left: 5px #B0197E solid; float: right; } @@ -49,18 +92,18 @@ body{ .weatherwidget{ float: right; width: 49%; - border: 8px #34495e solid; + border: 8px var(--border-color) solid; border-radius: 10px; - background-color: #34495e; + background-color: var(--border-color); margin-bottom: 1.5%; } .datadog{ margin-top: 20px; overflow: auto; - border: 8px #34495e solid; + border: 8px var(--border-color) solid; border-radius: 10px; - background-color: #34495e; + background-color: var(--border-color); width: 760px; height: 580px; float: right; @@ -78,14 +121,14 @@ body{ } .announcements-text-header{ - color: white; + color: var(--panel-header-text-color); font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; } .announcements-text-body{ - color: black; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 18px; text-align: center; @@ -98,22 +141,22 @@ body{ } .wikithoughts-text-header{ - color: white; + color: var(--panel-header-text-color); font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; } .wikithoughts-text-body{ - color: black; + color:var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 18px; text-align: center; } .calendar-frame-lvl1{ - background-color: #fcf6fa; - border: 8px #34495e solid; + background-color: var(--panel-body-color); + border: 8px var(--border-color) solid; border-radius: 10px; margin-top: 20px; height: 950px; @@ -127,19 +170,25 @@ body{ } .calendar-text-date{ - color: #4c4c4c; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 20px; - text-shadow: 2px 2px 3px rgb(176,25,126, 0.5); + text-shadow: 2px 2px 3px var(--shadow-color); text-align: center; } .calendar-text{ - color: black; + color: var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; font-size: 19px; } +.calendar-border{ + border: 0px solid; + border-top: 2px solid var(--panel-header-color); + margin: 10px 0; +} + @media screen and (max-width: 1899px) { .wikithoughts{ width: 49%; diff --git a/src/static/js/main.js b/src/static/js/main.js index 92f0f18..cc2f2e9 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -1,22 +1,45 @@ +let current_theme = ""; + +function resetAllThemes(){ + document.body.classList.forEach(cls => { + if (cls.startsWith('theme-')) { + document.body.classList.remove(cls); + }}); +} + +function setNewTheme(newTheme) { + if (newTheme === current_theme) { + return; + } + resetAllThemes(); + document.body.classList.toggle(newTheme); + current_theme = newTheme; +} + async function longUpdate() { const date = new Date(); const hour = date.getHours(); const month = date.getMonth() + 1; const day = date.getDate(); + const isDay = (hour > 9 && hour < 18); + let is_golden = true; let bgImage = "url(../static/img/darkmodeF.png)"; + if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { bgImage = "url(../static/img/jumpstartbang.png)"; + } else if (month === 4 && [10, 11, 12].includes(day)) { + is_golden = true; } else if (month === 10 && [29, 30, 31].includes(day)) { bgImage = "url(../static/img/spookymode.png)"; } else if (month === 11 && day === 2) { bgImage = "url(../static/img/duckymode2.png)"; } else if ([11, 12].includes(month)) { bgImage = "url(../static/img/wintermode.png)"; - } else if (hour > 9 && hour < 18) { + } else if (isDay) { bgImage = "url(../static/img/lightmodeF.png)"; } $("body").css("background-image", bgImage); @@ -26,34 +49,15 @@ async function longUpdate() { const data = await res.json(); $("#calendar").html(data.data); - const isDay = hour > 9 && hour < 18; - const panelBody = $(".panel-body"); - const plugBody = $(".plug-body"); - const wikiPages = $(".wikithoughts-text-body"); - const announcementsBody = $(".announcements-text-body"); - const calendarFrame = $(".calendar-frame-lvl1"); - const calendarTextDate = $(".calendar-text-date"); - const calendarText = $(".calendar-text"); - - if (isDay) { - panelBody.css("background-color", "white"); - plugBody.css("background-color", "white"); - wikiPages.css({ "background-color": "white", "color": "black" }); - announcementsBody.css("color", "black"); - calendarFrame.css("background-color", "white"); - calendarTextDate.css("color", "black"); - calendarText.css("color", "black"); - } else { - panelBody.css("background-color", "black"); - plugBody.css("background-color", "black"); - wikiPages.css({ "background-color": "black", "color": "white" }); - announcementsBody.css("color", "white"); - calendarFrame.css("background-color", "black"); - calendarTextDate.css("color", "white"); - calendarText.css("color", "white"); + if (is_golden){ + setNewTheme("theme-golden") + } else if (isDay) { + setNewTheme("theme-light") + } else{ + setNewTheme("theme-dark") } - $("#datadog").attr('src', ddog_dashboard + new Date().now()); + $("#datadog").attr('src', ddog_dashboard + Date().now()); } catch (err) { console.log(err); diff --git a/src/templates/index.html b/src/templates/index.html index 1c48aa3..676ff41 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -15,7 +15,7 @@
- +
@@ -37,7 +37,7 @@ data-font="Fira Sans" data-icons="Climacons Animated" data-days="100" - data-theme="kitty"> + data-theme="dark"> ROCHESTER WEATHER
- {% block head %}{% endblock %} diff --git a/src/templates/index.html b/src/templates/index.html index 676ff41..a2158f2 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -73,4 +73,5 @@
+ {% endblock %} \ No newline at end of file From dae2e139f766e32205225be33b697762fa2d3a6d Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 30 Mar 2026 17:30:15 -0400 Subject: [PATCH 15/48] fix: 50th only being set for 50th --- src/static/js/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index 4b9bf6d..3ea0e45 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -75,7 +75,6 @@ async function longUpdate() { const isDay = (hour > 9 && hour < 18); let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); - is_golden = true; let bgImage = "url(../static/img/darkmodeF.png)"; if (month === 2 && [12, 13, 14].includes(day)) { From f7e04b0aa9f79071628946e8b932fda09641115b Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 01:03:47 -0400 Subject: [PATCH 16/48] fix: Weather widget not updating properly --- mkdocs.yml | 6 +- src/api/endpoints.py | 20 ++++--- src/core/cshcalendar.py | 7 +-- src/core/wikithoughts.py | 121 +++++++++++++++++++++++---------------- src/main.py | 4 +- src/requirements.txt | 2 - src/static/css/style.css | 36 ++++++------ src/static/js/main.js | 49 ++++++++++------ 8 files changed, 141 insertions(+), 104 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index acafb5b..234a459 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,10 +38,10 @@ use_directory_urls: false nav: - Home: index.md - Getting Started: getting-started/getting-started.md - - Backend: - - Wikithoughts: core/Wiki-thoughts.md + - Backend: - Calendar: core/CSH Calendar.md - - Slack: core/Slack.md + - Slack Bot: core/Slack.md + - Wiki Thoughts: core/Wiki-thoughts.md - Endpoints: - Calendar: endpoints/calendar_endpoint.md - Announcements: endpoints/announcements.md diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 887d28d..28de1ea 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -14,6 +14,8 @@ logger: Logger = getLogger(__name__) router: APIRouter = APIRouter() +ACCEPT_MESSAGE: str = "Posting right now :^)" +DENY_MESSAGE: str = "Okay :( maybe next time" @router.get("/api/calendar") async def get_calendar() -> JSONResponse: @@ -121,16 +123,18 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: slack.add_announcement(message_object) if response_url: - await httpx.post( - response_url, - json={"text": "Posting right now :^)", "replace_original": True}, - ) + async with httpx.AsyncClient() as client: + await client.post( + response_url, + json={"text": ACCEPT_MESSAGE, "replace_original": True}, + ) else: if response_url: - await httpx.post( - response_url, - json={"text": "Okay :( maybe next time", "replace_original": True}, - ) + async with httpx.AsyncClient() as client: + await client.post( + response_url, + json={"text": DENY_MESSAGE, "replace_original": True}, + ) except Exception as e: logger.error(f"Error in message_actions: {e}") diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index dcaabc5..a9bb7d9 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -314,10 +314,9 @@ async def get_future_events() -> list[CalendarInfo]: if cal_correct_length: logger.info("Calendar cache is full length, rebuilding async!") - asyncio.create_task( - rebuild_calendar() - ) # Calendar is correct length, we can just run this in the background - # Make it a variable for GC purposes? idk sonarqube told me to do it + async with asyncio.TaskGroup() as taskGroup: + taskGroup.create_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() diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 4173fab..3856464 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -219,6 +219,64 @@ def process_category_page(r_json: dict[str, str]) -> tuple[list[str], bool | str return (titles, False) + +async def fetch_category_pages(response: httpx.Response) -> list[str]: + """ + Loops through and gets the list of every page for Jumpstart Curated + + Args: + response (httpx.Response): The response to be converted and searched through + + Returns: + list[str]: The list of titles to be fetched. + """ + + params: dict[str, str] = { + "action": "query", + "list": "categorymembers", + "cmtitle": f"Category:{WIKI_CATEGORY}", + "cmlimit": "500", + "format": "json", + } + + titles_found: list[str] = [] + + while True: + r_json: dict[str, str] = response.json() + + if "error" in r_json and r_json["error"].get("code") in ( + "readapidenied", + "notloggedin", + ): + if failed_authentication_attempts > REAUTHENTICATE_ATTEMPTS: + logger.warning( + "Reauthenticating the wikithought bot failed, sending empty response" + ) + return [] + + logger.info("Bot was unauthenticated, attempting to reauthenticate!") + await auth_bot() + if not (bot_authenticated): + logger.warning( + f"Failed to reauthenticate the bot! Attempt: {failed_authentication_attempts}" + ) + + failed_authentication_attempts += 1 + continue + + added, repeat_req = process_category_page(r_json) + titles_found += added + + if repeat_req not in (None, False, ""): + params["cmcontinue"] = repeat_req + + response = await client.get( + WIKI_API, params=params + ) + continue + break + return titles_found + async def refresh_category_pages() -> list[str]: """ Refreshes all pages of the category @@ -227,7 +285,8 @@ async def refresh_category_pages() -> list[str]: category (str): The name of the category to search through Returns: - list[str]: All the page titles found in this category, None if the bot is not authorized + list[str] + : All the page titles found in this category, None if the bot is not authorized """ global page_title_cache, last_updated_time, queued_pages, shown_pages time_now: datetime = datetime.now() @@ -245,54 +304,19 @@ async def refresh_category_pages() -> list[str]: } headers: dict[str, str] = headers_formatting() - # This needs to loop due to mediawiki limitations - - failed_authentication_attempts: int = 0 + response: httpx.Response = await client.get( + WIKI_API, params=params, headers=headers + ) - while True: - response: httpx.Response = await client.get( - WIKI_API, params=params, headers=headers - ) - - if response.status_code == 304: - last_updated_time = time_now - return page_title_cache - - elif response.status_code == 200: - headers_formatting(etag, last_modifed) - r_json: dict[str, str] = response.json() - - if "error" in r_json and r_json["error"].get("code") in ( - "readapidenied", - "notloggedin", - ): - if failed_authentication_attempts > REAUTHENTICATE_ATTEMPTS: - logger.warning( - "Reauthenticating the wikithought bot failed, sending empty response" - ) - return [] - - logger.info(f"Both was unauthenticated, attempting to reauthenticate!") - await auth_bot() - if not (bot_authenticated): - logger.warning( - "Failed to reauthenticate the bot! Attempt: " - + failed_authentication_attempts - ) - - failed_authentication_attempts += 1 - continue - - added, repeat_req = process_category_page(r_json) - titles += added - - if repeat_req not in (None, False, ""): - params["cmcontinue"] = repeat_req - continue - break - else: - logger.warning("Failed to update the CSH wiki page!") - return page_title_cache + if response.status_code == 304: + last_updated_time = time_now + return page_title_cache + + elif response.status_code == 200: + titles = await fetch_category_pages(response=response) + else: + logger.warning("Failed to update the CSH wiki page!") + return page_title_cache last_updated_time = time_now page_title_cache = titles @@ -335,7 +359,6 @@ async def refresh_page_dictionary() -> None: if "query" in r_json: for page in r_json["query"]["pages"].values(): wikitext = page["revisions"][0]["slots"]["main"]["*"] - cleaned_text = clean_wikitext(wikitext) # unfuck the text paragraphs = cleaned_text.split("\n\n") # Cut the first line diff --git a/src/main.py b/src/main.py index 43b7f13..cd14e90 100644 --- a/src/main.py +++ b/src/main.py @@ -27,8 +27,10 @@ @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Starting up the Jumpstart application!") - asyncio.create_task(cshcalendar.rebuild_calendar()) + async with asyncio.TaskGroup() as tg: + tg.create_task(cshcalendar.rebuild_calendar()) await wikithoughts.auth_bot() + yield logger.info("Shutting down the Jumpstart application!") await cshcalendar.close_client() diff --git a/src/requirements.txt b/src/requirements.txt index 0b1faf8..305684d 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -19,8 +19,6 @@ icalendar==7.0.3 recurring-ical-events==3.8.1 arrow==1.4.0 -# For Wikithoughts - # For the docker to run uvicorn==0.41.0 pyyaml==6.0.3 diff --git a/src/static/css/style.css b/src/static/css/style.css index ef5a5a5..750eb83 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -9,30 +9,30 @@ } .theme-dark { - --panel-header-color: #B0197E; /* Standard color for panels header*/ - --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ - --panel-body-color: #000000; /* text body */ - --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ - --border-color: #34495E; /* Used for borders around elements */ - --shadow-color: rgba(255, 0, 170, 0.705); /* Used for the shadow in the calendar */ + --panel-header-color: #B0197E; + --panel-header-text-color: #FFFFFF; + --panel-body-color: #000000; + --panel-body-text-color: #FFFFFF; + --border-color: #34495E; + --shadow-color: rgba(255, 0, 170, 0.705); } .theme-light { - --panel-header-color: #B0197E; /* Standard color for panels header*/ - --panel-body-color: #FFFFFF; /* text body */ - --panel-header-text-color: #FFFFFF; /* Used for headers in panels, like Page - csh/Wikithoughts */ - --panel-body-text-color: #000000; /* Used for the text within said panels */ - --border-color: #34495E; /* Used for borders around elements */ - --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ + --panel-header-color: #B0197E; + --panel-body-color: #FFFFFF; + --panel-header-text-color: #FFFFFF; + --panel-body-text-color: #000000; + --border-color: #34495E; + --shadow-color: rgb(176,25,126, 0.5); } .theme-golden { - --panel-header-color: #FFC64A; /* Standard color for panels header*/ - --panel-header-text-color: #000000; /* Used for headers in panels, like Page - csh/Wikithoughts */ - --panel-body-color: #000000; /* text body */ - --panel-body-text-color: #FFFFFF; /* Used for the text within said panels */ - --border-color: #34495E; /* Used for borders around elements */ - --shadow-color: rgba(206, 191, 149, 0.767); /* Used for the shadow in the calendar */ + --panel-header-color: #FFC64A; + --panel-header-text-color: #000000; + --panel-body-color: #000000; + --panel-body-text-color: #FFFFFF; + --border-color: #34495E; + --shadow-color: rgb(255, 212, 94); } body{ diff --git a/src/static/js/main.js b/src/static/js/main.js index 3ea0e45..6f22885 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -2,6 +2,7 @@ let currentPageTheme = ""; let currentDatadogTheme = ""; let currentWeatherTheme = ""; +let mode_enabled = false; /* The struct of all the different themes. - Page: The .css page theme to be loaded (make sure they start with theme-)! @@ -28,9 +29,7 @@ const allThemes = { function setDatadogTheme(newTheme) { - if (newTheme === currentDatadogTheme) { - return; - } + if (newTheme === currentDatadogTheme) return; currentDatadogTheme = newTheme; const iframe = document.getElementById("datadog"); @@ -40,23 +39,35 @@ function setDatadogTheme(newTheme) { } function setWeatherTheme(newTheme) { - if (newTheme === currentWeatherTheme) { - return; - } + if (newTheme === currentWeatherTheme) return; currentWeatherTheme = newTheme; - const widget = document.getElementById("weather-image"); - widget.setAttribute("data-theme", newTheme); - - if (globalThis.weatherWidget) { - widget.innerHTML = widget.innerHTML; - weatherWidget.init(); - } + + const oldWidget = document.getElementById("weather-image"); + + const newWidget = document.createElement("a"); + newWidget.className = "weatherwidget-io"; + newWidget.id = "weather-image"; + newWidget.href = "https://forecast7.com/en/43d16n77d61/rochester/?unit=us"; + + newWidget.setAttribute("data-label_1", "ROCHESTER"); + newWidget.setAttribute("data-label_2", "WEATHER"); + newWidget.setAttribute("data-font", "Fira Sans"); + newWidget.setAttribute("data-icons", "Climacons Animated"); + newWidget.setAttribute("data-days", "100"); + newWidget.setAttribute("data-theme", newTheme); + + newWidget.textContent = "ROCHESTER WEATHER"; + oldWidget.replaceWith(newWidget); + + setTimeout(() => { + if (globalThis.__weatherwidget_init) { + globalThis.__weatherwidget_init(); + } + }, 100); } function setNewPageTheme(newTheme) { - if (newTheme === currentPageTheme) { - return; - } + if (newTheme === currentPageTheme) return; currentPageTheme = newTheme; document.body.classList.forEach(cls => { @@ -77,6 +88,9 @@ async function longUpdate() { let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); let bgImage = "url(../static/img/darkmodeF.png)"; + is_golden = mode_enabled; + mode_enabled = !mode_enabled; + if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { @@ -111,9 +125,6 @@ async function longUpdate() { const res = await fetch('/api/calendar', { method: 'GET', mode: 'cors' }); const data = await res.json(); $("#calendar").html(data.data); - - $("#datadog").attr('src', ddog_dashboard + Date().now()); - } catch (err) { console.log(err); } From a7f3ecb7c37a20e4b91482df09ca09b27b47eca6 Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 01:19:27 -0400 Subject: [PATCH 17/48] fix: formatted --- src/api/endpoints.py | 1 + src/core/cshcalendar.py | 2 +- src/core/wikithoughts.py | 20 +++++++++----------- src/main.py | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 28de1ea..af56e1d 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -17,6 +17,7 @@ ACCEPT_MESSAGE: str = "Posting right now :^)" DENY_MESSAGE: str = "Okay :( maybe next time" + @router.get("/api/calendar") async def get_calendar() -> JSONResponse: """ diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index a9bb7d9..8b44d53 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -316,7 +316,7 @@ async def get_future_events() -> list[CalendarInfo]: 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 + # 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() diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 3856464..81a8c49 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -219,16 +219,15 @@ def process_category_page(r_json: dict[str, str]) -> tuple[list[str], bool | str return (titles, False) - async def fetch_category_pages(response: httpx.Response) -> list[str]: """ - Loops through and gets the list of every page for Jumpstart Curated - - Args: - response (httpx.Response): The response to be converted and searched through + Loops through and gets the list of every page for Jumpstart Curated + + Args: + response (httpx.Response): The response to be converted and searched through - Returns: - list[str]: The list of titles to be fetched. + Returns: + list[str]: The list of titles to be fetched. """ params: dict[str, str] = { @@ -270,13 +269,12 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: if repeat_req not in (None, False, ""): params["cmcontinue"] = repeat_req - response = await client.get( - WIKI_API, params=params - ) + response = await client.get(WIKI_API, params=params) continue break return titles_found + async def refresh_category_pages() -> list[str]: """ Refreshes all pages of the category @@ -311,7 +309,7 @@ async def refresh_category_pages() -> list[str]: if response.status_code == 304: last_updated_time = time_now return page_title_cache - + elif response.status_code == 200: titles = await fetch_category_pages(response=response) else: diff --git a/src/main.py b/src/main.py index cd14e90..43bc793 100644 --- a/src/main.py +++ b/src/main.py @@ -30,7 +30,7 @@ async def lifespan(app: FastAPI): async with asyncio.TaskGroup() as tg: tg.create_task(cshcalendar.rebuild_calendar()) await wikithoughts.auth_bot() - + yield logger.info("Shutting down the Jumpstart application!") await cshcalendar.close_client() From 4c8e037fa2b959a9bf0f449c952dfea8a69c6907 Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 11:14:33 -0400 Subject: [PATCH 18/48] fix: Oops, It isin't 50th yet --- src/static/js/main.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index 6f22885..3181218 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -2,7 +2,6 @@ let currentPageTheme = ""; let currentDatadogTheme = ""; let currentWeatherTheme = ""; -let mode_enabled = false; /* The struct of all the different themes. - Page: The .css page theme to be loaded (make sure they start with theme-)! @@ -88,9 +87,6 @@ async function longUpdate() { let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); let bgImage = "url(../static/img/darkmodeF.png)"; - is_golden = mode_enabled; - mode_enabled = !mode_enabled; - if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { From 14b403e5b6dc9054acf9bf695e5317c4a111e3dd Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 11:25:13 -0400 Subject: [PATCH 19/48] fix: Javascript moment --- src/static/js/main.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index 3181218..4c9c694 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -57,12 +57,6 @@ function setWeatherTheme(newTheme) { newWidget.textContent = "ROCHESTER WEATHER"; oldWidget.replaceWith(newWidget); - - setTimeout(() => { - if (globalThis.__weatherwidget_init) { - globalThis.__weatherwidget_init(); - } - }, 100); } function setNewPageTheme(newTheme) { @@ -144,12 +138,15 @@ async function mediumUpdate() { -mediumUpdate(); -longUpdate(); +document.addEventListener("DOMContentLoaded", () => { + mediumUpdate(); + longUpdate(); + + setInterval(longUpdate, 60000); + setInterval(mediumUpdate, 22000); -setInterval(longUpdate, 60000); -setInterval(mediumUpdate, 22000); -setInterval(() => { - if (globalThis.__weatherwidget_init) - globalThis.__weatherwidget_init(); -}, 1800000); \ No newline at end of file + setInterval(() => { + if (globalThis.__weatherwidget_init) + globalThis.__weatherwidget_init(); + }, 1800000); +}); From bc670369f86b922733da27026982178aba9d81d3 Mon Sep 17 00:00:00 2001 From: Weather Date: Thu, 2 Apr 2026 11:28:15 -0400 Subject: [PATCH 20/48] fix: no --- src/static/js/main.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index 725a474..2c5a8c9 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -87,8 +87,6 @@ async function longUpdate() { bgImage = "url(../static/img/jumpstartbang.png)"; } else if (is_golden) { bgImage = "url(../static/img/goldenmode.png)"; - } else if (is_golden) { - bgImage = "url(../static/img/goldenmode.png)"; } else if (month === 10 && [29, 30, 31].includes(day)) { bgImage = "url(../static/img/spookymode.png)"; } else if (month === 11 && day === 2) { From 60e0dde0dcf4d1e829fdacda666a0349b0583f05 Mon Sep 17 00:00:00 2001 From: Weather Date: Fri, 3 Apr 2026 01:25:26 -0400 Subject: [PATCH 21/48] docs: Themes in /docs --- .../core/{CSH Calendar.md => csh_calendar.md} | 2 -- docs/core/{Slack.md => slack.md} | 2 -- .../{Wiki-thoughts.md => wikithoughts.md} | 0 .../{calendar_endpoint.md => csh_calendar.md} | 0 docs/endpoints/{slack_bot.md => slack.md} | 2 +- docs/getting-started/setup.md | 0 docs/themes/how_to.md | 22 ++++++++++++++ docs/themes/overview.md | 30 +++++++++++++++++++ mkdocs.yml | 16 +++++----- src/core/wikithoughts.py | 12 ++++---- src/static/js/main.js | 16 ++++------ 11 files changed, 73 insertions(+), 29 deletions(-) rename docs/core/{CSH Calendar.md => csh_calendar.md} (96%) rename docs/core/{Slack.md => slack.md} (94%) rename docs/core/{Wiki-thoughts.md => wikithoughts.md} (100%) rename docs/endpoints/{calendar_endpoint.md => csh_calendar.md} (100%) rename docs/endpoints/{slack_bot.md => slack.md} (65%) delete mode 100644 docs/getting-started/setup.md create mode 100644 docs/themes/how_to.md create mode 100644 docs/themes/overview.md diff --git a/docs/core/CSH Calendar.md b/docs/core/csh_calendar.md similarity index 96% rename from docs/core/CSH Calendar.md rename to docs/core/csh_calendar.md index 986b1d9..247d129 100644 --- a/docs/core/CSH Calendar.md +++ b/docs/core/csh_calendar.md @@ -1,5 +1,3 @@ -## Overview - This core component is used for all the fetching, formatting and structuring for the CSH calendar portion of Jumpstart. --- diff --git a/docs/core/Slack.md b/docs/core/slack.md similarity index 94% rename from docs/core/Slack.md rename to docs/core/slack.md index f2f2e20..77ea4de 100644 --- a/docs/core/Slack.md +++ b/docs/core/slack.md @@ -1,5 +1,3 @@ -## Overview - This component handles the Slack Bot and it's related functions. Such as responding to announcments, requesting to upload to Jumpstart, and the other handles with Slack. --- diff --git a/docs/core/Wiki-thoughts.md b/docs/core/wikithoughts.md similarity index 100% rename from docs/core/Wiki-thoughts.md rename to docs/core/wikithoughts.md diff --git a/docs/endpoints/calendar_endpoint.md b/docs/endpoints/csh_calendar.md similarity index 100% rename from docs/endpoints/calendar_endpoint.md rename to docs/endpoints/csh_calendar.md diff --git a/docs/endpoints/slack_bot.md b/docs/endpoints/slack.md similarity index 65% rename from docs/endpoints/slack_bot.md rename to docs/endpoints/slack.md index 5766bbc..aa6a463 100644 --- a/docs/endpoints/slack_bot.md +++ b/docs/endpoints/slack.md @@ -1,4 +1,4 @@ - +::: api.endpoints.get_announcement ::: api.endpoints.slack_events ::: api.endpoints.message_actions \ No newline at end of file diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/themes/how_to.md b/docs/themes/how_to.md new file mode 100644 index 0000000..a3903f3 --- /dev/null +++ b/docs/themes/how_to.md @@ -0,0 +1,22 @@ +Jumpstart has many different themes displayed on the website, this is a step by step process on how to implement your own! + +**Any images that need to be added should be put in src/static/img** + +### Adding Background + +1. Go to the file: **src/static/js/main.js** +2. In the function long update, figure out the day, month, and hour of your theme. + - hour is in 24H format +3. Add it into the if statement (please change this in the future) + - Make sure it follows the bgImage = "url(../static/img/{**YOUR FILE HERE**})" + +### Adding CSS Theme +1. Go to the file **src/static/css/style.css** +2. Add a new class for the colors **MAKE SURE IT STARTS WITH {theme-}!!!** +3. Change the colors in this new class +4. Repeat the steps in the "Adding Background" +5. Add a new index into "allThemes" with your css theme, along with any changes to weatherwidget or datadog +6. change the themeToLoad in the if statement to load your new index + + + diff --git a/docs/themes/overview.md b/docs/themes/overview.md new file mode 100644 index 0000000..8cf5595 --- /dev/null +++ b/docs/themes/overview.md @@ -0,0 +1,30 @@ +Jumpstart has many different themes displayed on the website. Ranging from the light - dark mode, to full background and color shifts fo events. + +### All Themes +- Light Mode + - Default theme between 9 AM and 6 PM +- Dark Mode + - Default theme between 6 PM and 9 AM +- Valentine's Day + - Feburary 12th, 13th and 14th + - Changes title to "Constantly Smooching House" + - Adds decorative hearts around the logo +- BANG! + - March 13th + - Changes title to "BANG! Science" + - Gives an orange gradient to the background +- CSH 50th Anniversary + - April 9th, 10th, 11th, and 12 + - Replaces title with the CSH 50th Anniversary logo + - Replaces color scheme with a Golden / Black theme +- Halloween + - October 29th, 30th, and 31st + - Changes title to "Computer Spooky House" + - Logo is given orange and purple colors +- Duck! + - November 2nd + - Duck +- Winter + - November and December + - Changes title to "Christmas Season" + - Gives a red and green logo with a bright blue background diff --git a/mkdocs.yml b/mkdocs.yml index 234a459..1b7673e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,14 +39,16 @@ nav: - Home: index.md - Getting Started: getting-started/getting-started.md - Backend: - - Calendar: core/CSH Calendar.md - - Slack Bot: core/Slack.md - - Wiki Thoughts: core/Wiki-thoughts.md + - Calendar: core/csh_calendar.md + - Slack: core/slack.md + - Wikithoughts: core/wikithoughts.md - Endpoints: - - Calendar: endpoints/calendar_endpoint.md - - Announcements: endpoints/announcements.md - - Slack Bot: endpoints/slack_bot.md - - Wiki Thoughts: endpoints/wikithoughts.md + - Calendar: endpoints/csh_calendar.md + - Slack: endpoints/slack.md + - Wikithoughts: endpoints/wikithoughts.md + - Themes: + - Themes: themes/overview.md + - Adding Your Own: themes/how_to.md plugins: - search diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 81a8c49..bdf196f 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -38,9 +38,10 @@ # Precompile all the Regex operations -RE_LINK: Pattern[str] = re.compile(r'\[https?://[^\s"]+\s+"?([^\]"]+)"?\]') -RE_FILE: Pattern[str] = re.compile(r"\[\[File:[^\]]*\]\]", re.IGNORECASE) -RE_IMAGE: Pattern[str] = re.compile(r"\[\[Image:[^\]]*\]\]", re.IGNORECASE) +RE_LINK: Pattern[str] = re.compile( + r'\[https?://[^\s"]+\s+"?([^\]"]+)"?\]' +) # Https Links +RE_FILE_IMAGE = re.compile(r"\[\[(?:File|Image):[^\[\]]*\]\]", re.IGNORECASE) RE_PAGE_TEXT: Pattern[str] = re.compile(r"\[\[[^\|\]]*\|([^\]]+)\]\]") RE_PAGE: Pattern[str] = re.compile(r"\[\[([^\]]+)\]\]") RE_CSH: Pattern[str] = re.compile(r"\^\^([^^]+)\^\^") @@ -61,11 +62,10 @@ def clean_wikitext(text: str) -> str: """ reg_operations: tuple[Pattern[str]] = ( + RE_PAGE, + RE_FILE_IMAGE, RE_LINK, - RE_FILE, - RE_IMAGE, RE_PAGE_TEXT, - RE_PAGE, RE_CSH, RE_TEMPLATE, RE_HTML, diff --git a/src/static/js/main.js b/src/static/js/main.js index 2c5a8c9..8a21dfa 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -78,14 +78,15 @@ async function longUpdate() { const day = date.getDate(); const isDay = (hour > 9 && hour < 18); - let is_golden = (month === 4 && [9, 10, 11, 12].includes(day)); - let bgImage = "url(../static/img/darkmodeF.png)"; + let bgImage = "url(../static/img/darkmodeF.png)"; + let themeToLoad = "dark"; if (month === 2 && [12, 13, 14].includes(day)) { bgImage = "url(../static/img/valentinemode.png)"; } else if (month === 3 && day === 13) { bgImage = "url(../static/img/jumpstartbang.png)"; - } else if (is_golden) { + } else if (month === 4 && [9, 10, 11, 12].includes(day)) { + themeToLoad = "golden"; bgImage = "url(../static/img/goldenmode.png)"; } else if (month === 10 && [29, 30, 31].includes(day)) { bgImage = "url(../static/img/spookymode.png)"; @@ -94,19 +95,12 @@ async function longUpdate() { } else if ([11, 12].includes(month)) { bgImage = "url(../static/img/wintermode.png)"; } else if (isDay) { + themeToLoad = "light"; bgImage = "url(../static/img/lightmodeF.png)"; } $("body").css("background-image", bgImage); try { - - let themeToLoad = "dark"; - if (is_golden){ - themeToLoad = "golden"; - } else if (isDay) { - themeToLoad = "light"; - } - setNewPageTheme(allThemes[themeToLoad].page); setDatadogTheme(allThemes[themeToLoad].datadog); setWeatherTheme(allThemes[themeToLoad].weather); From 107f1e7e7250c150b951f06e3112186f4ba9fe3a Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Fri, 3 Apr 2026 01:40:54 -0400 Subject: [PATCH 22/48] Add tests to sonarqube workflow, add documentation to readme, add guards to slack --- .github/workflows/sonarqube.yml | 12 +++++++++++- README.md | 22 ++++++++++++++++++++-- sonar-project.properties | 2 ++ src/config.py | 20 ++++++++++++-------- src/core/slack.py | 14 +++++++++----- src/main.py | 1 - 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 7d77948..4a88f0a 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -20,7 +20,17 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.13' + python-version: '3.14' + + - name: Install dependencies + run: | + pip install uv + uv pip install -r src/requirements.txt --system + uv pip install -r tests/requirements.txt --system + + - name: Run tests with coverage + run: | + pytest -v - uses: SonarSource/sonarqube-scan-action@v6 env: diff --git a/README.md b/README.md index d25e5a2..c8347b6 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,27 @@ Jumpstart also has support for Docker Compose, a extended version of docker that ``` ## Development + +### Setup 1. Install uv on your system if not already on it (this just makes it easy) -2. Run: `uv venv .venv --python 3.14` +2. Run: `uv venv .venv` 3. Activate the virtual environment -4. Run: `uv pip install -r dev-requirements.txt`, `uv pip install -r src/requirements.txt`, `uv pip install -r tests/requirements,txt`, and `uv pip install -r docs/requirements.txt` + * Bash: `source .venv/bin/activate` + * Fish: `source .venv/bin/activate.fish` + * Windows: `.venv\Scripts\activate` + * Other: Good luck! +4. Run: + * `uv pip install -r dev-requirements.txt` + * `uv pip install -r src/requirements.txt` + * `uv pip install -r tests/requirements,txt` + * `uv pip install -r docs/requirements.txt` 5. Run: `pre-commit install` 6. You're all set! + +### Testing + +We're using the pytest framework to create tests. A good minimum coverage requirement is about <=90%. + +To run the tests just run: `pytest` + +`coverage.xml` and `htmlcov` should be generated. `coverage.xml` is used for Sonarqube, while `htmlcov` is a local html view into code coverage. The easiest way to view the coverage site is to enter the directory and run: `python -m http.server` and visit the site! diff --git a/sonar-project.properties b/sonar-project.properties index 7f80d2e..e2bc34b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1 +1,3 @@ sonar.projectKey=jumpstart-v2 +sonar.python.coverage.reportPaths=coverage.xml +sonar.coverage.exclusions=**/tests/**,**/*test*.py,**/test_*.py diff --git a/src/config.py b/src/config.py index 9dd7e22..aacf3a9 100644 --- a/src/config.py +++ b/src/config.py @@ -20,16 +20,20 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: str | None: The value of the environment variable, or the default value if it is not set. """ - value: str = os.getenv(name, default) - - if value in (None, ""): - logger.warning( - f"Environment variable '{name}' is not set, using default value: '{default if default is not None else 'None'}'" - ) + try: + value: str = os.getenv(name, default) + + if value in (None, ""): + logger.warning( + f"Environment variable '{name}' is not set, using default value: '{default if default is not None else 'None'}'" + ) + return default + + return value + except Exception as e: + logger.error(f"Error retrieving environment variable '{name}': {e}") return default - return value - BASE_DIR: str = os.path.dirname(os.path.abspath(__file__)) diff --git a/src/core/slack.py b/src/core/slack.py index 4e7ef20..a3dfc01 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -44,17 +44,20 @@ async def gather_emojis() -> dict: dict: A mapping of emoji names to their URLs. """ - logger.info("Gathering emojis from slack!") + emojis: dict = {} try: + if client is None: + raise ValueError("Slack client is not initialized") + emoji_request: dict = await client.emoji_list() assert emoji_request.get("ok", False) - return emoji_request.get("emoji", {}) + emojis = emoji_request.get("emoji", {}) except Exception as e: logger.error(f"Error gathering emojis: {e}") - return {} + return emojis async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: @@ -66,9 +69,10 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: announcement_text (str): The text of the announcement to be posted. """ - logger.info("Requesting upload announcement permission!") - try: + if client is None: + raise ValueError("Slack client is not initialized") + message: dict = copy.deepcopy(SLACK_DM_TEMPLATE) message[0]["text"]["text"] += announcement_text diff --git a/src/main.py b/src/main.py index 43b7f13..fc797aa 100644 --- a/src/main.py +++ b/src/main.py @@ -63,7 +63,6 @@ async def docs_redirect(): logger.warning("Documentation directory not found, skipping documentation setup!") logger.info("Importing API endpoints!") -# app.include_router(endpoints.router, prefix="/api") app.include_router(endpoints.router) logger.info("Finished setting up the application!") From f5e565ca9abe88db2604409a6f7cedb980a55fcd Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Fri, 3 Apr 2026 01:53:57 -0400 Subject: [PATCH 23/48] Migrate if elif to match in wikithoughts and actually declare failed_authentication_attempts --- src/core/wikithoughts.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 1721124..9ab191c 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -266,6 +266,8 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: titles_found: list[str] = [] + failed_authentication_attempts: int = 0 + while True: r_json: dict[str, str] = response.json() @@ -339,15 +341,16 @@ async def refresh_category_pages() -> list[str]: WIKI_API, params=params, headers=headers ) - if response.status_code == 304: - last_updated_time = time_now - return page_title_cache - - elif response.status_code == 200: - titles = await fetch_category_pages(response=response) - else: - logger.warning("Failed to update the CSH wiki page!") - return page_title_cache + match response.status_code: + case 304: + logger.info("Category pages not updated, refreshing last update!") + last_updated_time = time_now + return page_title_cache + case 200: + titles = await fetch_category_pages(response=response) + case _: + logger.warning("Failed to update the wiki category pages!") + return page_title_cache last_updated_time = time_now page_title_cache = titles From dc9cd4c4350b4a18385910577ca0bc7db3c084d6 Mon Sep 17 00:00:00 2001 From: Weather Date: Sun, 5 Apr 2026 23:45:05 -0400 Subject: [PATCH 24/48] fix: Endpoints and Slackbot --- src/api/endpoints.py | 6 +++--- src/core/cshcalendar.py | 4 ++-- src/core/slack.py | 1 + src/main.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 10622be..34b10b1 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -16,7 +16,7 @@ DENY_MESSAGE: str = "Okay :( maybe next time" -@router.get("/api/calendar") +@router.get("/calendar") async def get_calendar() -> JSONResponse: """ Returns calendar data. @@ -39,7 +39,7 @@ async def get_calendar() -> JSONResponse: return JSONResponse(events) -@router.get("/api/announcement") +@router.get("/announcement") def get_announcement() -> JSONResponse: """ Returns announcement data. @@ -150,7 +150,7 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: return JSONResponse({"status": "success"}, status_code=200) -@router.get("/api/wikithought") +@router.get("/wikithought") async def wikithought() -> JSONResponse: """ Returns a random CSH wiki thought from the MediaWiki API. diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 146e182..beedb0c 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -207,6 +207,8 @@ async def rebuild_calendar() -> None: """ global calendar_cache, cal_last_update, cal_constructed_event + + current_time: datetime = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) try: cal_constructed_event.clear() found_events: set[CalendarInfo] = set() @@ -215,8 +217,6 @@ async def rebuild_calendar() -> None: cal: Calendar = Calendar.from_ical(response.content) - current_time: datetime = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) - fetched_daily_events: list[Event] = recurring_ical_events.of(cal).between( current_time, current_time + timedelta(days=CALENDAR_OUTLOOK_DAYS) ) diff --git a/src/core/slack.py b/src/core/slack.py index a3dfc01..77a16f5 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -11,6 +11,7 @@ logger: Logger = getLogger(__name__) + client: AsyncWebClient | None = None try: client = AsyncWebClient(token=SLACK_API_TOKEN) diff --git a/src/main.py b/src/main.py index de97553..0fd0da7 100644 --- a/src/main.py +++ b/src/main.py @@ -65,7 +65,7 @@ async def docs_redirect(): logger.warning("Documentation directory not found, skipping documentation setup!") logger.info("Importing API endpoints!") -app.include_router(endpoints.router) +app.include_router(endpoints.router, prefix="/api") logger.info("Finished setting up the application!") From 001182f3289ef05d52949346a3db92a202637e94 Mon Sep 17 00:00:00 2001 From: Weather Date: Sun, 5 Apr 2026 23:48:35 -0400 Subject: [PATCH 25/48] fix: Slackbot not registering --- src/static/slack/dm_request_template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/slack/dm_request_template.json b/src/static/slack/dm_request_template.json index dc10391..a3e7554 100644 --- a/src/static/slack/dm_request_template.json +++ b/src/static/slack/dm_request_template.json @@ -14,7 +14,7 @@ "text": {"type": "plain_text", "text": "Yes"}, "style": "primary", "action_id": "yes_j", - "value": null + "value": "" }, { "type": "button", From 83255d2267a4050caa4311714a351e55f531f436 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 00:09:03 -0400 Subject: [PATCH 26/48] fix: Slack attempts to open a dm channel --- src/core/slack.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/slack.py b/src/core/slack.py index 77a16f5..fc532e3 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -5,6 +5,7 @@ from logging import getLogger, Logger from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.slack_response import SlackResponse from config import SLACK_API_TOKEN, SLACK_JUMPSTART_MESSAGE, SLACK_DM_TEMPLATE @@ -84,6 +85,13 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: } ) + dm_channel: SlackResponse = await client.conversations_open(users=user_id) + channel_id: str | None = dm_channel.get("channel", {}).get("id", None) + + if channel_id is None: + logger.warning(f"Unable to open dm with User {user_id}") + return + await client.chat_postMessage( channel=user_id, text=SLACK_JUMPSTART_MESSAGE, blocks=message ) From e7577bdcbe77ae29676e6a327170f26a47c99be3 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 00:16:20 -0400 Subject: [PATCH 27/48] fix: adds pytest channel --- src/core/slack.py | 1 + src/requirements.txt | 3 --- tests/src/core/test_slack.py | 7 +++++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/slack.py b/src/core/slack.py index fc532e3..8a9e2f4 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -6,6 +6,7 @@ from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web.slack_response import SlackResponse +from slack_sdk.errors import SlackApiError from config import SLACK_API_TOKEN, SLACK_JUMPSTART_MESSAGE, SLACK_DM_TEMPLATE diff --git a/src/requirements.txt b/src/requirements.txt index b345008..305684d 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -19,9 +19,6 @@ icalendar==7.0.3 recurring-ical-events==3.8.1 arrow==1.4.0 -# For Wikithoughts -httpx==0.28.1 - # For the docker to run uvicorn==0.41.0 pyyaml==6.0.3 diff --git a/tests/src/core/test_slack.py b/tests/src/core/test_slack.py index d410aff..8fbbbbe 100644 --- a/tests/src/core/test_slack.py +++ b/tests/src/core/test_slack.py @@ -99,6 +99,13 @@ def test_request_upload_via_dm_success_and_exception(monkeypatch): recorded = {} class FakeClient: + async def conversations_open(self, *, users): + return { + "ok": True, + "channel": { + "id": "FAKE_CHANNEL" + } + } async def chat_postMessage(self, *, channel, text, blocks): recorded["channel"] = channel recorded["text"] = text From eb7dd5fa621e2ef91069ecebd20f20b6845193e5 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 00:23:37 -0400 Subject: [PATCH 28/48] fix:Adds SlackAPIError --- src/core/slack.py | 3 +++ tests/src/core/test_slack.py | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/slack.py b/src/core/slack.py index 8a9e2f4..36b9cdd 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -96,6 +96,9 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: await client.chat_postMessage( channel=user_id, text=SLACK_JUMPSTART_MESSAGE, blocks=message ) + except SlackApiError as e: + logger.error(f"Slack Error: {e.response['error']}\n") + logger.error(f"Full Slack Error: {e.response}") except Exception as e: logger.error(f"Error messaging user {user_id}: {e}") diff --git a/tests/src/core/test_slack.py b/tests/src/core/test_slack.py index 8fbbbbe..e6c0361 100644 --- a/tests/src/core/test_slack.py +++ b/tests/src/core/test_slack.py @@ -100,12 +100,8 @@ def test_request_upload_via_dm_success_and_exception(monkeypatch): class FakeClient: async def conversations_open(self, *, users): - return { - "ok": True, - "channel": { - "id": "FAKE_CHANNEL" - } - } + return {"ok": True, "channel": {"id": "FAKE_CHANNEL"}} + async def chat_postMessage(self, *, channel, text, blocks): recorded["channel"] = channel recorded["text"] = text From 90a9f00cca171e0eec2da97791d64a7e5a0ee31a Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 00:35:27 -0400 Subject: [PATCH 29/48] docs: Error traces --- src/core/slack.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/slack.py b/src/core/slack.py index 36b9cdd..facea81 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -101,6 +101,9 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: logger.error(f"Full Slack Error: {e.response}") except Exception as e: logger.error(f"Error messaging user {user_id}: {e}") + logger.error(f"Exception type: {type(e)}") + logger.error(f"Exception repr: {repr(e)}") + logger.exception("Full stack trace:") def convert_user_response_to_bool(message_data: dict) -> bool: From 23619d344c7365360cf02a88ac2473c5702790f3 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 00:56:12 -0400 Subject: [PATCH 30/48] Message --- src/core/slack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/slack.py b/src/core/slack.py index facea81..d508374 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -78,6 +78,8 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: message: dict = copy.deepcopy(SLACK_DM_TEMPLATE) + print(message) + message[0]["text"]["text"] += announcement_text message[1]["elements"][0]["value"] = json.dumps( { From 6bbcd6c595ac90d26627de51acd05aedf7a6fe86 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 01:03:35 -0400 Subject: [PATCH 31/48] fix: removes dict conversion from slack DM template --- src/config.py | 2 +- src/core/slack.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/config.py b/src/config.py index aacf3a9..3e3eb81 100644 --- a/src/config.py +++ b/src/config.py @@ -56,4 +56,4 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: WIKI_CATEGORY: str = _get_env_variable("WIKI_CATEGORY", "JobAdvice") with open(os.path.join(BASE_DIR, "static", "slack", "dm_request_template.json")) as f: - SLACK_DM_TEMPLATE = dict(json.load(f)) + SLACK_DM_TEMPLATE = json.load(f) diff --git a/src/core/slack.py b/src/core/slack.py index d508374..36b9cdd 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -78,8 +78,6 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: message: dict = copy.deepcopy(SLACK_DM_TEMPLATE) - print(message) - message[0]["text"]["text"] += announcement_text message[1]["elements"][0]["value"] = json.dumps( { @@ -103,9 +101,6 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: logger.error(f"Full Slack Error: {e.response}") except Exception as e: logger.error(f"Error messaging user {user_id}: {e}") - logger.error(f"Exception type: {type(e)}") - logger.error(f"Exception repr: {repr(e)}") - logger.exception("Full stack trace:") def convert_user_response_to_bool(message_data: dict) -> bool: From 2c61edd5977500988da8d7245f58684c2b2dd590 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 01:05:27 -0400 Subject: [PATCH 32/48] fix: pytest slack dm template is list --- tests/src/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/test_config.py b/tests/src/test_config.py index 49c16e0..767b36f 100644 --- a/tests/src/test_config.py +++ b/tests/src/test_config.py @@ -55,4 +55,4 @@ def test_with_slack_token_parses_values(monkeypatch) -> None: assert cfg.CALENDAR_OUTLOOK_DAYS == 5 assert cfg.CALENDAR_EVENT_MAXIMUM == 20 assert cfg.CALENDAR_TIMEZONE == "UTC" - assert isinstance(cfg.SLACK_DM_TEMPLATE, dict) + assert isinstance(cfg.SLACK_DM_TEMPLATE, list) From 5bbbb7be97629324da69b97139b43a77d3b2f5b9 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 01:10:42 -0400 Subject: [PATCH 33/48] fix: reverts opening dm channel --- src/core/slack.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/core/slack.py b/src/core/slack.py index 36b9cdd..3f0a6ee 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -85,14 +85,7 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: "user": user_id, } ) - - dm_channel: SlackResponse = await client.conversations_open(users=user_id) - channel_id: str | None = dm_channel.get("channel", {}).get("id", None) - - if channel_id is None: - logger.warning(f"Unable to open dm with User {user_id}") - return - + await client.chat_postMessage( channel=user_id, text=SLACK_JUMPSTART_MESSAGE, blocks=message ) From 020e01f3174bdb94dd8005542126378798548b05 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 02:13:02 -0400 Subject: [PATCH 34/48] fix: styling for wikithoughts --- src/core/slack.py | 2 +- src/core/wikithoughts.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/slack.py b/src/core/slack.py index 3f0a6ee..885d53a 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -85,7 +85,7 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: "user": user_id, } ) - + await client.chat_postMessage( channel=user_id, text=SLACK_JUMPSTART_MESSAGE, blocks=message ) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 3a14c51..ea430c1 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -46,10 +46,9 @@ # Precompile all the Regex operations -RE_LINK: Pattern[str] = re.compile( - r'\[https?://[^\s"]+\s+"?([^\]"]+)"?\]' -) # Https Links -RE_FILE_IMAGE = re.compile(r"\[\[(?:File|Image):[^\[\]]*\]\]", re.IGNORECASE) +RE_LINK: Pattern[str] = re.compile(r'\[https?://[^\s"]+\s+"?([^\]"]+)"?\]') +RE_FILE: Pattern[str] = re.compile(r"\[\[File:[^\]]*\]\]", re.IGNORECASE) +RE_IMAGE: Pattern[str] = re.compile(r"\[\[Image:[^\]]*\]\]", re.IGNORECASE) RE_PAGE_TEXT: Pattern[str] = re.compile(r"\[\[[^\|\]]*\|([^\]]+)\]\]") RE_PAGE: Pattern[str] = re.compile(r"\[\[([^\]]+)\]\]") RE_CSH: Pattern[str] = re.compile(r"\^\^([^^]+)\^\^") @@ -70,10 +69,11 @@ def clean_wikitext(text: str) -> str: """ reg_operations: tuple[Pattern[str]] = ( - RE_PAGE, - RE_FILE_IMAGE, + RE_FILE, + RE_IMAGE, RE_LINK, RE_PAGE_TEXT, + RE_PAGE, RE_CSH, RE_TEMPLATE, RE_HTML, From 5ecf972d8bf9b412ebf3ff04a08672cb914b5de3 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 17:38:15 -0400 Subject: [PATCH 35/48] fix: Logs error --- src/core/wikithoughts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index ea430c1..0d9eb59 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -160,7 +160,8 @@ async def auth_bot() -> None: logger.info("Bot was authenticated successfully!") else: bot_authenticated = False - logger.warning("Bot was unable to authenticate!") + logger.warning(f"Bot was unable to authenticate! Response: {returned_json}") + def headers_formatting( @@ -287,6 +288,8 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: logger.warning( f"Failed to reauthenticate the bot! Attempt: {failed_authentication_attempts}" ) + else: + logger.info("Bot was able to re-auth during runtime!") failed_authentication_attempts += 1 continue From a24ee0cf0de11eb2c81ea8d0c275e8cf2ae51a78 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 21:02:24 -0400 Subject: [PATCH 36/48] feat: Slack announcments displays username --- src/api/endpoints.py | 5 +++-- src/core/slack.py | 43 +++++++++++++++++++++++++++++++++++----- src/core/wikithoughts.py | 1 - src/static/js/main.js | 3 ++- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 34b10b1..b0f0421 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -48,7 +48,7 @@ def get_announcement() -> JSONResponse: JSONResponse: A JSON response containing the announcement data. """ - return JSONResponse({"data": slack.get_announcement()}) + return JSONResponse(slack.get_announcement()) @router.post("/slack/events") @@ -127,7 +127,8 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: form_json.get("actions", [{}])[0].get("value", '{text:""}') ).get("text", None) - slack.add_announcement(message_object) + user_id = form_json.get("user", {}).get("id") + slack.add_announcement(message_object, user_id) if response_url: async with httpx.AsyncClient() as client: diff --git a/src/core/slack.py b/src/core/slack.py index 885d53a..e85070e 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -15,12 +15,15 @@ client: AsyncWebClient | None = None + try: client = AsyncWebClient(token=SLACK_API_TOKEN) except Exception as e: logger.error(f"Failed to initialize Slack client: {e}") -announcements: list[str] = ["Welcome to Jumpstart!"] +announcements: list[dict[str, str]] = [ + {"content": "Welcome to Jumpstart!", "user": "Jumpstart"} +] def clean_text(raw: str) -> str: @@ -63,6 +66,31 @@ async def gather_emojis() -> dict: return emojis +async def get_username(user_id: str) -> str: + """ + Attempts to retrieve a slack username relating to a user id + + Args: + user_id (str): The ID of the user to retrieve + + Returns: + str: The username, or an empty string if not applicable + """ + + response = await client.users_info(user=user_id) + user = response.get("user", None) + + if user is None: + logger.warning(f"Unable to find a user under the id {user_id}") + return "Unknown" + + display_name = user.get("profile", {}).get("display_name", None) + real_name = user.get("real_name", None) + username = user.get("name", None) + + return real_name or display_name or username or "Unknown" + + async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: """ Sends a DM to the user with the announcement text and a prompt to post it to Jumpstart. @@ -119,12 +147,12 @@ def convert_user_response_to_bool(message_data: dict) -> bool: return user_response -def get_announcement() -> str | None: +def get_announcement() -> dict[str, str] | None: """ Returns the next announcement in the queue. Returns: - str | None: The next announcement text, or None if there are no announcements. + dict | None: The next announcement text and user, or None if there are no announcements. """ if len(announcements) == 0: @@ -136,16 +164,21 @@ def get_announcement() -> str | None: return announcements.pop(0) -def add_announcement(announcement_text: str) -> None: +def add_announcement(announcement_text: str, user_id: str) -> None: """ Adds an announcement to the queue. Args: announcement_text (str): The text of the announcement to be added. + user_id (str): The user_id of the person """ if announcement_text is None or announcement_text.strip() == "": logger.warning("Attempted to add empty announcement, skipping!") return - announcements.append(announcement_text) + username: str = get_username(user_id=user_id) + + new_addition: dict[str, str] = {"content": announcement_text, "user": username} + + announcements.append(new_addition) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 0d9eb59..c0110ab 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -163,7 +163,6 @@ async def auth_bot() -> None: logger.warning(f"Bot was unable to authenticate! Response: {returned_json}") - def headers_formatting( new_etag: str | None = None, new_last_modified: str | None = None ) -> dict[str, str]: diff --git a/src/static/js/main.js b/src/static/js/main.js index 8c76e5e..c7e0af4 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -123,7 +123,8 @@ async function mediumUpdate() { const announcementData = await announcementRes.json(); $("#wikipageheader").text(wikiData.page + " - csh/Wikithoughts") $("#wikipagetext").text(wikiData.content); - $("#announcement").text(announcementData.data.substring(0, 910)); + $("#announcement").text(announcementData.content.substring(0, 910)); + $("#announcements-text-header").text("Announcements - " + announcementData.user) } catch (err) { console.log(err); } From 156b3931dbb2b4d35fba68f65565580b7bac8e3f Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 21:08:46 -0400 Subject: [PATCH 37/48] fix: await --- src/api/endpoints.py | 2 +- src/core/slack.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index b0f0421..5a613c7 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -128,7 +128,7 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: ).get("text", None) user_id = form_json.get("user", {}).get("id") - slack.add_announcement(message_object, user_id) + await slack.add_announcement(message_object, user_id) if response_url: async with httpx.AsyncClient() as client: diff --git a/src/core/slack.py b/src/core/slack.py index e85070e..c2a7c23 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -164,7 +164,7 @@ def get_announcement() -> dict[str, str] | None: return announcements.pop(0) -def add_announcement(announcement_text: str, user_id: str) -> None: +async def add_announcement(announcement_text: str, user_id: str) -> None: """ Adds an announcement to the queue. @@ -177,7 +177,7 @@ def add_announcement(announcement_text: str, user_id: str) -> None: logger.warning("Attempted to add empty announcement, skipping!") return - username: str = get_username(user_id=user_id) + username: str = await get_username(user_id=user_id) new_addition: dict[str, str] = {"content": announcement_text, "user": username} From 1d0613d6cc4aed955e6c809d8dc068d77d896635 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 21:22:02 -0400 Subject: [PATCH 38/48] fix: Correct Javascript ID --- src/static/js/main.js | 2 +- src/templates/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index c7e0af4..a462c7d 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -124,7 +124,7 @@ async function mediumUpdate() { $("#wikipageheader").text(wikiData.page + " - csh/Wikithoughts") $("#wikipagetext").text(wikiData.content); $("#announcement").text(announcementData.content.substring(0, 910)); - $("#announcements-text-header").text("Announcements - " + announcementData.user) + $("#announcements-header").text("Announcements - " + announcementData.user) } catch (err) { console.log(err); } diff --git a/src/templates/index.html b/src/templates/index.html index 39a7810..ea2cdab 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -19,7 +19,7 @@
- Announcements + Announcements
From 51c55b6d2988157d2beb7cb9e0d7f50553a1d3b4 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 21:23:14 -0400 Subject: [PATCH 39/48] oops --- src/static/js/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/main.js b/src/static/js/main.js index a462c7d..9039f7c 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -124,7 +124,7 @@ async function mediumUpdate() { $("#wikipageheader").text(wikiData.page + " - csh/Wikithoughts") $("#wikipagetext").text(wikiData.content); $("#announcement").text(announcementData.content.substring(0, 910)); - $("#announcements-header").text("Announcements - " + announcementData.user) + $("#announcement-header").text("Announcements - " + announcementData.user) } catch (err) { console.log(err); } From 5e90e67aaf38e5e14f98a1f5a704b081ea598793 Mon Sep 17 00:00:00 2001 From: Weather Date: Mon, 6 Apr 2026 22:02:52 -0400 Subject: [PATCH 40/48] fix: Bot properly re-auths during runtime --- src/core/wikithoughts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index c0110ab..1a3e715 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -287,10 +287,12 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: logger.warning( f"Failed to reauthenticate the bot! Attempt: {failed_authentication_attempts}" ) + await asyncio.sleep(3) else: logger.info("Bot was able to re-auth during runtime!") failed_authentication_attempts += 1 + response = await client.get(WIKI_API, params=params) continue added, repeat_req = process_category_page(r_json) From 362860f236dc9a13c5c9b48b5e6fec80f5de7ab0 Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 7 Apr 2026 14:29:55 -0400 Subject: [PATCH 41/48] feat: Character limits usernames --- src/core/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/slack.py b/src/core/slack.py index c2a7c23..a5fda15 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -178,7 +178,7 @@ async def add_announcement(announcement_text: str, user_id: str) -> None: return username: str = await get_username(user_id=user_id) - + username = username[:40] new_addition: dict[str, str] = {"content": announcement_text, "user": username} announcements.append(new_addition) From 83848592842f118985529551c4c9d0347e26570e Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 7 Apr 2026 14:38:30 -0400 Subject: [PATCH 42/48] tests: Updated tests for new slack --- src/api/endpoints.py | 6 +++++- src/core/slack.py | 4 +--- tests/src/core/test_slack.py | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 5a613c7..9a74d7f 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -128,7 +128,11 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: ).get("text", None) user_id = form_json.get("user", {}).get("id") - await slack.add_announcement(message_object, user_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: diff --git a/src/core/slack.py b/src/core/slack.py index a5fda15..56d2e55 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -164,7 +164,7 @@ def get_announcement() -> dict[str, str] | None: return announcements.pop(0) -async def add_announcement(announcement_text: str, user_id: str) -> None: +def add_announcement(announcement_text: str, username: str) -> None: """ Adds an announcement to the queue. @@ -177,8 +177,6 @@ async def add_announcement(announcement_text: str, user_id: str) -> None: logger.warning("Attempted to add empty announcement, skipping!") return - username: str = await get_username(user_id=user_id) - username = username[:40] new_addition: dict[str, str] = {"content": announcement_text, "user": username} announcements.append(new_addition) diff --git a/tests/src/core/test_slack.py b/tests/src/core/test_slack.py index e6c0361..b1a143e 100644 --- a/tests/src/core/test_slack.py +++ b/tests/src/core/test_slack.py @@ -140,18 +140,18 @@ def test_get_and_add_announcement(monkeypatch): skip_announcements: list[str | None] = [None, "", " "] for ann in skip_announcements: - slack.add_announcement(ann) + slack.add_announcement(ann, "FAKE ID") assert slack.get_announcement() is None test_announcements: list[str] = ["First", "Second", "Third"] for ann in test_announcements: - slack.add_announcement(ann) + slack.add_announcement(ann, "FAKE ID") for ann in test_announcements: - assert slack.get_announcement() == ann + assert slack.get_announcement().get("content", "") == ann assert ( - slack.get_announcement() == test_announcements[-1] + slack.get_announcement().get("content", "") == test_announcements[-1] ) # should return last announcement when queue is empty From e5df8e465c969b67ec7eb08d2e341d1d682f1c98 Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 7 Apr 2026 21:15:32 -0400 Subject: [PATCH 43/48] feat: timestamps --- src/core/slack.py | 27 +++++++++++++++++++++++---- src/static/css/style.css | 28 ++++++++++++++++++++++++++++ src/static/js/main.js | 5 +++-- src/templates/index.html | 8 ++++++-- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/core/slack.py b/src/core/slack.py index 56d2e55..9ad121e 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -8,8 +8,14 @@ from slack_sdk.web.slack_response import SlackResponse from slack_sdk.errors import SlackApiError -from config import SLACK_API_TOKEN, SLACK_JUMPSTART_MESSAGE, SLACK_DM_TEMPLATE - +from config import ( + SLACK_API_TOKEN, + SLACK_JUMPSTART_MESSAGE, + SLACK_DM_TEMPLATE, + CALENDAR_TIMEZONE, +) +from datetime import datetime +from zoneinfo import ZoneInfo logger: Logger = getLogger(__name__) @@ -22,7 +28,13 @@ logger.error(f"Failed to initialize Slack client: {e}") announcements: list[dict[str, str]] = [ - {"content": "Welcome to Jumpstart!", "user": "Jumpstart"} + { + "content": "Welcome to Jumpstart!", + "user": "Jumpstart", + "timestamp": datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) + .strftime("%I:%M %p") + .lstrip("0"), + } ] @@ -177,6 +189,13 @@ def add_announcement(announcement_text: str, username: str) -> None: logger.warning("Attempted to add empty announcement, skipping!") return - new_addition: dict[str, str] = {"content": announcement_text, "user": username} + current_time = ( + datetime.now(tz=ZoneInfo(CALENDAR_TIMEZONE)).strftime("%I:%M %p").lstrip("0") + ) + new_addition: dict[str, str] = { + "content": announcement_text, + "user": username, + "timestamp": current_time, + } announcements.append(new_addition) diff --git a/src/static/css/style.css b/src/static/css/style.css index 750eb83..3ffbc71 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -8,6 +8,12 @@ --shadow-color: rgb(176,25,126, 0.5); /* Used for the shadow in the calendar */ } +/* b { + display: flex; + align-items: center; + justify-content: center; +} */ + .theme-dark { --panel-header-color: #B0197E; --panel-header-text-color: #FFFFFF; @@ -125,6 +131,17 @@ body{ font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; + white-space: pre-line; +} + +.announcements-stamp-header{ + color: var(--panel-header-text-color); + font-family: 'Courier New', Courier, monospace; + font-size: 20px; + text-align: left; + white-space: pre-line; + display: block; + font-style: italic; } .announcements-text-body{ @@ -145,6 +162,17 @@ body{ font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; + white-space: pre-line; +} + +.wikithoughts-page-name-header{ + color: var(--panel-header-text-color); + font-family: 'Courier New', Courier, monospace; + font-size: 20px; + text-align: left; + white-space: pre-line; + display: block; + font-style: italic; } .wikithoughts-text-body{ diff --git a/src/static/js/main.js b/src/static/js/main.js index 9039f7c..98cb8a6 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -121,10 +121,11 @@ async function mediumUpdate() { ]); const wikiData = await wikiRes.json(); const announcementData = await announcementRes.json(); - $("#wikipageheader").text(wikiData.page + " - csh/Wikithoughts") + $("#wikipageheader").text(wikiData.page) $("#wikipagetext").text(wikiData.content); $("#announcement").text(announcementData.content.substring(0, 910)); - $("#announcement-header").text("Announcements - " + announcementData.user) + $("#announcement-stamp").text(announcementData.user + " - " + announcementData.timestamp) + /*$("#announcement-header").text("Announcements\n" + announcementData.user)*/ } catch (err) { console.log(err); } diff --git a/src/templates/index.html b/src/templates/index.html index ea2cdab..7d5437a 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -19,7 +19,9 @@
- Announcements + Recent Announcement: + N/A/span> +
@@ -55,11 +57,13 @@
- Wiki Page - N/A + csh/Wikithoughts: + Page Name
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
From 22308e7e7dfaad99c6fbfd92c35cb242880a99b0 Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 8 Apr 2026 00:19:23 -0400 Subject: [PATCH 44/48] feat: timestamps --- src/static/css/style.css | 10 ---------- src/static/js/main.js | 2 +- src/templates/index.html | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/static/css/style.css b/src/static/css/style.css index 3ffbc71..f3f7066 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -165,16 +165,6 @@ body{ white-space: pre-line; } -.wikithoughts-page-name-header{ - color: var(--panel-header-text-color); - font-family: 'Courier New', Courier, monospace; - font-size: 20px; - text-align: left; - white-space: pre-line; - display: block; - font-style: italic; -} - .wikithoughts-text-body{ color:var(--panel-body-text-color); font-family: 'Courier New', Courier, monospace; diff --git a/src/static/js/main.js b/src/static/js/main.js index 98cb8a6..671f10f 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -121,7 +121,7 @@ async function mediumUpdate() { ]); const wikiData = await wikiRes.json(); const announcementData = await announcementRes.json(); - $("#wikipageheader").text(wikiData.page) + $("#wikipageheader").text("csh/Wikithoughts - " + wikiData.page) $("#wikipagetext").text(wikiData.content); $("#announcement").text(announcementData.content.substring(0, 910)); $("#announcement-stamp").text(announcementData.user + " - " + announcementData.timestamp) diff --git a/src/templates/index.html b/src/templates/index.html index 7d5437a..8388290 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -57,8 +57,7 @@
- csh/Wikithoughts: - Page Name + csh/Wikithoughts:
From d23ed53b65ef6f633c92c42a0313819ecc773184 Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 8 Apr 2026 01:12:19 -0400 Subject: [PATCH 45/48] feat: Calendar backend now sends a JSON --- src/api/endpoints.py | 6 +++--- src/core/cshcalendar.py | 25 +++++++++++-------------- src/static/js/main.js | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 9a74d7f..b5985a6 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -25,10 +25,10 @@ async def get_calendar() -> JSONResponse: JSONResponse: A JSON response containing the calendar data. """ - events: dict[str, str] = {} + events: list[dict[str, str]] = [] try: - get_future_events_ical: tuple[ + get_future_events_ical: list[ cshcalendar.CalendarInfo ] = await cshcalendar.get_future_events() events = cshcalendar.format_events(get_future_events_ical) @@ -36,7 +36,7 @@ async def get_calendar() -> JSONResponse: logger.error(f"Error fetching calendar events: {e}") return JSONResponse({"status": "error", "message": str(e)}, status_code=500) - return JSONResponse(events) + return JSONResponse({"data": events}) @router.get("/announcement") diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index beedb0c..1657e01 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -160,7 +160,7 @@ def calendar_to_html(seg_header: str, seg_content: str) -> str: return ret_string -def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: +def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]: """ Formats a parsed list of CalendarInfos, and returns the HTML required for front end @@ -172,18 +172,15 @@ def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: """ current_date: date = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) - final_events: str = "
" if not events: - final_events += BORDER_STRING + return {"data": [{"header": ":(", "content": "No Events on the Calendar"}]} - final_events += calendar_to_html(":(", "No Events on the Calendar") - - final_events += BORDER_STRING - - return {"data": final_events} + formatted_list: list[dict[str, str]] = [] for event in events: + content_dict: dict[str, str] = {} + event_cur_happening: bool = event.date < current_date if event_cur_happening: formatted: str = ( @@ -191,14 +188,14 @@ def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: if event.location else "Happening Now!" ) - final_events += calendar_to_html(formatted, event.name) + content_dict["header"] = formatted + content_dict["content"] = str(event.name) else: - final_events += calendar_to_html( - time_humanizer(current_date, event.date), event.name - ) + content_dict["header"] = time_humanizer(current_date, event.date) + content_dict["content"] = str(event.name) - final_events += BORDER_STRING - return {"data": final_events} + formatted_list.append(content_dict) + return formatted_list async def rebuild_calendar() -> None: diff --git a/src/static/js/main.js b/src/static/js/main.js index 671f10f..457cb72 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -71,6 +71,41 @@ function setNewPageTheme(newTheme) { document.body.classList.toggle(newTheme); } +function createCalendarEvent(headerText, contentText) { + const container = document.createElement("div"); + container.className = "calendar-event-container-lvl2"; + + const headerLabel = document.createElement("span"); + headerLabel.className = "calendar-text-date"; + headerLabel.textContent = headerText; + container.appendChild(headerLabel); + + container.appendChild(document.createElement("br")); + + const contentLabel = document.createElement("span"); + contentLabel.className = "calendar-text"; + contentLabel.textContent = contentText; + container.appendChild(contentLabel); + + return container; +} + +async function generateCalendar(calData) { + + const calendar = document.getElementById('calendar'); + calendar.replaceChildren(); + calendar.appendChild(document.createElement("br")); + + for (const event of calData) { + const newEvent = createCalendarEvent(event.header, event.content); + calendar.appendChild(newEvent); + + const bracket = document.createElement("hr"); + bracket.className = "calendar-border"; + calendar.appendChild(bracket); + } +} + async function longUpdate() { const date = new Date(); const hour = date.getHours(); @@ -107,7 +142,7 @@ async function longUpdate() { const res = await fetch('/api/calendar', { method: 'GET', mode: 'cors' }); const data = await res.json(); - $("#calendar").html(data.data); + await generateCalendar(data.data); } catch (err) { console.log(err); } From 83df2a972835e2511cdfac3eb43f566c2aaaad93 Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 8 Apr 2026 01:14:31 -0400 Subject: [PATCH 46/48] docs: Updated docstrings --- src/core/cshcalendar.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 1657e01..01b7c34 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -136,30 +136,6 @@ def repl(match: re.Match[str]) -> str: return TIME_PATTERN.sub(repl, unformatted_string) - -def calendar_to_html(seg_header: str, seg_content: str) -> str: - """ - Formats a header and content into the HTML for the calendar front end - - Args: - seg_header (str): The header of the calendar segment - seg_content (str): The content in the calendar segment - - Returns: - str: - """ - - ret_string: str = ( - """
""" - + seg_header - + """
""" - ) - ret_string += ( - "" + seg_content + "
" - ) - return ret_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 @@ -168,7 +144,7 @@ def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]: events: The list of CalendarInfos to be formatted Returns: - dict: Returns a dictionary with the "data" key mapping to the HTML data. + list[dict[str, str]]: Returns a dictionary with the "data" key mapping to a list of dictionarys of each event. """ current_date: date = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) @@ -258,7 +234,7 @@ async def get_future_events() -> list[CalendarInfo]: custom object has name, date and the location Returns: - list: A list of CalendarInfo objects + list[CalendarInfo]: A list of CalendarInfo objects """ global \ From 1fa475d6645623d80a239e012cb6a7e295a9ea62 Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 8 Apr 2026 01:19:41 -0400 Subject: [PATCH 47/48] fix: Removed list of announcments --- src/core/slack.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/core/slack.py b/src/core/slack.py index 9ad121e..1b410b3 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -27,15 +27,14 @@ except Exception as e: logger.error(f"Failed to initialize Slack client: {e}") -announcements: list[dict[str, str]] = [ - { - "content": "Welcome to Jumpstart!", - "user": "Jumpstart", - "timestamp": datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) - .strftime("%I:%M %p") - .lstrip("0"), - } -] +current_announcement: dict[str, str] = { + "content": "Welcome to Jumpstart!", + "user": "Jumpstart", + "timestamp": datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) + .strftime("%I:%M %p") + .lstrip("0") +} + def clean_text(raw: str) -> str: @@ -164,16 +163,10 @@ def get_announcement() -> dict[str, str] | None: Returns the next announcement in the queue. Returns: - dict | None: The next announcement text and user, or None if there are no announcements. + dict[str,str]: The next announcement text and user, or None if there are no announcements. """ - if len(announcements) == 0: - return None - - if len(announcements) == 1: - return announcements[0] - - return announcements.pop(0) + return current_announcement def add_announcement(announcement_text: str, username: str) -> None: @@ -184,6 +177,7 @@ def add_announcement(announcement_text: str, username: str) -> None: announcement_text (str): The text of the announcement to be added. user_id (str): The user_id of the person """ + global current_announcement if announcement_text is None or announcement_text.strip() == "": logger.warning("Attempted to add empty announcement, skipping!") @@ -198,4 +192,4 @@ def add_announcement(announcement_text: str, username: str) -> None: "timestamp": current_time, } - announcements.append(new_addition) + current_announcement = new_addition From d192f6a973b3bfca05a4759a1596728da4f87733 Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 8 Apr 2026 01:24:31 -0400 Subject: [PATCH 48/48] tests: fixed pytest --- tests/src/core/test_slack.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/src/core/test_slack.py b/tests/src/core/test_slack.py index b1a143e..ea952a4 100644 --- a/tests/src/core/test_slack.py +++ b/tests/src/core/test_slack.py @@ -135,8 +135,8 @@ def test_get_and_add_announcement(monkeypatch): slack = import_slack_module(monkeypatch) - slack.announcements.clear() - assert slack.get_announcement() is None + slack.current_announcement = None + assert slack.current_announcement is None skip_announcements: list[str | None] = [None, "", " "] for ann in skip_announcements: @@ -148,8 +148,6 @@ def test_get_and_add_announcement(monkeypatch): for ann in test_announcements: slack.add_announcement(ann, "FAKE ID") - - for ann in test_announcements: assert slack.get_announcement().get("content", "") == ann assert (