diff --git a/devenv.nix b/devenv.nix
index 85e5426..c46ffe5 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -18,15 +18,9 @@
sync.allExtras = true;
};
};
- typst = {
- enable = true;
- };
};
packages = [
- pkgs.google-fonts
- pkgs.hanken-grotesk
- pkgs.typstyle
pkgs.uv
];
diff --git a/pyproject.toml b/pyproject.toml
index ec68b5c..185fa2f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,10 +18,8 @@ dependencies = [
"cssselect==1.3.0",
"cssutils==2.11.1",
"discord.py==2.7.1",
- "fastapi[standard]>=0.136.1",
"html5lib==1.1",
"idna==3.10",
- "immichpy>=4.1.3",
"Jinja2==3.1.6",
"lxml==6.0.0",
"MarkupSafe==3.0.2",
@@ -29,12 +27,10 @@ dependencies = [
"premailer==3.10.0",
"python-dateutil==2.9.0.post0",
"python-dotenv==1.1.1",
- "pytz==2026.1.post1",
"requests==2.32.4",
"six==1.17.0",
"soupsieve==2.7",
"typing_extensions==4.14.0",
- "typst>=0.14.8",
"urllib3==2.5.0",
"webencodings==0.5.1",
]
diff --git a/src/kronos-ui/__init__.py b/src/kronos-ui/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/kronos-ui/main.py b/src/kronos-ui/main.py
deleted file mode 100644
index 259ae48..0000000
--- a/src/kronos-ui/main.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from datetime import datetime
-import json
-import os
-import shutil
-
-from fastapi import FastAPI, File, UploadFile, Form
-from fastapi.responses import HTMLResponse, Response
-from fastapi.staticfiles import StaticFiles
-import typst
-
-from kronos.mobilizon import get_raw_events
-
-app = FastAPI()
-
-app.mount("/static", StaticFiles(directory="src/kronos-ui/static/"), name="static")
-
-
-@app.get("/", response_class=HTMLResponse)
-async def root():
- with open("src/kronos-ui/static/index.html", "rb") as f:
- index = f.read()
- return index
-
-
-@app.get("/calendar", responses={200: {"content": {"image/png": {}}}}, response_class=Response)
-async def calendar():
- get_raw_events()
-
- png_bytes = typst.compile("src/typst/calendar.typ", format="png")
-
- return Response(content=png_bytes, media_type="image/png")
-
-
-@app.post("/event_card", responses={200: {"content": {"image/png": {}}}}, response_class=Response)
-async def event_card( # noqa: PLR0913
- title: str = Form(""),
- description: str = Form(""),
- begins=Form(...),
- ends=Form(...),
- speaker: str = Form(""),
- picture: UploadFile = File(...),
-):
- pic_path = os.path.join("src", "typst", "assets", "tmp", f"{datetime.now()}.jpg")
- fp = open(pic_path, "wb")
- if picture and picture.size > 0:
- shutil.copyfileobj(picture.file, fp)
- else:
- with open(os.path.join("src", "typst", "assets", "photo.jpg"), "rb") as placeholder:
- shutil.copyfileobj(placeholder, fp)
-
- # TODO: pack this in a method
- events = [
- {
- "title": title,
- "beginsOn": begins,
- "endsOn": ends,
- "description": description,
- "physicalAddress": None,
- "picture": os.path.relpath(pic_path, start="src/typst"),
- "url": None,
- "txt_description": description,
- "txt_date": f"{begins.split('T')[0]} {begins.split('T')[1]}-{ends.split('T')[1]}",
- # "txt_date": "2026-04-29 18:00-19:00",
- "speaker": speaker,
- }
- ]
- sys_inputs = {"events": json.dumps(events)}
- print(sys_inputs)
- # FIXME: remove hardcoded paths
- png_bytes = typst.compile("src/typst/event.typ", format="png", sys_inputs=sys_inputs)
-
- fp.close()
-
- return Response(content=png_bytes, media_type="image/png")
diff --git a/src/kronos-ui/setzer.py b/src/kronos-ui/setzer.py
deleted file mode 100644
index 6f38ff7..0000000
--- a/src/kronos-ui/setzer.py
+++ /dev/null
@@ -1,3 +0,0 @@
-import typst
-
-typst.compile("src/typst/calendar.typ", output="calendar.pdf")
diff --git a/src/kronos-ui/static/index.html b/src/kronos-ui/static/index.html
deleted file mode 100644
index dcbe7af..0000000
--- a/src/kronos-ui/static/index.html
+++ /dev/null
@@ -1,164 +0,0 @@
-
-
-
-
-
- Kronos UI -- Generator Genialnych Grafik
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Kronos
-
- Kalendarz
- Karta
-
-
-
-
Kalendarz
-
-
-

-
-
-
-
Karta
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/kronos/main.py b/src/kronos/main.py
index 8db068b..25e9219 100755
--- a/src/kronos/main.py
+++ b/src/kronos/main.py
@@ -4,6 +4,7 @@ import sys
from zoneinfo import ZoneInfo
from argparse import ArgumentParser
+import requests
from bs4 import BeautifulSoup
from dotenv import load_dotenv
from jinja2 import Environment, FileSystemLoader, select_autoescape
@@ -11,13 +12,13 @@ from premailer import transform
from .discordbot import KronosBot, intents
-from .mobilizon import get_raw_events
-
+MOBILIZON_API_URL = "https://events.hs3.pl/api"
+QUERY_LIMIT = 100
+TIME_WINDOW_DAYS = 365
TRUNCATE_AFTER_CHARACTERS = 300
-TEMPLATES = {
- "templates/newsletter_template.html": "newsletter_events.html",
-}
+TEMPLATE_FILENAME = "templates/newsletter_template.html"
+OUTPUT_FILENAME = "newsletter_events.html"
def sanitize_html(raw_html: str) -> str:
@@ -28,8 +29,69 @@ def sanitize_html(raw_html: str) -> str:
return cleaned
+def get_time_window(days: int = 365):
+ now_utc = datetime.datetime.now(datetime.timezone.utc)
+ ends_utc = now_utc + datetime.timedelta(days=days)
+
+ begins_iso = now_utc.replace(microsecond=0).isoformat().replace("+00:00", "Z")
+ ends_iso = ends_utc.replace(microsecond=0).isoformat().replace("+00:00", "Z")
+ return begins_iso, ends_iso
+
+
+def build_graphql_query():
+ return """
+ query SearchEventsInWindow($beginsOn: DateTime, $endsOn: DateTime, $limit: Int) {
+ searchEvents(beginsOn: $beginsOn, endsOn: $endsOn, limit: $limit) {
+ total
+ elements {
+ __typename
+ ... on Event {
+ id
+ title
+ description
+ beginsOn
+ picture {
+ url
+ }
+ url
+ physicalAddress {
+ description
+ locality
+ }
+ }
+ }
+ }
+ }
+ """
+
+
+def fetch_events(begins_on: str, ends_on: str, limit: int = QUERY_LIMIT):
+ query = build_graphql_query()
+ variables = {"beginsOn": begins_on, "endsOn": ends_on, "limit": limit}
+ payload = {"query": query, "variables": variables}
+ headers = {"Content-Type": "application/json"}
+
+ resp = requests.post(MOBILIZON_API_URL, json=payload, headers=headers)
+ resp.raise_for_status()
+ data = resp.json()
+ if "errors" in data:
+ raise RuntimeError(f"GraphQL error: {data['errors']}")
+
+ events = []
+ for elem in data["data"]["searchEvents"]["elements"]:
+ if elem.get("__typename") == "Event":
+ begins_iso = elem.get("beginsOn")
+ if begins_iso:
+ dt = datetime.datetime.fromisoformat(begins_iso.replace("Z", "+00:00"))
+ dt_begins = datetime.datetime.fromisoformat(begins_on.replace("Z", "+00:00"))
+ dt_ends = datetime.datetime.fromisoformat(ends_on.replace("Z", "+00:00"))
+ if dt_begins <= dt < dt_ends:
+ events.append(elem)
+ return events
+
+
def prepare_events_for_template(raw_events: list) -> list:
- days_week = [
+ jours_semaine = [
"poniedziałek",
"wtorek",
"środa",
@@ -38,7 +100,7 @@ def prepare_events_for_template(raw_events: list) -> list:
"sobota",
"niedziela",
]
- month_year = [
+ mois_annee = [
"",
"stycznia",
"lutego",
@@ -70,7 +132,6 @@ def prepare_events_for_template(raw_events: list) -> list:
begins_iso = ev.get("beginsOn")
picture = ev.get("picture")
picture_url = picture.get("url") if picture else None
- picture_filename = ev.get("picture_filename")
link = ev.get("url") or ""
if picture_url and "?" in picture_url:
@@ -85,15 +146,15 @@ def prepare_events_for_template(raw_events: list) -> list:
location = ""
dt_utc = datetime.datetime.fromisoformat(begins_iso.replace("Z", "+00:00"))
- dt_warsaw = dt_utc.astimezone(ZoneInfo("Europe/Warsaw"))
- day_name = days_week[dt_warsaw.weekday()]
- day_num = dt_warsaw.day
- month_name = month_year[dt_warsaw.month]
- year = dt_warsaw.year
- hour = dt_warsaw.hour
- minute = dt_warsaw.minute
+ dt_paris = dt_utc.astimezone(ZoneInfo("Europe/Paris"))
+ jour_nom = jours_semaine[dt_paris.weekday()]
+ jour_num = dt_paris.day
+ mois_nom = mois_annee[dt_paris.month]
+ annee = dt_paris.year
+ heure = dt_paris.hour
+ minute = dt_paris.minute
minute_str = f"{minute:02d}"
- full_date = f"{day_name}, {day_num} {month_name} {year} @ {hour}:{minute_str}"
+ full_date = f"{jour_nom}, {jour_num} {mois_nom} {annee} @ {heure}:{minute_str}"
prepared.append(
{
@@ -101,7 +162,6 @@ def prepare_events_for_template(raw_events: list) -> list:
"description": truncated,
"full_date": full_date,
"picture_url": picture_url,
- "picture_filename": picture_filename,
"location": location,
"link": link,
"start_time": dt_utc,
@@ -139,27 +199,45 @@ def _main():
_args = parser.parse_args()
try:
- raw_events = get_raw_events()
+ begins_on, ends_on = get_time_window(days=TIME_WINDOW_DAYS)
+ print(
+ f"Fetching events between {begins_on} and {ends_on}...",
+ file=sys.stderr,
+ )
+
+ raw_events = list(
+ filter(
+ lambda x: "https://events.hs3.pl/" in x.get("url", ""),
+ fetch_events(begins_on, ends_on),
+ )
+ )
+ if not raw_events:
+ print("No events found in the requested period.", file=sys.stderr)
+ sys.exit(0)
+
+ for ev in raw_events:
+ print(f"{ev.get('title', 'Untitled')} — {ev.get('beginsOn', 'Unknown date')}")
+
+ raw_events.sort(key=lambda ev: ev.get("beginsOn", ""))
+
events = prepare_events_for_template(raw_events)
- for template_filename, output_filename in TEMPLATES.items():
- output = render_newsletter(
- events=events, template_dir=os.getcwd(), template_name=template_filename
- )
+ os.makedirs("dist", exist_ok=True)
+ output_path = os.path.join(os.getcwd(), "dist", OUTPUT_FILENAME)
+ html_output = render_newsletter(
+ events=events, template_dir=os.getcwd(), template_name=TEMPLATE_FILENAME
+ )
- os.makedirs("dist", exist_ok=True)
- output_path = os.path.join(os.getcwd(), "dist", output_filename)
- with open(output_path, "w", encoding="utf-8") as f:
- f.write(output)
- print(f"File '{output_path}' generated successfully.", file=sys.stderr)
+ with open(output_path, "w", encoding="utf-8") as f:
+ f.write(html_output)
+ print(f"File '{output_path}' generated successfully.", file=sys.stderr)
- if output_filename.endswith(".html"):
- inlined_output_path = os.path.join(os.getcwd(), "dist", f"inlined_{output_filename}")
- inline_css(output_path, inlined_output_path)
- print(
- f"File '{inlined_output_path}' (inline CSS) generated.",
- file=sys.stderr,
- )
+ inlined_output_path = os.path.join(os.getcwd(), "dist", "newsletter_events_inlined.html")
+ inline_css(output_path, inlined_output_path)
+ print(
+ f"File '{inlined_output_path}' (inline CSS) generated.",
+ file=sys.stderr,
+ )
discord_token = os.getenv("DISCORD_TOKEN")
client = KronosBot(intents=intents)
diff --git a/src/kronos/mobilizon.py b/src/kronos/mobilizon.py
deleted file mode 100644
index 9881c09..0000000
--- a/src/kronos/mobilizon.py
+++ /dev/null
@@ -1,143 +0,0 @@
-import datetime
-import json
-import os
-import sys
-import requests
-
-from bs4 import BeautifulSoup
-from pytz import timezone
-
-MOBILIZON_API_URL = "https://events.hs3.pl/api"
-QUERY_LIMIT = 100
-TIME_WINDOW_DAYS = 365
-
-
-def get_time_window(start_utc=datetime.datetime.now(datetime.timezone.utc), days: int = 365):
- now_utc = start_utc
- ends_utc = now_utc + datetime.timedelta(days=days)
-
- begins_iso = now_utc.replace(microsecond=0).isoformat().replace("+00:00", "Z")
- ends_iso = ends_utc.replace(microsecond=0).isoformat().replace("+00:00", "Z")
- return begins_iso, ends_iso
-
-
-def build_graphql_query():
- return """
- query SearchEventsInWindow($beginsOn: DateTime, $endsOn: DateTime, $limit: Int) {
- searchEvents(beginsOn: $beginsOn, endsOn: $endsOn, limit: $limit) {
- total
- elements {
- __typename
- ... on Event {
- id
- title
- description
- beginsOn
- endsOn
- picture {
- url
- }
- url
- physicalAddress {
- description
- locality
- }
- }
- }
- }
- }
- """
-
-
-def fetch_events(begins_on: str, ends_on: str, limit: int = QUERY_LIMIT):
- query = build_graphql_query()
- variables = {"beginsOn": begins_on, "endsOn": ends_on, "limit": limit}
- payload = {"query": query, "variables": variables}
- headers = {"Content-Type": "application/json"}
-
- resp = requests.post(MOBILIZON_API_URL, json=payload, headers=headers)
- resp.raise_for_status()
- data = resp.json()
- if "errors" in data:
- raise RuntimeError(f"GraphQL error: {data['errors']}")
-
- events = []
- for elem in data["data"]["searchEvents"]["elements"]:
- if elem.get("__typename") == "Event":
- begins_iso = elem.get("beginsOn")
- if begins_iso:
- dt = datetime.datetime.fromisoformat(begins_iso.replace("Z", "+00:00"))
- dt_begins = datetime.datetime.fromisoformat(begins_on.replace("Z", "+00:00"))
- dt_ends = datetime.datetime.fromisoformat(ends_on.replace("Z", "+00:00"))
- if dt_begins <= dt < dt_ends:
- events.append(elem)
- return events
-
-
-def save_json_content(events):
- json_dirname = "src/typst/content"
- os.makedirs(json_dirname, exist_ok=True)
- with open(os.path.join(json_dirname, "events.json"), "w") as f:
- json.dump(sorted(events, key=lambda event: event["beginsOn"]), f)
-
-
-def extract_picture(event):
- picture = event.get("picture")
- picture_url = picture.get("url") if picture else None
- picture_filename = None
-
- if picture_url:
- if "?" in picture_url:
- picture_url = picture_url.split("?", 1)[0]
-
- picture_dirname = "src/typst/assets/tmp"
- picture_filename = os.path.join(picture_dirname, picture_url.split("/")[-1])
- event["picture_filename"] = picture_filename
-
- response = requests.get(picture_url)
-
- os.makedirs(picture_dirname, exist_ok=True)
- with open(picture_filename, "wb") as picture_file:
- picture_file.write(response.content)
-
-
-def get_raw_events():
- begins_on, ends_on = get_time_window(days=TIME_WINDOW_DAYS)
- print(
- f"Fetching events between {begins_on} and {ends_on}...",
- file=sys.stderr,
- )
-
- raw_events = list(
- filter(
- lambda x: "https://events.hs3.pl/" in x.get("url", ""),
- fetch_events(begins_on, ends_on),
- )
- )
- if not raw_events:
- print("No events found in the requested period.", file=sys.stderr)
- sys.exit(0)
-
- for ev in raw_events:
- print(f"{ev.get('title', 'Untitled')} — {ev.get('beginsOn', 'Unknown date')}")
-
- ev["txt_description"] = BeautifulSoup(ev["description"], "html.parser").get_text()
-
- local_tz = timezone("Europe/Warsaw")
- begins_date = datetime.datetime.fromisoformat(ev["beginsOn"]).astimezone(local_tz)
- ends_date = datetime.datetime.fromisoformat(ev["endsOn"]).astimezone(local_tz)
- begins = (begins_date.strftime("%Y-%m-%d"), begins_date.strftime("%H:%M"))
- ends = (ends_date.strftime("%Y-%m-%d"), ends_date.strftime("%H:%M"))
- txt_date = f"{begins[0]} {begins[1]}-"
- if begins[0] != ends[0]:
- txt_date += f"{ends[0]}"
- txt_date += f"{ends[1]}"
- ev["txt_date"] = txt_date
-
- extract_picture(ev)
-
- save_json_content(raw_events)
-
- raw_events.sort(key=lambda ev: ev.get("beginsOn", ""))
-
- return raw_events
diff --git a/src/typst/.gitignore b/src/typst/.gitignore
deleted file mode 100644
index 20634ad..0000000
--- a/src/typst/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-assets/tmp/*
-*.pdf
diff --git a/src/typst/assets/calendar.svg b/src/typst/assets/calendar.svg
deleted file mode 100644
index 69b9967..0000000
--- a/src/typst/assets/calendar.svg
+++ /dev/null
@@ -1,25941 +0,0 @@
-
-
-
-
diff --git a/src/typst/assets/event.svg b/src/typst/assets/event.svg
deleted file mode 100644
index 517ccb8..0000000
--- a/src/typst/assets/event.svg
+++ /dev/null
@@ -1,1244 +0,0 @@
-
-
-
-
diff --git a/src/typst/assets/hs3_logo_hires.png b/src/typst/assets/hs3_logo_hires.png
deleted file mode 100644
index a78beac..0000000
Binary files a/src/typst/assets/hs3_logo_hires.png and /dev/null differ
diff --git a/src/typst/assets/logo.png b/src/typst/assets/logo.png
deleted file mode 100644
index dd8e793..0000000
Binary files a/src/typst/assets/logo.png and /dev/null differ
diff --git a/src/typst/assets/photo.jpg b/src/typst/assets/photo.jpg
deleted file mode 100644
index 718b180..0000000
Binary files a/src/typst/assets/photo.jpg and /dev/null differ
diff --git a/src/typst/calendar.typ b/src/typst/calendar.typ
deleted file mode 100644
index 622fb6a..0000000
--- a/src/typst/calendar.typ
+++ /dev/null
@@ -1,88 +0,0 @@
-#import "@preview/cades:0.3.1": qr-code
-
-#set page(
- paper: "a4",
- background: context {
- place(center + horizon, {
- image("assets/calendar.svg")
- })
- },
- margin: (top: .5cm, bottom: .5cm, x: 0cm),
-)
-
-#place(
- top + left,
- dx: 8mm,
- dy: -14mm,
- image("assets/hs3_logo_hires.png", height: 58mm, width: 58mm),
-)
-
-#place(
- top + left,
- dx: 13mm,
- dy: 145mm,
- qr-code("https://hs3.pl", width: 32mm),
-)
-
-#let add_event(number, title, description, date, address) = {
- let ex = 85mm + calc.rem-euclid(number, 2) * 67mm
- let ey = 46mm + calc.div-euclid(number, 2) * 76mm
-
- set text(
- fill: white,
- font: "Rajdhani",
- )
-
- place(
- top + left,
- dx: ex,
- dy: ey,
- block(
- width: 32mm,
- height: 48mm,
-
- [
- #block(
- height: 14mm,
- text(
- weight: "bold",
- title,
- ),
- )
- #move(dy: 3mm, block(
- height: 8mm,
- clip: true,
- text(
- weight: "bold",
- size: 10pt,
- date,
- ),
- ))
- #move(dy: 4mm, block(
- height: 12mm,
- clip: true,
- text(size: 10pt, address),
- ))
- ],
- ),
- )
-}
-
-#let events = json("content/events.json")
-#let index = 0
-
-#for event in events {
- add_event(
- index,
- event.title,
- event.txt_description,
- event.txt_date,
- "Hackerspace Trójmiasto, al. Wojska Polskiego 41, Gdańsk",
- )
-
- index = index + 1
-
- if (index >= 6) {
- break
- }
-}
diff --git a/src/typst/content/.gitkeep b/src/typst/content/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/typst/event.typ b/src/typst/event.typ
deleted file mode 100644
index 2e665bd..0000000
--- a/src/typst/event.typ
+++ /dev/null
@@ -1,86 +0,0 @@
-#import "@preview/bulb:0.1.0": dither
-
-#set page(
- width: 297mm,
- height: 169mm,
- background: context {
- place(center + horizon, {
- image("assets/event.svg")
- })
- },
- margin: (top: 0cm, bottom: .5cm, x: .5cm),
-)
-
-#let make_event_card(title, description, date, address, picture, name) = {
- move(
- dx: 148mm,
- dy: 23mm,
- image(
- dither(
- read(picture, encoding: none),
- mode: "bw",
- method: "bayer8x8",
- size: 360,
- ),
- ),
- )
- place(
- top + left,
- dx: 94mm,
- dy: -4mm,
- image("assets/hs3_logo_hires.png", height: 38mm, width: 38mm),
- )
-
- set text(font: "Hanken Grotesk", size: 14pt)
-
- place(
- top + left,
- dx: 12mm,
- dy: 30mm,
- block(
- width: 118mm,
- height: 130mm,
- grid(
- rows: (2fr, 6fr, 0.75fr, 0.5fr),
- text(size: 28pt, weight: "bold", title),
- text(size: 10pt, description),
- text(
- size: 24pt,
- weight: "bold",
- grid(
- columns: (1fr, 1fr),
- date.split(" ").at(0), align(right)[#date.split(" ").at(1)],
- ),
- ),
- address,
- ),
- ),
- )
-
- align(right + bottom)[#text(size: 14pt, weight: "bold", name)]
-}
-
-#let events = none
-#if sys.inputs.len() < 1 {
- events = json("content/events.json")
-} else {
- events = json(bytes(sys.inputs.events))
-}
-#let index = 0
-
-#for event in events {
- make_event_card(
- event.title,
- event.txt_description,
- event.txt_date,
- "Gdańsk, al. Wojska Polskiego 41",
- event.picture,
- event.speaker,
- )
-
- index = index + 1
-
- if index < events.len() {
- pagebreak()
- }
-}