Posts the daily USI mensa menu to @usi_mensa on Telegram and/or to a Discord channel via webhook, around 10:00 Europe/Zurich.
Menus are currently treated as identical across the configured campuses, so the bot sends one combined channel message. On Mondays, it first sends a week-at-a-glance preview for Monday-Friday, marked as tentative because the official menu may still change.
This project is unofficial and was created independently. It is not affiliated with, endorsed by, or maintained by the Universita della Svizzera italiana (USI), SV Group, or the USI mensa.
- Scrapes the official SV Gastronomie menu page with Playwright.
- Posts to Telegram and/or Discord — configure one or both.
- Sends a daily combined menu for all configured campuses.
- Sends a Monday-only weekly preview before the daily message.
- Clicks the real date tabs when collecting weekly menus.
- Uses retry/backoff for transient scrape failures.
- Falls back to the official menu link when scraping breaks.
- Runs from GitHub Actions on a weekday schedule, with a Zurich local-time gate for DST safety.
| Telegram | Discord |
|---|---|
![]() |
![]() |
Sent on Mondays before the daily message, on both Telegram and Discord (as a code block).
USI Mensa - Week at a glance
Tentative, may change. Check the official menu page for updates.
Day Menu
---------- ------------------------------------------------------------
Mon 20 Apr Autentico: Cordon bleu di pollo; Pasta: Penne alla norma; Giardino: Piccata di tofu alla crema di datterini
Tue 21 Apr Autentico: Spezzatino di maiale ai funghi; Pasta: Fusilli chiles en nogada; Giardino: Tomino del boscaiolo
Wed 22 Apr Autentico: Polpette di carne al pomodoro; Pasta: Pasta con seppioline; Giardino: Frittata pomodorini e mozzarella
- channel_job.py is the scheduled entry point. It checks runtime gates, fetches menu data, builds messages, and sends them to the configured platforms.
- menu_fetcher.py handles Playwright page loading, day-tab clicks, text extraction, parsing, and message formatting (both Telegram HTML and Discord embeds).
- campus.py stores the campus display names and menu URLs.
- .github/workflows/send-channel.yml installs dependencies, installs Chromium, and runs the job.
The workflow runs at 05:00 UTC on weekdays. GitHub Actions can fire scheduled jobs 3–5+ hours late, so the Python-side time gate accepts an 8-hour window (07:00–15:00 Europe/Zurich) rather than an exact hour.
Manual workflow_dispatch runs bypass that time gate for testing.
Daily message:
- Sent Monday-Friday when the current day menu parses successfully.
- Contains all parsed menu cards and prices.
- Uses one combined campus heading.
Monday weekly preview:
- Sent only on Mondays.
- Sent before the normal daily message.
- Sent only if Monday's normal daily menu parsed successfully.
- Reuses Monday's already-fetched menu.
- Scrapes Tuesday-Friday in one shared browser session by clicking each date tab.
- Formats as a monospace table (Telegram) or a code-block embed (Discord).
- Includes a "Tentative, may change" note.
Failure handling:
- No day section found: skip, likely closed/holiday/no menu published.
- Day section found but zero parsed cards: send a fallback status message with the official link.
- Scrape/fetch error after retries: send a fallback status message with the official link.
- Weekly preview batch issue: log the error and continue to the normal daily message.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m playwright install chromiumAt least one platform must be configured. Set only the credentials you need — the other platform is silently skipped.
Telegram only:
TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=... TIMEZONE=Europe/Zurich \
python channel_job.pyDiscord only:
DISCORD_WEBHOOK_URL=... TIMEZONE=Europe/Zurich \
python channel_job.pyBoth at once:
TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=... DISCORD_WEBHOOK_URL=... TIMEZONE=Europe/Zurich \
python channel_job.pyAdd GITHUB_EVENT_NAME=workflow_dispatch to bypass the local-time gate during testing.
At least one of Telegram or Discord must be configured. Both can be active at the same time.
Just join @usi_mensa on Telegram — no setup needed.
If you're forking this repo for your own channel:
- Add
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDas GitHub Actions secrets. - Make sure the Telegram bot is allowed to post in the target channel.
- See the Telegram Bot documentation for creating a bot and obtaining its token and your channel's chat ID.
- In your Discord server, go to Server Settings → Integrations → Webhooks → New Webhook.
- Choose the channel, copy the webhook URL.
- Add it as a
DISCORD_WEBHOOK_URLGitHub Actions secret.
That's it — no bot account needed. The daily menu posts as a rich embed, and the Monday weekly preview posts as a monospace table in a code block.
If you want the bot for your own server or channel, fork the repo and add only the secrets you need. If you only add DISCORD_WEBHOOK_URL, Telegram is silently skipped. If you only add the Telegram secrets, Discord is silently skipped.
Keep the workflow enabled and use workflow_dispatch for a manual smoke test when needed. The workflow has a 15 minute timeout so a stuck browser install or scrape cannot run indefinitely.
Edit campus.py to add, remove, or rename campuses. The current channel message assumes menus are identical across campuses and scrapes only DEFAULT_CAMPUS.
- Do not commit
.env; it is ignored. - Keep all credentials (
TELEGRAM_BOT_TOKEN,TELEGRAM_CHAT_ID,DISCORD_WEBHOOK_URL) in GitHub Actions secrets. The Discord webhook URL is a full secret — anyone with it can post to your channel, so treat it like a password and regenerate it if accidentally exposed. - The scraper depends on the live SV Gastronomie website. If the site changes its tab markup or text structure, the scrape may need adjustment.
MIT License. See LICENSE.


