Files
challenge_status_tracker/README.md
T
2026-08-30 14:25:56 +02:00

3.9 KiB

ctf_status.py

Reads the per-author challenge folders straight off the Nextcloud share (WebDAV, no sync, no mount) and tells you who to scream at.

Setup

cp ctf_status.env.example ctf_status.env && chmod 600 ctf_status.env
$EDITOR ctf_status.env      # app password from Nextcloud → Settings → Security

Use

./ctf_status.py                          # terminal summary
./ctf_status.py -o overview.md           # + HedgeDoc markdown to a file
./ctf_status.py --stdout | wl-copy       # straight to clipboard, paste into HedgeDoc
./ctf_status.py --update                 # overwrite the note in HEDGEDOC_NOTE
./ctf_status.py --update <note-url>      # ... or an explicit one

Keeping a HedgeDoc note up to date

--update overwrites an existing note in place, so the URL you share stays the same. Every run rewrites the whole body, starting with

---
tags: Logistics, h4tum CTF 2026
---

and a **Last updated:** <date> <time> <tz> line under the heading, so a stale note is obvious at a glance. Tags come from HEDGEDOC_TAGS (or --tags).

The note's permission has to be freely (Menu → Permission), because the run is anonymous. HedgeDoc 1.x has no REST API for editing a note, so the script talks the editor's own engine.io/socket.io + OT protocol over long-polling — it reads the current text, sends one "delete everything, insert this" operation, waits for the server's ack, and reads the note back to confirm. Anyone with the note open sees the change live. Still requests-only, no new dependencies.

--hedgedoc <instance> (POST a new note) is unrelated and only works where anonymous note creation is enabled — on hedgedoc.cit.tum.de it is not.

Every 30 minutes, on a server

Copy the repo to the server, create ctf_status.env there (it is gitignored, so it does not travel with the code), then:

./install-timer.sh

It writes systemd user units into ~/.config/systemd/user/, substituting whatever directory the repo actually sits in, enables the timer, turns on lingering so it keeps firing when you are not logged in, and does one run to prove it works. Re-run it any time; it just overwrites the units.

systemctl --user list-timers ctf-status.timer   # when it next fires
journalctl --user -u ctf-status.service -n 50   # what it did
systemctl --user start ctf-status.service       # run it now
systemctl --user disable --now ctf-status.timer # stop it

The timer fires at :00 and :30 with up to a minute of jitter, and Persistent=true catches up a run missed while the machine was down. Plain cron works too, if you would rather:

*/30 * * * * /path/to/planner/ctf_status.py --update --quiet

--quiet keeps a successful run silent, so you only hear about it when the note changed or something broke. Overlapping runs are prevented by a lock file, and the HedgeDoc write is retried (--retries, default 3) before the run gives up with a one-line error and a non-zero exit.

What it flags

verdict meaning
🔴 SCREAM nothing submitted, or they filled out template.md in place
🟡 NAG submitted, but placeholders / empty sections remain
🟢 OK every challenge fully filled out

A template.md that is still pristine is ignored. One that has real content in it is counted as a challenge and flagged — so nobody's work is lost, they just get yelled at.

Also flagged per challenge: unnamed challenge, [placeholder] values left in any of Category/Difficulty/Event/Author/Tester/Status, empty prose sections, and category/difficulty values outside the allowed set.

Layout it expects

<NC_ROOT>/
  alice/
    README.md          # ignored
    template.md        # ignored while pristine
    baby-rsa.md        # ← a challenge, any filename
  bob/

Any .md that isn't README.md is parsed as a challenge, so the file naming convention doesn't have to be enforced by the script. If you want it enforced, add the check in Challenge.problems.