Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
LOGGING_LEVEL=DEBUG

CALENDAR_URL=
CALENDAR_OUTLOOK_DAYS=
CALENDAR_EVENT_MAXIMUM=
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ cython_debug/
.cursorignore
.cursorindexingignore


# shtl
.shtl/
shtl.toml

# Marimo
marimo/_static/
marimo/_lsp/
Expand Down
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ ENV UV_NO_CACHE=1
WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --group docs --frozen
RUN uv sync --frozen

FROM base AS docbuilder
ENV UV_NO_CACHE=1

RUN uv sync --group docs --frozen

WORKDIR /appdocs

COPY mkdocs.yml .
Expand All @@ -20,8 +22,8 @@ WORKDIR /app

RUN uv run zensical build --config-file /appdocs/mkdocs.yml

FROM ghcr.io/astral-sh/uv:python3.14-alpine
ENV UV_NO_CACHE=1
FROM python:3.14-alpine
ENV PATH="/app/.venv/bin:$PATH"

COPY --from=base /app/.venv /app/.venv
COPY src /app
Expand All @@ -34,4 +36,4 @@ RUN addgroup -g 2000 jumpgroup && \

USER jumpstart

CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "/app/logging_config.yaml", "--proxy-headers", "--forwarded-allow-ips", "*"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "/app/logging_config.yaml", "--proxy-headers", "--forwarded-allow-ips", "*"]
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---

services:
jumpstart:
build: .
Expand Down
37 changes: 26 additions & 11 deletions src/api/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from logging import getLogger, Logger
import json
from logging import Logger, getLogger

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

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

import urllib.parse

logger: Logger = getLogger(__name__)
logger.setLevel(LOGGING_LEVEL)

router: APIRouter = APIRouter()


Expand All @@ -23,10 +27,14 @@ async def get_calendar() -> JSONResponse:
events: list[dict[str, str]] = []

try:
get_future_events_ical: list[
cshcalendar.CalendarInfo
] = await cshcalendar.get_future_events()
events = cshcalendar.format_events(get_future_events_ical)
get_future_events_ical: (
list[cshcalendar.CalendarInfo] | None
) = await cshcalendar.get_future_events()

if get_future_events_ical is None:
raise Exception("Gathering future events resulted in None")

events.extend(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)
Expand Down Expand Up @@ -64,7 +72,10 @@ async def slack_events(request: Request) -> JSONResponse:
logger.warning(f"Received a Fake Slack Event!: {raw_body}")
return JSONResponse({"error": "Invalid signature"}, status_code=403)

body: dict = json.loads(raw_body)
try:
body: dict = json.loads(raw_body)
except json.JSONDecodeError:
return JSONResponse({"error": "Invalid JSON body"}, status_code=400)

# Challenge from Bot Authentication
if request.headers.get("content-type") == "application/json":
Expand Down Expand Up @@ -93,7 +104,11 @@ async def message_actions(request: Request) -> JSONResponse:
logger.warning(f"Received a Fake Slack Message Action! {raw_body}")
return JSONResponse({"error": "Invalid signature"}, status_code=403)

form_data = urllib.parse.parse_qs(raw_body.decode("utf-8"))
try:
form_data = urllib.parse.parse_qs(raw_body.decode("utf-8"))
except UnicodeDecodeError:
return JSONResponse({"error": "Invalid request body"}, status_code=400)

payload = form_data.get("payload", [None])[0]

if payload is None:
Expand Down
65 changes: 49 additions & 16 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import os
import json
import logging
import os
from typing import overload

from dotenv import load_dotenv
from typing import Any

load_dotenv()

logger: logging.Logger = logging.getLogger(__name__)


def _get_env_variable(name: str, default: str | None = None) -> str | Any:
@overload
def _get_env_variable(name: str, default: None = None) -> str | None: ...


@overload
def _get_env_variable(name: str, default: str) -> str: ...


def _get_env_variable(name: str, default: str | None = None) -> str | None:
"""
Retrieves an environment variable, with an optional default value.

Expand Down Expand Up @@ -38,9 +47,37 @@ def _get_env_variable(name: str, default: str | None = None) -> str | Any:

BASE_DIR: str = os.path.dirname(os.path.abspath(__file__))

_raw_logging_level: str = _get_env_variable("LOGGING_LEVEL", "DEBUG")
LOGGING_LEVEL: int = logging.INFO

match _raw_logging_level:
case "DEBUG":
LOGGING_LEVEL = logging.DEBUG
case "WARN":
LOGGING_LEVEL = logging.WARN
case "ERROR":
LOGGING_LEVEL = logging.ERROR
case "FATAL":
LOGGING_LEVEL = logging.FATAL
case "CRITICAL":
LOGGING_LEVEL = logging.CRITICAL

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(
_get_env_variable("WATCHED_CHANNELS", "0,1,2").split(",")
)

SLACK_DM_TEMPLATE_FILEPATH: str = os.path.join(
BASE_DIR, "static", "slack", "dm_request_template.json"
)
SLACK_MEETINGS_KEYWORD: str = _get_env_variable("SLACK_MEETINGS_KEYWORD", "meeting")
SLACK_NONTECHNICAL_SEMINAR_KEYWORD: str = _get_env_variable("SLACK_NONTECHNICAL_SEMINAR_KEYWORD", "non-technical")
SLACK_TECHNICAL_SEMINAR_KEYWORD: str = _get_env_variable("SLACK_TECHNICAL_SEMINAR_KEYWORD", "technical")
SLACK_NONTECHNICAL_SEMINAR_KEYWORD: str = _get_env_variable(
"SLACK_NONTECHNICAL_SEMINAR_KEYWORD", "non-technical"
)
SLACK_TECHNICAL_SEMINAR_KEYWORD: str = _get_env_variable(
"SLACK_TECHNICAL_SEMINAR_KEYWORD", "technical"
)

SLACK_ALLOW_ANNOUNCEMENTS: bool = (
_get_env_variable("SLACK_ALLOW_ANNOUNCEMENTS", "false") == "true"
Expand All @@ -51,14 +88,9 @@ def _get_env_variable(name: str, default: str | None = None) -> str | Any:
SLACK_FROSH_GROUP_ID: str = _get_env_variable("SLACK_FROSH_GROUP_ID", "")
SLACK_TEST_GROUP_ID: str = _get_env_variable("SLACK_TEST_GROUP_ID", "")

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

WATCHED_CHANNELS: tuple[str] = tuple(
_get_env_variable("WATCHED_CHANNELS", "0,1,2").split(",")
)
SLACK_DM_TEMPLATE: dict | None = None
SLACK_DM_TEMPLATE: list | None = None

CALENDAR_URL: str = _get_env_variable("CALENDAR_URL", "")
CALENDAR_OUTLOOK_DAYS: int = int(_get_env_variable("CALENDAR_OUTLOOK_DAYS", "7"))
Expand All @@ -67,9 +99,10 @@ def _get_env_variable(name: str, default: str | None = None) -> str | Any:
CALENDAR_CACHE_REFRESH: int = int(_get_env_variable("CALENDAR_CACHE_REFRESH", "10"))

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)
WIKIBOT_USER: str = _get_env_variable("WIKIBOT_USER", "")
WIKIBOT_PASSWORD: str = _get_env_variable("WIKIBOT_PASSWORD", "")
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)
if os.path.exists(SLACK_DM_TEMPLATE_FILEPATH):
with open(SLACK_DM_TEMPLATE_FILEPATH, mode="r") as f:
SLACK_DM_TEMPLATE = json.load(f)
Loading
Loading