From f0137fa079ce92e30987cebd699a66208b64be5b Mon Sep 17 00:00:00 2001 From: Weather Date: Fri, 13 Mar 2026 00:48:38 -0500 Subject: [PATCH 01/11] here ya go nick --- .env.template | 8 +- docker-compose.yml | 7 ++ src/api/endpoints.py | 8 +- src/config.py | 5 + src/core/cshcalendar.py | 38 +++--- src/core/wikithoughts.py | 244 +++++++++++++++++++++++++++++++++++++++ src/main.py | 4 +- src/requirements.txt | 2 + src/static/css/style.css | 10 +- src/static/js/main.js | 15 +-- src/templates/index.html | 6 +- 11 files changed, 316 insertions(+), 31 deletions(-) create mode 100644 src/core/wikithoughts.py diff --git a/.env.template b/.env.template index 78ff702..9bc7bcc 100644 --- a/.env.template +++ b/.env.template @@ -3,6 +3,12 @@ CALENDAR_OUTLOOK_DAYS= CALENDAR_EVENT_MAXIMUM= CALENDAR_CACHE_REFRESH= CALENDAR_TIMEZONE= + WATCHED_CHANNELS= SLACK_API_TOKEN= -SLACK_JUMPSTART_MESSAGE= \ No newline at end of file +SLACK_JUMPSTART_MESSAGE= + +WIKI_API= +WIKIBOT_USER= +WIKIBOT_PASSWORD= +WIKI_CATEGORY= \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 084be92..40dfeaf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,13 @@ services: - CALENDAR_OUTLOOK_DAYS=${CALENDAR_OUTLOOK_DAYS} - CALENDAR_EVENT_MAXIMUM=${CALENDAR_EVENT_MAXIMUM} - CALENDAR_TIMEZONE=${CALENDAR_TIMEZONE} + - WATCHED_CHANNELS=${WATCHED_CHANNELS} - SLACK_API_TOKEN=${SLACK_API_TOKEN} - SLACK_JUMPSTART_MESSAGE=${SLACK_JUMPSTART_MESSAGE} + + - WIKI_API=${WIKI_API} + - WIKIBOT_USER=${WIKIBOT_USER} + - WIKIBOT_PASSWORD=${WIKIBOT_PASSWORD} + - WIKI_CATEGORY=${WIKI_CATEGORY} + \ No newline at end of file diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 11b4af7..ea7646e 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -9,7 +9,7 @@ from fastapi import APIRouter, Request, Form from fastapi.responses import JSONResponse -from core import slack, cshcalendar +from core import slack, cshcalendar, wikithoughts logger: Logger = getLogger(__name__) router: APIRouter = APIRouter() @@ -129,6 +129,12 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: return JSONResponse({"status": "success"}, status_code=200) +@router.get("/wikithought") +async def wikithought() -> JSONResponse: + returned_page_data: dict[str, str] = await wikithoughts.get_next_display() + return JSONResponse(returned_page_data) + + @router.get("/showerthoughts") async def showerthoughts() -> JSONResponse: """ diff --git a/src/config.py b/src/config.py index 7c45f0b..7e6a685 100644 --- a/src/config.py +++ b/src/config.py @@ -18,6 +18,11 @@ 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") + if SLACK_API_TOKEN in (None, ""): raise Exception("Missing SLACK_API_TOKEN") diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index cd4c365..b73b5de 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -45,23 +45,21 @@ def __init__(self, name: str, date_time: date, location: str | None = None): self.name: str = name if isinstance(date_time, date) and not isinstance(date_time, datetime): - date_time = arrow.combine( + date_time = date_time.combine( date_time, time.min, tzinfo=ZoneInfo(CALENDAR_TIMEZONE) ) elif not date_time.tzinfo: - date_time = date_time.replace(tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) + date_time = date_time.astimezone(tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) self.date: arrow.arrow = arrow.get(date_time) # Arrow has way cooler stuff self.location: str | None = location def __eq__(self, other): if not isinstance(other, CalendarInfo): return False - return (self.name == other.name) and (self.date == self.date) + return (self.name == other.name) and (self.date == other.date) def __hash__(self): - return ( - hash(self.name) + hash(self.date) - ) # Might be stupid only hashing off name, but as of right now I think the implementation works + return hash((self.name, self.date)) def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: @@ -123,8 +121,6 @@ async def rebuild_calendar() -> None: """ global calendar_cache, cal_last_update, cal_currently_rebuilding - cal_currently_rebuilding = True - try: found_events: set[CalendarInfo] = set() response: httpx.Response = await cshcal_client.get(CALENDAR_URL, timeout=10) @@ -132,19 +128,36 @@ async def rebuild_calendar() -> None: cal: Calendar = Calendar.from_ical(response.content) - current_time: date = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) + 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) ) for event in fetched_daily_events: + dt = event.get("DTSTART").dt + + if isinstance(dt, date) and not isinstance(dt, datetime): + dt = datetime.combine(dt, time.min, tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) + + elif dt.tzinfo is None: + dt = dt.replace(tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) + + else: + dt = dt.astimezone(ZoneInfo(CALENDAR_TIMEZONE)) + new_event: CalendarInfo = CalendarInfo( event.get("SUMMARY"), - event.get("DTSTART").dt, + dt, event.get("LOCATION"), ) + + before = len(found_events) found_events.add(new_event) + after = len(found_events) + + if before == after: + print("Duplicate detected:", new_event.name, new_event.date) except Exception as e: logger.warning("Failed to rebuild calendar cache! Error:") logger.warning(e) @@ -198,6 +211,7 @@ async def get_future_events() -> tuple[CalendarInfo]: logger.info("Pulling from CSH calendar cache!") return calendar_cache + cal_currently_rebuilding = True logger.info("Checking to rebuild CSH Calendar...") try: headers: dict[str, str | None] = {} @@ -222,9 +236,6 @@ async def get_future_events() -> tuple[CalendarInfo]: header_none_match = response.headers.get("ETag") header_last_modified = response.headers.get("Last-Modified") - if cal_currently_rebuilding: - return await wait_for_rebuild() - if cal_correct_length: logger.info("Calendar cache is full length, rebuilding async!") asyncio.create_task( @@ -233,6 +244,7 @@ async def get_future_events() -> tuple[CalendarInfo]: else: logger.info("Calendar cache is NOT full length, yielding rebuild!") await rebuild_calendar() + return calendar_cache except Exception as e: logger.warning("Failed to fetch the Calendar!") diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py new file mode 100644 index 0000000..3e26df5 --- /dev/null +++ b/src/core/wikithoughts.py @@ -0,0 +1,244 @@ +import httpx +import asyncio +from datetime import datetime, timedelta +from itertools import islice +from config import WIKIBOT_PASSWORD, WIKIBOT_USER, WIKI_CATEGORY,WIKI_API +import logging +import random + +logging.basicConfig(level=logging.INFO) + + +BATCH_SIZE: int = 50 # max titles per request +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__) + +bot_authenticated: bool = False +last_updated_time: datetime | None = None + +page_title_cache: list = [] +page_dict_cache: dict = {} + +etag: str | None = None +last_modifed: str | None = None + +queued_pages: list[str] = [] +shown_pages: list[str] = [] + + +def batch_iterable(iterable: list, size: int): + """ + Generator function for splitting up lists into smaller lists + To be frank, found this online when researching about the MediaWiki API + + Yields: + A batch split by the requested size + """ + it = iter(iterable) + while True: + batch = list(islice(it, size)) + if not batch: + break + yield batch + + +async def auth_bot(): + """ + Authenticates the CSH Wiki bot, logging if it was succesful or not + """ + token_req: httpx.Response = await client.get( + WIKI_API, + params={"action": "query", "meta": "tokens", "type": "login", "format": "json"}, + ) + token_req.raise_for_status() + + login_token: httpx.Response = token_req.json()["query"]["tokens"]["logintoken"] + login_req = await client.post( + WIKI_API, + data={ + "action": "login", + "lgname": WIKIBOT_USER, + "lgpassword": WIKIBOT_PASSWORD, + "lgtoken": login_token, + "format": "json", + }, + ) + login_req.raise_for_status() + + returned_json: dict = login_req.json()["login"] + if returned_json and returned_json["result"] == "Success": + global bot_authenticated + + bot_authenticated = True + logger.info("Bot was authenticated succesfully!") + else: + logger.warning("Bot was unable to authenticate!") + + +async def refresh_category_pages(): + """ + Function for fetching all pages of a MediaWiki Category + + Args: + 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 + """ + global \ + page_title_cache, \ + last_updated_time, \ + etag, \ + last_modifed, \ + queued_pages, \ + shown_pages + + if not bot_authenticated: + logger.warning("Bot is not authenticated, cancelling fetch of category pages") + return + + time_now: datetime = datetime.now() + if ( + len(page_title_cache) > 0 + and last_updated_time + and time_now < last_updated_time + timedelta(minutes=10) + ): + return + + titles: list[str] = [] + params: dict[str, str] = { + "action": "query", + "list": "categorymembers", + "cmtitle": f"Category:{WIKI_CATEGORY}", + "cmlimit": "500", + "format": "json", + } + + headers = {} + # This needs to loop due to mediawiki limitations + while True: + if not "cmcontinue" in params: + if etag: + headers["If-None-Match"] = etag + if last_modifed: + headers["If-Modified-Since"] = last_modifed + else: + headers = {} + 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: + etag = response.headers.get("ETag") + last_modifed = response.headers.get("Last-Modified") + + r_json: dict[str, str] = response.json() + for page in r_json["query"]["categorymembers"]: + titles.append(page["title"]) + + # Loop to keep everything going + if "continue" in r_json: + params["cmcontinue"] = r_json["continue"]["cmcontinue"] + else: + break + else: + logger.warning("Failed to update the CSH wiki page!") + return page_title_cache + + last_updated_time = time_now + page_title_cache = titles + queued_pages = titles.copy() + random.shuffle(queued_pages) + shown_pages = [] + + await refresh_page_dictionary() + return page_title_cache + + +async def refresh_page_dictionary(): + """ + Function for fetching the first "Sentence" of each title + + Args: + titles (list[str]): Each title of the page to search through + + Returns: + dict {title: str}: The title of each page, along with its corresponding sentence/first paragraph + + """ + global page_dict_cache, page_title_cache + + if not page_title_cache: + return {} + + results: dict[str, str] = {} + tasks: list = [] + for batch in batch_iterable(page_title_cache, BATCH_SIZE): + params = { + "action": "query", + "prop": "extracts", + "titles": "|".join(batch), + "explaintext": 1, + "exintro": 1, + "format": "json", + } + tasks.append(client.get(WIKI_API, params=params, timeout=10)) + # Code for running all this async? Pretty sure this works + responses = await asyncio.gather(*tasks) + + for r in responses: + r_json = r.json() + for page in r_json["query"]["pages"].values(): + + first_paragraph = ( + page.get("extract","").strip() + ) # Incase nick messes up? + + results[page["title"]] = first_paragraph + + page_dict_cache = results + + +def reset_queues(): + global queued_pages,shown_pages + """ + Swaps Queued and Shown pages que + """ + queued_pages = shown_pages + shown_pages = [] + +async def get_next_display(): + """ + Returns the next wiki page to be displayed in JSON Form, with the keys "page" as the title of the page + and "content" as the first paragraph on the page + + Returns: + dict["page": str,"content": str]: The JSON of the page name and the first paragraph + """ + global queued_pages, shown_pages + await refresh_category_pages() + + que_empty:bool = len(queued_pages) == 0 + if que_empty and len(shown_pages) == 0: + logger.warning("ERROR?!?") + return {"page": "ERROR GETTING PAGE", "content": "ERROR FETCHING CONTENT"} + elif que_empty: + reset_queues() + + new_page: str = queued_pages.pop() + + if que_empty: + reset_queues() + + shown_pages.append(new_page) + + if new_page in page_dict_cache: + new_content = page_dict_cache[new_page] + return {"page": new_page, "content": new_content} + + return {"page": new_page, "content": "ERROR FETCHING CONTENT"} diff --git a/src/main.py b/src/main.py index 8fb77a1..b200a7a 100644 --- a/src/main.py +++ b/src/main.py @@ -19,7 +19,7 @@ from config import BASE_DIR from api import endpoints -from core import cshcalendar +from core import cshcalendar, wikithoughts logger: Logger = getLogger(__name__) @@ -27,6 +27,8 @@ @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Starting up the Jumpstart application!") + + await wikithoughts.auth_bot() yield logger.info("Shutting down the Jumpstart application!") await cshcalendar.close_cal_client() diff --git a/src/requirements.txt b/src/requirements.txt index 305684d..0b1faf8 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -19,6 +19,8 @@ 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 9bcfd42..f9e702e 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -83,20 +83,20 @@ body{ text-align: center; } -.shower-thoughts{ +.wikithoughts{ float: left; width: 100%; padding-left: 1%; } -.shower-thoughts-text-header{ +.wikithoughts-text-header{ color: white; font-family: 'Courier New', Courier, monospace; font-size: 20px; text-align: center; } -.shower-thoughts-text-body{ +.wikithoughts-text-body{ color: black; font-family: 'Courier New', Courier, monospace; font-size: 18px; @@ -133,7 +133,7 @@ body{ } @media screen and (max-width: 1899px) { - .shower-thoughts{ + .wikithoughts{ width: 49%; padding-top: 1%; } @@ -175,7 +175,7 @@ body{ /* margin-top: 2%; */ width: 100%; } - .shower-thoughts{ + .wikithoughts{ width: 100%; padding-top: 2%; } diff --git a/src/static/js/main.js b/src/static/js/main.js index 2aa6022..3571ede 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -29,7 +29,7 @@ async function longUpdate() { const isDay = hour > 9 && hour < 18; const panelBody = $(".panel-body"); const plugBody = $(".plug-body"); - const showerBody = $(".shower-thoughts-text-body"); + const wikiPages = $(".wikithoughts-text-body"); const announcementsBody = $(".announcements-text-body"); const calendarFrame = $(".calendar-frame-lvl1"); const calendarTextDate = $(".calendar-text-date"); @@ -38,7 +38,7 @@ async function longUpdate() { if (isDay) { panelBody.css("background-color", "white"); plugBody.css("background-color", "white"); - showerBody.css({ "background-color": "white", "color": "black" }); + wikiPages.css({ "background-color": "white", "color": "black" }); announcementsBody.css("color", "black"); calendarFrame.css("background-color", "white"); calendarTextDate.css("color", "black"); @@ -46,7 +46,7 @@ async function longUpdate() { } else { panelBody.css("background-color", "black"); plugBody.css("background-color", "black"); - showerBody.css({ "background-color": "black", "color": "white" }); + wikiPages.css({ "background-color": "black", "color": "white" }); announcementsBody.css("color", "white"); calendarFrame.css("background-color", "black"); calendarTextDate.css("color", "white"); @@ -62,13 +62,14 @@ async function longUpdate() { async function mediumUpdate() { try { - const [showerRes, announcementRes] = await Promise.all([ - fetch('/api/showerthoughts', { method: 'GET', mode: 'cors' }), + const [wikiRes, announcementRes] = await Promise.all([ + fetch('/api/wikithought', { method: 'GET', mode: 'cors' }), fetch('/api/announcement', { method: 'GET', mode: 'cors' }) ]); - const showerData = await showerRes.json(); + const wikiData = await wikiRes.json(); const announcementData = await announcementRes.json(); - $("#showerthoughts").text(showerData.data); + $("#wikipageheader").text("csh/Wikithoughts - " + wikiData.page) + $("#wikipagetext").text(wikiData.content); $("#announcement").text(announcementData.data.substring(0, 910)); } catch (err) { console.log(err); diff --git a/src/templates/index.html b/src/templates/index.html index 1f23266..ca264d1 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -52,14 +52,14 @@ } (document,'script','weatherwidget-io-js'); -
+
- Shower Thoughts - /r/showerthoughts + Wiki Page - N/A
- 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. + 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 4a0eba41c22a3aefa02be248db16a50fbd77321c Mon Sep 17 00:00:00 2001 From: Weather Date: Fri, 13 Mar 2026 01:30:31 -0500 Subject: [PATCH 02/11] MediaWiki implementation --- src/core/wikithoughts.py | 49 ++++++++++++++++++++++++++++++++-------- src/static/js/main.js | 2 +- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 3e26df5..1148769 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -2,9 +2,10 @@ import asyncio from datetime import datetime, timedelta from itertools import islice -from config import WIKIBOT_PASSWORD, WIKIBOT_USER, WIKI_CATEGORY,WIKI_API +from config import WIKIBOT_PASSWORD, WIKIBOT_USER, WIKI_CATEGORY, WIKI_API import logging import random +import re logging.basicConfig(level=logging.INFO) @@ -29,6 +30,28 @@ shown_pages: list[str] = [] +def clean_wikitext(text: str) -> str: + """ + Function for cleaning markdown text to be displayed + """ + # [[Page|Text]] → Text + text = re.sub(r"\[\[[^\|\]]*\|([^\]]+)\]\]", r"\1", text) + + # [[Page]] → Page + text = re.sub(r"\[\[([^\]]+)\]\]", r"\1", text) + + # Remove templates {{...}} + text = re.sub(r"\{\{.*?\}\}", "", text, flags=re.DOTALL) + + # Remove bold/italic markup + text = re.sub(r"''+", "", text) + + # Remove HTML tags + text = re.sub(r"<.*?>", "", text) + + return text.strip() + + def batch_iterable(iterable: list, size: int): """ Generator function for splitting up lists into smaller lists @@ -99,7 +122,7 @@ async def refresh_category_pages(): if not bot_authenticated: logger.warning("Bot is not authenticated, cancelling fetch of category pages") return - + time_now: datetime = datetime.now() if ( len(page_title_cache) > 0 @@ -127,7 +150,9 @@ async def refresh_category_pages(): headers["If-Modified-Since"] = last_modifed else: headers = {} - response: httpx.Response = await client.get(WIKI_API, params=params, headers=headers) + response: httpx.Response = await client.get( + WIKI_API, params=params, headers=headers + ) if response.status_code == 304: last_updated_time = time_now @@ -181,10 +206,11 @@ async def refresh_page_dictionary(): for batch in batch_iterable(page_title_cache, BATCH_SIZE): params = { "action": "query", - "prop": "extracts", + "prop": "revisions", + "rvprop": "content", + "rvslots": "main", "titles": "|".join(batch), - "explaintext": 1, - "exintro": 1, + "explaintext": True, "format": "json", } tasks.append(client.get(WIKI_API, params=params, timeout=10)) @@ -194,9 +220,13 @@ async def refresh_page_dictionary(): for r in responses: r_json = 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 first_paragraph = ( - page.get("extract","").strip() + paragraphs[0].strip() if paragraphs else "" ) # Incase nick messes up? results[page["title"]] = first_paragraph @@ -205,13 +235,14 @@ async def refresh_page_dictionary(): def reset_queues(): - global queued_pages,shown_pages + global queued_pages, shown_pages """ Swaps Queued and Shown pages que """ queued_pages = shown_pages shown_pages = [] + async def get_next_display(): """ Returns the next wiki page to be displayed in JSON Form, with the keys "page" as the title of the page @@ -223,7 +254,7 @@ async def get_next_display(): global queued_pages, shown_pages await refresh_category_pages() - que_empty:bool = len(queued_pages) == 0 + que_empty: bool = len(queued_pages) == 0 if que_empty and len(shown_pages) == 0: logger.warning("ERROR?!?") return {"page": "ERROR GETTING PAGE", "content": "ERROR FETCHING CONTENT"} diff --git a/src/static/js/main.js b/src/static/js/main.js index 3571ede..fca6108 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -68,7 +68,7 @@ async function mediumUpdate() { ]); const wikiData = await wikiRes.json(); const announcementData = await announcementRes.json(); - $("#wikipageheader").text("csh/Wikithoughts - " + wikiData.page) + $("#wikipageheader").text(wikiData.page + " - csh/Wikithoughts") $("#wikipagetext").text(wikiData.content); $("#announcement").text(announcementData.data.substring(0, 910)); } catch (err) { From 050ccb3fd7f39de51efcb19835ecfed8f605d412 Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 17 Mar 2026 03:40:33 -0400 Subject: [PATCH 03/11] Wikithoughts almost finished --- .env.template | 4 ++- docker-compose.yml | 3 +- src/api/endpoints.py | 7 ++-- src/core/cshcalendar.py | 78 +++++++++++++++++++++++----------------- src/core/wikithoughts.py | 61 +++++++++++++++++++++++-------- src/main.py | 3 +- src/static/js/main.js | 16 ++------- 7 files changed, 105 insertions(+), 67 deletions(-) diff --git a/.env.template b/.env.template index 9bc7bcc..b705e10 100644 --- a/.env.template +++ b/.env.template @@ -11,4 +11,6 @@ SLACK_JUMPSTART_MESSAGE= WIKI_API= WIKIBOT_USER= WIKIBOT_PASSWORD= -WIKI_CATEGORY= \ No newline at end of file +WIKI_CATEGORY= + +WITH_EXTENSION=0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 40dfeaf..00eef18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,5 +19,4 @@ services: - WIKI_API=${WIKI_API} - WIKIBOT_USER=${WIKIBOT_USER} - WIKIBOT_PASSWORD=${WIKIBOT_PASSWORD} - - WIKI_CATEGORY=${WIKI_CATEGORY} - \ No newline at end of file + - WIKI_CATEGORY=${WIKI_CATEGORY} \ No newline at end of file diff --git a/src/api/endpoints.py b/src/api/endpoints.py index ea7646e..c5d7b0e 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -16,7 +16,7 @@ @router.get("/calendar") -def get_calendar() -> JSONResponse: +async def get_calendar() -> JSONResponse: """ Returns calendar data. @@ -24,9 +24,8 @@ def get_calendar() -> JSONResponse: JSONResponse: A JSON response containing the calendar data. """ - get_future_events_ical: tuple[cshcalendar.CalendarInfo] = asyncio.run( - cshcalendar.get_future_events() - ) + logger.warning("UPDATING CALENDAR") + get_future_events_ical: tuple[cshcalendar.CalendarInfo] = await cshcalendar.get_future_events() formatted_events: dict = cshcalendar.format_events(get_future_events_ical) return JSONResponse(formatted_events) diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index b73b5de..58dcf60 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -17,7 +17,7 @@ ) import asyncio -calendar_cache: tuple[CalendarInfo] = () # The current cache of the calendar +calendar_cache: list[CalendarInfo] = [] # The current cache of the calendar cal_last_update: date | None = ( None # The last time the calendar was fetched and updated the cache ) @@ -62,6 +62,29 @@ def __hash__(self): return hash((self.name, self.date)) +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: tuple[CalendarInfo]) -> dict[str, str]: """ Formats a parsed list of CalendarInfos, and returns the HTML required for front end @@ -78,39 +101,21 @@ def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: if not events: final_events += "
" - final_events += ( - """
""" - + " " - + """
""" - ) - final_events += ( - "" - + "No Events on the Calendar!" - + "
" - ) + + final_events += calendar_to_html(":(","No Events on the Calendar") + + final_events += "
" + return {"data": final_events} for event in events: - formatted: str = "" - if event.date < current_date: - formatted = ( - f"Happening in {event.location}!" - if event.location - else "Happening Now!" - ) + event_cur_happening: bool = event.date < current_date + if event_cur_happening: + formatted:str = f"Happening in {event.location}!" if event.location else "Happpening Now!" + final_events += calendar_to_html(formatted,event.name) else: - formatted = event.date.humanize().title() + final_events += calendar_to_html(event.date.humanize().title(),event.name) - final_events += ( - """
""" - + formatted - + """
""" - ) - final_events += ( - "" - + "".join(event.name) - + "
" - ) final_events += "
" return {"data": final_events} @@ -120,8 +125,8 @@ 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_currently_rebuilding - try: + cal_currently_rebuilding = True found_events: set[CalendarInfo] = set() response: httpx.Response = await cshcal_client.get(CALENDAR_URL, timeout=10) response.raise_for_status() @@ -161,6 +166,7 @@ async def rebuild_calendar() -> None: except Exception as e: logger.warning("Failed to rebuild calendar cache! Error:") logger.warning(e) + cal_currently_rebuilding = False cal_last_update = current_time calendar_cache = sorted(found_events, key=lambda x: x.date)[ @@ -178,6 +184,8 @@ async def wait_for_rebuild() -> tuple[CalendarInfo]: """ global cal_currently_rebuilding while cal_currently_rebuilding: + logger.warning(cal_currently_rebuilding) + logger.warning("PAUSING FOR A REBUILD!") await asyncio.sleep(1) return calendar_cache @@ -191,6 +199,7 @@ async def get_future_events() -> tuple[CalendarInfo]: Returns: list: A list of CalendarInfo objects """ + global \ calendar_cache, \ cal_last_update, \ @@ -211,7 +220,7 @@ async def get_future_events() -> tuple[CalendarInfo]: logger.info("Pulling from CSH calendar cache!") return calendar_cache - cal_currently_rebuilding = True + logger.info("Checking to rebuild CSH Calendar...") try: headers: dict[str, str | None] = {} @@ -236,6 +245,10 @@ async def get_future_events() -> tuple[CalendarInfo]: header_none_match = response.headers.get("ETag") header_last_modified = response.headers.get("Last-Modified") + if cal_currently_rebuilding: + await rebuild_calendar() + return calendar_cache + if cal_correct_length: logger.info("Calendar cache is full length, rebuilding async!") asyncio.create_task( @@ -251,7 +264,7 @@ async def get_future_events() -> tuple[CalendarInfo]: logger.warning(e) -async def close_cal_client() -> None: +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 @@ -259,6 +272,7 @@ async def close_cal_client() -> None: global cshcal_client try: await cshcal_client.aclose() + logger.info("Succesfully closed the cshcal client") except RuntimeError as e: logger.warning("EVENT LOOP HAS ALREADY CLOSED, FAILED TO CLOSE csh_cal") return diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 1148769..86eedc7 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -9,8 +9,9 @@ logging.basicConfig(level=logging.INFO) - +CYCLE_DEBOUNCE_TIME=12 # How long it takes to resfresh wiki titles BATCH_SIZE: int = 50 # max titles per request + HEADERS: dict[str, str] = {"User-Agent": "JumpstartFetcher/1.0"} AUTH: tuple[str] = (WIKIBOT_USER, WIKIBOT_PASSWORD) @@ -28,26 +29,45 @@ queued_pages: list[str] = [] shown_pages: list[str] = [] +current_page: dict[str,str] = {"page": "NA","content":"NA"} +page_last_updated: datetime | None = None def clean_wikitext(text: str) -> str: """ - Function for cleaning markdown text to be displayed + Function for cleaning markdown text using regex commands + + Args: + text (str): The text to be cleaned + + Returns: + str: The cleaned up text string """ - # [[Page|Text]] → Text + + # [https... Text] -> Text + text = re.sub(r"\[https?:\/\/\S+\s+\"?([^\]\"]+)\"?\]", r"\1", text) + + # [[File: Page|Text]] → Text + text = re.sub(r"\[\[File:[^\]]*\]\]", "", text, flags=re.IGNORECASE) + + # [[Image: Page|Text]] -> Text + text = re.sub(r"\[\[Image:[^\]]*\]\]", "", text, flags=re.IGNORECASE) + # [[Page|Text]] -> Text text = re.sub(r"\[\[[^\|\]]*\|([^\]]+)\]\]", r"\1", text) - # [[Page]] → Page + # [[Page]] -> Page text = re.sub(r"\[\[([^\]]+)\]\]", r"\1", text) # Remove templates {{...}} text = re.sub(r"\{\{.*?\}\}", "", text, flags=re.DOTALL) + # Remove HTML tags + text = re.sub(r"<.*?>", "", text) + # Remove bold/italic markup text = re.sub(r"''+", "", text) - # Remove HTML tags - text = re.sub(r"<.*?>", "", text) + return text.strip() @@ -68,7 +88,7 @@ def batch_iterable(iterable: list, size: int): yield batch -async def auth_bot(): +async def auth_bot() -> None: """ Authenticates the CSH Wiki bot, logging if it was succesful or not """ @@ -101,9 +121,9 @@ async def auth_bot(): logger.warning("Bot was unable to authenticate!") -async def refresh_category_pages(): +async def refresh_category_pages() -> list[str]: """ - Function for fetching all pages of a MediaWiki Category + Refreshes all pages of the category Args: category (str): The name of the category to search through @@ -235,11 +255,16 @@ async def refresh_page_dictionary(): def reset_queues(): - global queued_pages, shown_pages """ Swaps Queued and Shown pages que """ + global queued_pages, shown_pages + logger.warning("RESETING QUEUES FOR WIKITHOUGHTS") + if len(queued_pages) > 0: + return + queued_pages = shown_pages + random.shuffle(queued_pages) shown_pages = [] @@ -251,15 +276,21 @@ async def get_next_display(): Returns: dict["page": str,"content": str]: The JSON of the page name and the first paragraph """ - global queued_pages, shown_pages + global queued_pages, shown_pages, page_last_updated, current_page + + if page_last_updated and (page_last_updated < datetime.now() + timedelta(seconds=CYCLE_DEBOUNCE_TIME)): + logger.warning("Pulling from quote cache!") + return current_page + await refresh_category_pages() que_empty: bool = len(queued_pages) == 0 if que_empty and len(shown_pages) == 0: logger.warning("ERROR?!?") - return {"page": "ERROR GETTING PAGE", "content": "ERROR FETCHING CONTENT"} + current_page = {"page": "ERROR GETTING PAGE", "content": "ERROR FETCHING CONTENT"} elif que_empty: reset_queues() + que_empty = False new_page: str = queued_pages.pop() @@ -270,6 +301,8 @@ async def get_next_display(): if new_page in page_dict_cache: new_content = page_dict_cache[new_page] - return {"page": new_page, "content": new_content} + current_page = {"page": new_page, "content": new_content} + else: + current_page = {"page": new_page, "content": "ERROR FETCHING CONTENT"} - return {"page": new_page, "content": "ERROR FETCHING CONTENT"} + return current_page diff --git a/src/main.py b/src/main.py index b200a7a..45d2b36 100644 --- a/src/main.py +++ b/src/main.py @@ -31,7 +31,8 @@ async def lifespan(app: FastAPI): await wikithoughts.auth_bot() yield logger.info("Shutting down the Jumpstart application!") - await cshcalendar.close_cal_client() + await cshcalendar.close_client() + logger.info("Succesfully shut down the Jumpstart application!") diff --git a/src/static/js/main.js b/src/static/js/main.js index fca6108..d45fa7e 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -76,21 +76,11 @@ async function mediumUpdate() { } } -async function shortUpdate() { - try { - const res = await fetch('/api/harold', { method: 'GET', mode: 'cors' }); - const data = await res.json(); - $("#harold-file-name").text(data.data); - } catch (err) { - console.log(err); - } -} -shortUpdate(); + mediumUpdate(); longUpdate(); -setInterval(longUpdate, 360000); -setInterval(mediumUpdate, 13000); -setInterval(shortUpdate, 4000); +setInterval(longUpdate, 60000); +setInterval(mediumUpdate, 22000); setInterval(() => { if (window.__weatherwidget_init) window.__weatherwidget_init(); }, 1800000); \ No newline at end of file From 41f0bdd9743bb29e213b503eb9fd9101d1f1414c Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 17 Mar 2026 03:40:52 -0400 Subject: [PATCH 04/11] Formatted --- src/api/endpoints.py | 4 +++- src/core/cshcalendar.py | 36 +++++++++++++++++++----------------- src/core/wikithoughts.py | 24 ++++++++++++++---------- src/main.py | 2 +- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index c5d7b0e..5f8fc26 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -25,7 +25,9 @@ async def get_calendar() -> JSONResponse: """ logger.warning("UPDATING CALENDAR") - get_future_events_ical: tuple[cshcalendar.CalendarInfo] = await cshcalendar.get_future_events() + get_future_events_ical: tuple[ + cshcalendar.CalendarInfo + ] = await cshcalendar.get_future_events() formatted_events: dict = cshcalendar.format_events(get_future_events_ical) return JSONResponse(formatted_events) diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 58dcf60..cd3f644 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -17,7 +17,7 @@ ) import asyncio -calendar_cache: list[CalendarInfo] = [] # The current cache of the calendar +calendar_cache: list[CalendarInfo] = [] # The current cache of the calendar cal_last_update: date | None = ( None # The last time the calendar was fetched and updated the cache ) @@ -69,22 +69,21 @@ def calendar_to_html(seg_header: str, seg_content: str) -> str: 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 - + """
""" - ) + """
""" + + seg_header + + """
""" + ) ret_string += ( - "" - + seg_content - + "
" - ) + "" + seg_content + "
" + ) return ret_string + def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: """ Formats a parsed list of CalendarInfos, and returns the HTML required for front end @@ -102,19 +101,23 @@ def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: if not events: final_events += "
" - final_events += calendar_to_html(":(","No Events on the Calendar") + final_events += calendar_to_html(":(", "No Events on the Calendar") final_events += "
" return {"data": final_events} for event in events: - event_cur_happening: bool = event.date < current_date + event_cur_happening: bool = event.date < current_date if event_cur_happening: - formatted:str = f"Happening in {event.location}!" if event.location else "Happpening Now!" - final_events += calendar_to_html(formatted,event.name) + formatted: str = ( + f"Happening in {event.location}!" + if event.location + else "Happpening Now!" + ) + final_events += calendar_to_html(formatted, event.name) else: - final_events += calendar_to_html(event.date.humanize().title(),event.name) + final_events += calendar_to_html(event.date.humanize().title(), event.name) final_events += "
" return {"data": final_events} @@ -220,7 +223,6 @@ async def get_future_events() -> tuple[CalendarInfo]: logger.info("Pulling from CSH calendar cache!") return calendar_cache - logger.info("Checking to rebuild CSH Calendar...") try: headers: dict[str, str | None] = {} @@ -248,7 +250,7 @@ async def get_future_events() -> tuple[CalendarInfo]: if cal_currently_rebuilding: await rebuild_calendar() return calendar_cache - + if cal_correct_length: logger.info("Calendar cache is full length, rebuilding async!") asyncio.create_task( diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 86eedc7..7ca1cd8 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO) -CYCLE_DEBOUNCE_TIME=12 # How long it takes to resfresh wiki titles +CYCLE_DEBOUNCE_TIME = 12 # How long it takes to resfresh wiki titles BATCH_SIZE: int = 50 # max titles per request HEADERS: dict[str, str] = {"User-Agent": "JumpstartFetcher/1.0"} @@ -29,10 +29,11 @@ queued_pages: list[str] = [] shown_pages: list[str] = [] -current_page: dict[str,str] = {"page": "NA","content":"NA"} +current_page: dict[str, str] = {"page": "NA", "content": "NA"} page_last_updated: datetime | None = None + def clean_wikitext(text: str) -> str: """ Function for cleaning markdown text using regex commands @@ -43,7 +44,7 @@ def clean_wikitext(text: str) -> str: Returns: str: The cleaned up text string """ - + # [https... Text] -> Text text = re.sub(r"\[https?:\/\/\S+\s+\"?([^\]\"]+)\"?\]", r"\1", text) @@ -63,12 +64,10 @@ def clean_wikitext(text: str) -> str: # Remove HTML tags text = re.sub(r"<.*?>", "", text) - + # Remove bold/italic markup text = re.sub(r"''+", "", text) - - return text.strip() @@ -262,7 +261,7 @@ def reset_queues(): logger.warning("RESETING QUEUES FOR WIKITHOUGHTS") if len(queued_pages) > 0: return - + queued_pages = shown_pages random.shuffle(queued_pages) shown_pages = [] @@ -278,16 +277,21 @@ async def get_next_display(): """ global queued_pages, shown_pages, page_last_updated, current_page - if page_last_updated and (page_last_updated < datetime.now() + timedelta(seconds=CYCLE_DEBOUNCE_TIME)): + if page_last_updated and ( + page_last_updated < datetime.now() + timedelta(seconds=CYCLE_DEBOUNCE_TIME) + ): logger.warning("Pulling from quote cache!") return current_page - + await refresh_category_pages() que_empty: bool = len(queued_pages) == 0 if que_empty and len(shown_pages) == 0: logger.warning("ERROR?!?") - current_page = {"page": "ERROR GETTING PAGE", "content": "ERROR FETCHING CONTENT"} + current_page = { + "page": "ERROR GETTING PAGE", + "content": "ERROR FETCHING CONTENT", + } elif que_empty: reset_queues() que_empty = False diff --git a/src/main.py b/src/main.py index 45d2b36..1478883 100644 --- a/src/main.py +++ b/src/main.py @@ -32,7 +32,7 @@ async def lifespan(app: FastAPI): yield logger.info("Shutting down the Jumpstart application!") await cshcalendar.close_client() - + logger.info("Succesfully shut down the Jumpstart application!") From 1cae825771b25b6ac6d617678324ad5e71f8b182 Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 17 Mar 2026 14:53:43 -0400 Subject: [PATCH 05/11] Switched to Event Signals --- .env.template | 2 -- README.md | 16 +++++++------ src/api/endpoints.py | 1 - src/core/cshcalendar.py | 52 ++++++++++++++++------------------------ src/core/wikithoughts.py | 22 ++++++----------- src/main.py | 3 ++- 6 files changed, 38 insertions(+), 58 deletions(-) diff --git a/.env.template b/.env.template index b705e10..853032e 100644 --- a/.env.template +++ b/.env.template @@ -12,5 +12,3 @@ WIKI_API= WIKIBOT_USER= WIKIBOT_PASSWORD= WIKI_CATEGORY= - -WITH_EXTENSION=0 \ No newline at end of file diff --git a/README.md b/README.md index cc9c7bf..1a6fc25 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -# TODO -- Add a link the mkDocs we do - -# Jumpstart V2: Electric Boogaloo +# Jumpstart ![image](./docs/images/jumpstart_transparant.png) ![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 important information at the entrance of CSH. -This is a backend re-write of the previous jumpstart. +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. +See it live [here](http://jumpstart-squared.cs.house/)! -See it live [here](https://jumpstart.csh.rit.edu)! +## Installing +2. Clone and cd into the repo: `git clone https://github.com/WeatherGod3218/jumpstartV2 +>> (OPTIONAL): Make another branch if your working on a large thing! ## Setup 1. Make sure you have docker installed diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 5f8fc26..0096147 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -24,7 +24,6 @@ async def get_calendar() -> JSONResponse: JSONResponse: A JSON response containing the calendar data. """ - logger.warning("UPDATING CALENDAR") get_future_events_ical: tuple[ cshcalendar.CalendarInfo ] = await cshcalendar.get_future_events() diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index cd3f644..52d0e68 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -27,7 +27,8 @@ header_last_modified: str | None = ( None # Used for the httpx.get to see when the calndar was last modified ) -cal_currently_rebuilding: bool = False # Tells if the calendar is being rebuilt +cal_constructed_event: asyncio.Event = asyncio.Event() +cal_constructed_event.clear() logger: Logger = getLogger(__name__) logger.info("Starting up the calendar service!") @@ -127,9 +128,9 @@ 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_currently_rebuilding + global calendar_cache, cal_last_update, cal_constructed_event try: - cal_currently_rebuilding = True + cal_constructed_event.clear() found_events: set[CalendarInfo] = set() response: httpx.Response = await cshcal_client.get(CALENDAR_URL, timeout=10) response.raise_for_status() @@ -160,41 +161,23 @@ async def rebuild_calendar() -> None: event.get("LOCATION"), ) - before = len(found_events) found_events.add(new_event) - after = len(found_events) - if before == after: - print("Duplicate detected:", new_event.name, new_event.date) + cal = None + fetched_daily_events = None except Exception as e: logger.warning("Failed to rebuild calendar cache! Error:") logger.warning(e) - cal_currently_rebuilding = False + cal_constructed_event.set() cal_last_update = current_time calendar_cache = sorted(found_events, key=lambda x: x.date)[ :CALENDAR_EVENT_MAXIMUM ] # Only cache the first elements of this list - cal_currently_rebuilding = False + cal_constructed_event.set() -async def wait_for_rebuild() -> tuple[CalendarInfo]: - """ - Simple yielding function for waiting to return the freshly made calendar cache, rather than proceeding with the obtain - - Returns: - list: A list of CalendarInfo objects - """ - global cal_currently_rebuilding - while cal_currently_rebuilding: - logger.warning(cal_currently_rebuilding) - logger.warning("PAUSING FOR A REBUILD!") - await asyncio.sleep(1) - - return calendar_cache - - -async def get_future_events() -> tuple[CalendarInfo]: +async def get_future_events() -> list[CalendarInfo]: """ Returns the first events up to event maximum within the the calendar outlook day amount custom object has name, date and the location @@ -208,13 +191,15 @@ async def get_future_events() -> tuple[CalendarInfo]: cal_last_update, \ header_last_modified, \ header_none_match, \ - cal_currently_rebuilding + cal_constructed_event - if cal_currently_rebuilding: - return await wait_for_rebuild() + if not cal_constructed_event.is_set(): + await cal_constructed_event.wait() + return calendar_cache cur_time: date = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) cal_correct_length: bool = len(calendar_cache) == CALENDAR_EVENT_MAXIMUM + if ( cal_last_update and cal_correct_length @@ -247,15 +232,18 @@ async def get_future_events() -> tuple[CalendarInfo]: header_none_match = response.headers.get("ETag") header_last_modified = response.headers.get("Last-Modified") - if cal_currently_rebuilding: - await rebuild_calendar() + if ( + not cal_constructed_event.is_set() + ): # Double check, since it might have changed for the last modifed + await cal_constructed_event.wait() return calendar_cache if cal_correct_length: logger.info("Calendar cache is full length, rebuilding async!") - asyncio.create_task( + running_task = 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 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 7ca1cd8..1721561 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -7,8 +7,6 @@ import random import re -logging.basicConfig(level=logging.INFO) - CYCLE_DEBOUNCE_TIME = 12 # How long it takes to resfresh wiki titles BATCH_SIZE: int = 50 # max titles per request @@ -97,8 +95,8 @@ async def auth_bot() -> None: ) token_req.raise_for_status() - login_token: httpx.Response = token_req.json()["query"]["tokens"]["logintoken"] - login_req = await client.post( + login_token: dict = token_req.json()["query"]["tokens"]["logintoken"] + login_req: httpx.Response = await client.post( WIKI_API, data={ "action": "login", @@ -159,7 +157,7 @@ async def refresh_category_pages() -> list[str]: "format": "json", } - headers = {} + headers: dict[str, str] = {} # This needs to loop due to mediawiki limitations while True: if not "cmcontinue" in params: @@ -204,15 +202,9 @@ async def refresh_category_pages() -> list[str]: return page_title_cache -async def refresh_page_dictionary(): +async def refresh_page_dictionary() -> None: """ - Function for fetching the first "Sentence" of each title - - Args: - titles (list[str]): Each title of the page to search through - - Returns: - dict {title: str}: The title of each page, along with its corresponding sentence/first paragraph + Fetches the pages based off the cace of page titles, and updates the page cache accordingly """ global page_dict_cache, page_title_cache @@ -253,7 +245,7 @@ async def refresh_page_dictionary(): page_dict_cache = results -def reset_queues(): +def reset_queues() -> None: """ Swaps Queued and Shown pages que """ @@ -267,7 +259,7 @@ def reset_queues(): shown_pages = [] -async def get_next_display(): +async def get_next_display() -> dict[str, str]: """ Returns the next wiki page to be displayed in JSON Form, with the keys "page" as the title of the page and "content" as the first paragraph on the page diff --git a/src/main.py b/src/main.py index 1478883..4cd921f 100644 --- a/src/main.py +++ b/src/main.py @@ -6,6 +6,7 @@ """ import os +import asyncio from logging import getLogger, Logger @@ -27,7 +28,7 @@ @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Starting up the Jumpstart application!") - + asyncio.create_task(cshcalendar.rebuild_calendar()) await wikithoughts.auth_bot() yield logger.info("Shutting down the Jumpstart application!") From fdffe7870b846a6e8c34982e993dfaaadb42b620 Mon Sep 17 00:00:00 2001 From: Weather Date: Tue, 17 Mar 2026 14:56:34 -0400 Subject: [PATCH 06/11] READ ME RAHHHHHH --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a6fc25..0693ccd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This project uses Python, [FastAPI](https://fastapi.tiangolo.com/), HTML/CSS, an See it live [here](http://jumpstart-squared.cs.house/)! ## Installing -2. Clone and cd into the repo: `git clone https://github.com/WeatherGod3218/jumpstartV2 +1. Clone and cd into the repo: `git clone https://github.com/WeatherGod3218/jumpstartV2 >> (OPTIONAL): Make another branch if your working on a large thing! ## Setup From 661f608551eea55cf49619c10b6168b5a291aab2 Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 18 Mar 2026 01:14:36 -0400 Subject: [PATCH 07/11] Slack? --- README.md | 2 +- src/api/endpoints.py | 7 +++---- src/core/{cshcalendar.py => cshcalendar/__init__.py} | 0 src/core/slack.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) rename src/core/{cshcalendar.py => cshcalendar/__init__.py} (100%) diff --git a/README.md b/README.md index 0693ccd..0004bb1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This project uses Python, [FastAPI](https://fastapi.tiangolo.com/), HTML/CSS, an See it live [here](http://jumpstart-squared.cs.house/)! ## Installing -1. Clone and cd into the repo: `git clone https://github.com/WeatherGod3218/jumpstartV2 +1. Clone and cd into the repo: git clone https://github.com/WeatherGod3218/jumpstartV2 >> (OPTIONAL): Make another branch if your working on a large thing! ## Setup diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 0096147..601dcb8 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -4,7 +4,6 @@ import httpx import random import textwrap -import asyncio from fastapi import APIRouter, Request, Form from fastapi.responses import JSONResponse @@ -58,14 +57,14 @@ async def slack_events(request: Request) -> JSONResponse: try: logger.info("Received Slack event!") - + + body: dict = await request.json() if request.headers.get("content-type") == "application/json": - body: dict = await request.json() + if body.get("type") == "url_verification": return JSONResponse({"challenge": body.get("challenge")}) - body: dict = await request.json() if not body: return JSONResponse({"challenge": body.get("challenge")}) diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar/__init__.py similarity index 100% rename from src/core/cshcalendar.py rename to src/core/cshcalendar/__init__.py diff --git a/src/core/slack.py b/src/core/slack.py index 6420a08..e8ab340 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -8,10 +8,10 @@ from config import SLACK_API_TOKEN, SLACK_JUMPSTART_MESSAGE, SLACK_DM_TEMPLATE + logger: Logger = getLogger(__name__) client: AsyncWebClient | None = None - try: client = AsyncWebClient(token=SLACK_API_TOKEN) except Exception as e: From ffb18ab02bfdff87ea85ebd6822f0150a5da99dd Mon Sep 17 00:00:00 2001 From: Weather Date: Wed, 18 Mar 2026 01:27:41 -0400 Subject: [PATCH 08/11] Merging --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3663282..ddaf717 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,7 +39,7 @@ nav: - Home: index.md - Getting Started: docs/getting-started/getting-started.md - Endpoints: - - v1: docs/endpoints/endpoints.md + - endpoints: endpoints/endpoints.md - Scripts: - Setup: docs/getting-started/setup.md From c48de89c7b6f3d079095ae36d97e19631dbcbae7 Mon Sep 17 00:00:00 2001 From: gravy Date: Wed, 18 Mar 2026 01:53:13 -0400 Subject: [PATCH 09/11] Remove showerthoughts endpoint and update wikithought Removed the showerthoughts endpoint and updated the wikithought endpoint documentation. --- src/api/endpoints.py | 38 ++++---------------------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 601dcb8..35c2151 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -130,42 +130,12 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: @router.get("/wikithought") async def wikithought() -> JSONResponse: - returned_page_data: dict[str, str] = await wikithoughts.get_next_display() - return JSONResponse(returned_page_data) - - -@router.get("/showerthoughts") -async def showerthoughts() -> JSONResponse: """ - Returns a random shower thought from the Reddit API. + Returns a random CSH wiki thought from the MediaWiki API. Returns: - JSONResponse: A JSON response containing a random shower thought. + 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) - response: dict = {"data": "No shower thoughts found."} - - try: - logger.info("Fetching shower thoughts from Reddit API...") - - async with httpx.AsyncClient() as client: - reddit_data: httpx.Response = await client.get( - "https://www.reddit.com/r/showerthoughts/top.json", - headers={"User-agent": "Showerthoughtbot 0.1"}, - ) - - reddit_json = reddit_data.json() - - if len(reddit_json["data"]["children"]) == 0: - logger.warning("No shower thoughts found in Reddit API response.") - return JSONResponse(response) - - shower_thought: str = textwrap.fill( - (random.choice(reddit_json["data"]["children"])["data"]["title"]), 50 - ) - - response["data"] = shower_thought - except Exception as e: - logger.error(f"Error fetching shower thoughts: {e}") - - return JSONResponse(response) From 0493acb781f339546db739de1fd7e769c03ae9b9 Mon Sep 17 00:00:00 2001 From: gravy Date: Wed, 18 Mar 2026 02:12:39 -0400 Subject: [PATCH 10/11] Refactor date_time handling and fix typo Refactor date_time initialization logic in the constructor and fix typo in 'Happening Now!' string. --- src/core/cshcalendar/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/core/cshcalendar/__init__.py b/src/core/cshcalendar/__init__.py index 52d0e68..7005771 100644 --- a/src/core/cshcalendar/__init__.py +++ b/src/core/cshcalendar/__init__.py @@ -44,13 +44,6 @@ class CalendarInfo: def __init__(self, name: str, date_time: date, location: str | None = None): self.name: str = name - - if isinstance(date_time, date) and not isinstance(date_time, datetime): - date_time = date_time.combine( - date_time, time.min, tzinfo=ZoneInfo(CALENDAR_TIMEZONE) - ) - elif not date_time.tzinfo: - date_time = date_time.astimezone(tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) self.date: arrow.arrow = arrow.get(date_time) # Arrow has way cooler stuff self.location: str | None = location @@ -114,7 +107,7 @@ def format_events(events: tuple[CalendarInfo]) -> dict[str, str]: formatted: str = ( f"Happening in {event.location}!" if event.location - else "Happpening Now!" + else "Happening Now!" ) final_events += calendar_to_html(formatted, event.name) else: From 0b1bff33dbd646106e2c22561a1730fdb6d0402d Mon Sep 17 00:00:00 2001 From: gravy Date: Wed, 18 Mar 2026 02:26:45 -0400 Subject: [PATCH 11/11] Fix typos in auth_bot and refresh_page_dictionary --- src/core/wikithoughts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 1721561..dc35c5f 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -87,7 +87,7 @@ def batch_iterable(iterable: list, size: int): async def auth_bot() -> None: """ - Authenticates the CSH Wiki bot, logging if it was succesful or not + Authenticates the CSH Wiki bot, logging if it was successful or not """ token_req: httpx.Response = await client.get( WIKI_API, @@ -113,7 +113,7 @@ async def auth_bot() -> None: global bot_authenticated bot_authenticated = True - logger.info("Bot was authenticated succesfully!") + logger.info("Bot was authenticated successfully!") else: logger.warning("Bot was unable to authenticate!") @@ -204,7 +204,7 @@ async def refresh_category_pages() -> list[str]: async def refresh_page_dictionary() -> None: """ - Fetches the pages based off the cace of page titles, and updates the page cache accordingly + Fetches the pages based off the cache of page titles, and updates the page cache accordingly """ global page_dict_cache, page_title_cache