
e-Dnevnik bot is a self-hosted alerting system that reads from the official CARNet e-Dnevnik portal and regularly polls for new information. Supported event types:
--readinglist)The bot can log in as multiple AAI/AOSI users from the skole.hr domain and check for new information for all of them, either as a one-shot run or as a long-running service that polls at regular intervals (e.g. hourly). All new, previously unseen events will trigger an alert. The bot can deliver alerts through the following messaging systems and services:
Each alert can be broadcast through multiple services simultaneously, and each service can have multiple recipients. All authentication credentials remain exclusively on your PC or server.
Important note (May 2023): e-Dnevnik bot will not be able to fetch data unless it is hosted inside Croatia, as CARNet has implemented a firewall that blocks access from non-Croatian IP addresses. This affects all popular cloud VM and VPS providers such as Oracle Cloud, Contabo, AWS, Azure, and GitHub Actions. Self-hosting at home or in a local office is strongly recommended.
The bot uses Google’s Application Programming Interface (API) Services to add school exam events to your Google Calendar.
Use of information received from Google APIs complies with the Google API Services User Data Policy, including the Limited Use requirements.
Your Google information is used solely to provide the user-facing calendar integration feature. No data is collected, stored remotely, or tracked in any way. All authorization data (e.g. the Google API token) is stored locally alongside the application and never leaves your premises.
The bot requires:
The bot runs on virtually any embedded device and any supported operating system, using approximately 20–25 MB of RSS memory during normal service operation.
Download the binary from the releases page along with the example configuration file.
NAME
e-dnevnik-bot
FLAGS
-v, --verbose verbose/debug log level
-0, --fulldebug log every scraped event (only with verbose mode)
-d, --daemon enable daemon mode (running as a service)
-?, --help display help
-t, --test send a test event (to check if messaging works)
-l, --colorlogs enable colorized console logs
--version display program version
--readinglist send reading list alerts
-j, --jitter BOOL enable slight (up to 10%) jitter for tick intervals (default: true)
-f, --conffile STRING configuration file (in TOML) (default: .e-dnevnik.toml)
-b, --database STRING alert database file (default: .e-dnevnik.db)
-g, --calendartoken STRING Google Calendar token file (default: calendar_token.json)
-c, --cpuprofile STRING CPU profile output file
-m, --memprofile STRING memory profile output file
-i, --interval DURATION interval between polls when in daemon mode (default: 1h0m0s)
-p, --relevance DURATION maximum relevance period for grade and exam events (0 = unlimited) (default: 0s)
-r, --retries UINT number of retry attempts on error (default: 3)
By default, the bot runs from the current working directory and loads its TOML configuration from .e-dnevnik.toml, or from the file specified with the -f flag.
Other flags:
-b: path to the alert database file used to track seen alerts (default: .e-dnevnik.db); note that the .sqlite extension is appended automatically, so the actual file on disk will be .e-dnevnik.db.sqlite,-d: enables daemon/service mode, where the bot runs continuously and wakes up at regular intervals (specified with -i); disabled by default,-f: path to the configuration file containing usernames, passwords, and messaging service settings (in TOML format),-i: interval between polls in daemon/service mode (minimum 1h, default 1h),-r: number of retry attempts on scraping or delivery failures (default: 3),-t: sends a test message to all configured messaging services; calendar backends (Google Calendar, CalDAV) only ever receive exam events, so they are not exercised by -t,-v: enables verbose/debug logging for detailed insight into bot operation; disabled by default,-l: enables colorized console logging with JSON output disabled,-g: path to the Google Calendar API token file for reading and storing the OAuth2 token,-p: maximum relevance period for grade and exam events; events older than this period are silently skipped (0 = unlimited),--version: displays the program version,-j: enables a ±10% random jitter on the poll interval,--readinglist: enables processing and alerting on reading list events.The configuration file consists of several blocks. The user block can be repeated as many times as needed. The Telegram, Discord, Slack, and e-mail blocks can each appear only once, but all can be enabled or disabled independently. Recipient lists (user IDs, chat IDs, and to addresses) are defined as arrays and support any number of entries. Alerts are broadcast to all enabled messaging services simultaneously.
File permissions are your responsibility. The bot does not enforce restrictive permissions on the configuration file, even though it contains AAI/AOSI passwords and messenger tokens. Set
chmod 0600 .e-dnevnik.toml(or equivalent) yourself, and ensure the file’s directory is not world-readable. Containerised deployments should use a dedicated volume that is mounted with appropriate ownership.The bot may rewrite your configuration file. When the WhatsApp
groupsfield contains a name that resolves to a JID (see the WhatsApp configuration section), and the file is writable by the bot’s user, the resolved JID is migrated fromgroupsintouseridsand the file is rewritten in place. Comments and key ordering are not preserved — the rewrite uses a TOML encoder that emits a canonical form. If you maintain the file under version control or rely on inline comments, either keep the file read-only and manage the JIDs yourself, or expect a single one-time rewrite on first successful resolution.
Credentials are read once, at startup. The configuration file is parsed when the bot starts, and the values it holds — AAI/AOSI passwords, messenger tokens, SMTP credentials — are used unchanged for the lifetime of the process. There is no reload signal. After rotating a password or a token, restart the bot (
systemctl restart e-dnevnik-bot, or recreate the container) before expecting the new value to take effect.
[[user]]
username = "ime.prezime@skole.hr"
password = "lozinka"
As many [[user]] blocks as needed can be specified; all users are processed in parallel.
[telegram]
token = "telegram_bot_token"
chatids = [ "chat_id", "chat_id2" ]
Steps required:
[discord]
token = "discord_bot_token"
userids = [ "user_id", "user_id2" ]
Steps required:
[slack]
token = "xoxb-slack_bot_token"
chatids = [ "chat_id", "chat_id2" ]
Steps required:
[mail]
server = "smtp.gmail.com"
port = "587"
username = "user.name@gmail.com"
password = "legacy_app_password"
from = "user.name@gmail.com"
subject = "Nova ocjena iz e-Dnevnika"
to = [ "user.name@gmail.com", "user2.name2@gmail.com" ]
Upgrading:
fromis now required. It used to be optional, but an emptyfromis rejected by the SMTP library, and the mail messenger treated that as a per-recipient failure — quietly discarding every alert it was given. The address is now validated at startup, so a[mail]block withserverset andfrommissing aborts the bot rather than running with mail silently broken. Because that abort happens before anything starts, the other messengers stop too: addfrombefore upgrading.
Steps required:
[whatsapp]
phonenumber = "+385XXYYYYYYY"
userids = [ "+385XXYYYYYYY@s.whatsapp.net", "XXXYYYYYYY-ZZZZZZZZZZ@s.whatsapp.net" ]
groups = [ "group1", "group2" ]
Steps required:
userids are either a personal JID (in the form +385XXYYYYYYY@s.whatsapp.net, for direct messages) or a group chat JID (in the form XXXYYYYYYY-ZZZZZZZZZZ@s.whatsapp.net). If you do not know a group’s JID, you can specify groups by name in the groups field; the bot will resolve each name to its JID at startup. If the configuration file is writable by the bot, the resolved JIDs will be migrated from groups into userids automatically, so subsequent runs skip the name-resolution step. Using userids directly is preferred for performance reasons..e-dnevnik.wa.sqlite in the working directory. When running in Docker, keep the whole working directory on a persistent volume (as in the examples below) so that the pairing survives container restarts. Do not bind-mount the database file alone: SQLite keeps -wal and -shm files next to each database, and losing them on an unclean stop discards recent session writes.[calendar]
name = "Djeca ispiti"
Steps required:
credentials.json to the working directory.calendar_token.json (configurable with -g). Subsequent runs use the cached token automatically.name field specifies the target Google Calendar by name (e.g. a calendar you created called Djeca ispiti). Only exam events are added as calendar entries.[caldav]
url = "https://cloud.example.com/remote.php/dav/calendars/pero/ispiti/"
username = "pero"
password = "app-password"
An alternative to Google Calendar that needs no Google account and no interactive first run.
url is the full URL of the calendar collection, not the server root. Nextcloud shows it under the calendar’s “Copy private link” menu; Radicale uses http://127.0.0.1:5232/<user>/<calendar>/; Fastmail lists it under Settings → Calendars → Calendar sharing / CalDAV.https. Plain http is accepted only for localhost, 127.0.0.0/8 or ::1, so a Radicale on the same machine works without TLS.-t emulation sends a grade, which calendar backends ignore, so it does not test CalDAV. A 401/403/404 on that first exam is treated as permanent, and that exam is dropped rather than retried. Verify the URL and app password with a CalDAV client beforehand (e.g. by subscribing to the same URL) rather than finding out on the first real exam.http is loopback-only (see point 3): a Docker-network hostname such as http://radicale:5232/ is rejected even though it never leaves your infrastructure. Use https, or run the bot on the same host network as the CalDAV server so it can reach it via localhost.[digest]
enabled = true # set to false to turn the digest off
day = "sunday"
hour = 18
Once a week, each user gets one short message listing every exam in the coming week (Monday to Sunday), sent to all chat messengers (Telegram, Discord, Slack, mail, WhatsApp). Calendar backends ignore it, because they already hold each exam as its own entry.
[digest] section. To turn it off, set enabled = false. day is an English weekday name (default sunday). hour is an hour from 0 to 23 (default 18) in the bot’s local time zone. In Docker, that is UTC unless you set TZ (see below).day at hour, so with the default 1h poll interval it arrives within the hour. If that poll is missed or fails to scrape a user, a later poll still sends it. A late digest lists only exams from that day onward.For a minimal systemd setup running the service as user ubuntu inside /home/ubuntu/e-dnevnik, save the following file to /etc/systemd/system/e-dnevnik.service:
# /etc/systemd/system/e-dnevnik.service
[Unit]
Description=e-Dnevnik daemon
After=network-online.target
[Service]
Type=notify
WatchdogSec=30s
WorkingDirectory=/home/ubuntu/e-dnevnik
ExecStart=/home/ubuntu/e-dnevnik/e-dnevnik-bot --daemon --verbose
Restart=always
RestartSec=2
User=ubuntu
Group=ubuntu
[Install]
WantedBy=multi-user.target
Then enable the service with the usual commands:
systemctl daemon-reload
systemctl enable --now e-dnevnik
systemctl status e-dnevnik
Note: e-dnevnik-bot supports the systemd sd_notify, Type=notify, and WatchdogSec= features from version 0.13.0 onwards. Earlier versions are only compatible with Type=simple.
Up-to-date images for Linux amd64/arm64/arm are available on Docker Hub. To set this up, create a persistent directory named ednevnik in your working directory, place the configuration file there, and mount it into the container:
cd some/workdir
mkdir ednevnik
curl https://raw.githubusercontent.com/dkorunic/e-dnevnik-bot/main/.e-dnevnik.toml.example \
--output ednevnik/.e-dnevnik.toml
editor ednevnik/.e-dnevnik.toml
docker pull dkorunic/e-dnevnik-bot
docker run --detach \
--volume "$(pwd)/ednevnik:/ednevnik" \
--env TZ=Europe/Zagreb \
--restart unless-stopped \
dkorunic/e-dnevnik-bot \
--daemon \
--verbose \
--database /ednevnik/.e-dnevnik.db \
--conffile /ednevnik/.e-dnevnik.toml
To use docker compose, first ensure Docker Compose is installed.
Create a docker-compose.yml file in your project directory (e.g. ~/docker-compose/ednevnik):
user@server:~/docker-compose$ mkdir ednevnik
user@server:~/docker-compose$ cd ednevnik
user@server:~/docker-compose$ editor docker-compose.yml
Paste the following into docker-compose.yml:
# More info at https://github.com/dkorunic/e-dnevnik-bot
services:
ednevnik:
container_name: e-dnevnik
image: dkorunic/e-dnevnik-bot:latest
command:
- "--daemon"
- "--database=/ednevnik/.e-dnevnik.db"
- "--conffile=/ednevnik/.e-dnevnik.toml"
# Local time zone for the weekly digest and log timestamps
environment:
- TZ=Europe/Zagreb
# Volumes store your data between container upgrades
volumes:
- ./ednevnik:/ednevnik
restart: unless-stopped
Inside the project directory, create a directory called ednevnik for persistent storage, then download and edit the configuration file:
user@server:~/docker-compose/ednevnik$ mkdir ednevnik
user@server:~/docker-compose/ednevnik$ curl https://raw.githubusercontent.com/dkorunic/e-dnevnik-bot/main/.e-dnevnik.toml.example \
--output ednevnik/.e-dnevnik.toml
user@server:~/docker-compose/ednevnik$ editor ednevnik/.e-dnevnik.toml
From the project directory where docker-compose.yml is located, start the service with:
user@server:~/docker-compose/ednevnik$ docker compose up -d
The -d / --detach flag runs the containers in the background.
To stop the service, run:
user@server:~/docker-compose/ednevnik$ docker compose down
Warning: CARNet has implemented a firewall that blocks access to e-Dnevnik from non-Croatian IP addresses. GitHub Actions runners use IP addresses outside Croatia, so this integration will fail to scrape data. It is kept here for reference only. For reliable operation, self-host the bot on hardware located in Croatia.
This integration was originally created by Luka Kladaric @allixsenos — thanks, Luka! The original Gist is here; a copy is reproduced below:
name: e-imenik run
# Controls when the action will run.
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Run on push to any branch for testing
push:
# Run every 6 hours
schedule:
- cron: "0 */6 * * *"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render the config file
run: |
cat > .e-dnevnik.toml <<EOF
[[user]]
username = "$SKOLE_USERNAME"
password = "$SKOLE_PASSWORD"
[telegram]
token = "$TELEGRAM_TOKEN"
chatids = [ "$TELEGRAM_CHATID" ]
EOF
env:
SKOLE_USERNAME: $
SKOLE_PASSWORD: $
TELEGRAM_TOKEN: $
TELEGRAM_CHATID: $
- name: Run dkorunic/e-dnevnik-bot
run: |
docker run -t \
--volume "$(pwd):/ednevnik" \
--user $(id -u):$(id -g) \
dkorunic/e-dnevnik-bot \
--verbose \
--database /ednevnik/.e-dnevnik.db \
--conffile /ednevnik/.e-dnevnik.toml
- name: Delete the config file so it doesn't get committed back :)
run: rm .e-dnevnik.toml
- name: Commit the DB
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_author: GitHub Actions <actions@github.com>
This GitHub Actions workflow runs every 6 hours. On each run it checks out the repository (which includes the database), renders the configuration file from secrets, runs the bot, and commits the updated database back to the repository, providing persistence across runs.