"""The HedgeDoc note, as markdown. It only covers challenges included in this CTF.""" from __future__ import annotations from collections import Counter from datetime import datetime from .challenge import BLOCKED_ICON, STATUS_ICON, STATUS_ORDER from .overview import Overview, icons REPO_URL = "https://gitea.cato447.de/cato447/challenge_status_tracker" def md_cell(v: str) -> str: return v.replace("|", "\\|") def render_hedgedoc(ov: Overview, title: str, tags: str = "") -> str: stamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") stages = ov.stages groups = ov.by_category() total = Counter(ch.stage for ch in ov.challenges) L = [] if tags: # HedgeDoc reads this YAML block off the top of the note L += ["---", f"tags: {tags}", "---", ""] L += [f"# {title}", "", f"**Last updated:** {stamp}", "", f"*generated by [challenge_status_tracker]({REPO_URL}) from the README.md of every challenge " f"included in this CTF — {len(ov.challenges)} challenges*", "", " · ".join(f"{STATUS_ICON[s]} **{s or 'unknown'}** {total[s]}" for s in stages if total[s] or s), ""] ready = ov.testable L += [f"## ✅ Ready for infra testing ({len(ready)} of {len(ov.challenges)})", "", "*every included challenge at Testing Ready or beyond whose repo backs that up — " "these can be deployed and tested against the infrastructure now*", ""] if ready: L += ["| | Challenge | Category | Difficulty | Status | Event | Author | Tester |", "| --- | --- | --- | --- | --- | --- | --- | --- |"] for ch in ready: L.append("| " + " | ".join(md_cell(x) for x in ( STATUS_ICON[ch.stage], f"[{ch.title}]({ch.repo.web_url})", ch.category or "?", ch.difficulty or "?", ch.stage, ch.get("Event"), ch.get("Author"), ch.get("Tester") if ch.has_tester else "—")) + " |") else: L.append("*nothing yet*") L.append("") if ov.blocked: L += [f"## {BLOCKED_ICON} Claims Testing Ready+, but the repo disagrees " f"({len(ov.blocked)})", "", "*the README says so, but description.yml or the repo shows it is not " "finished — kept out of challs.txt until this is fixed*", ""] for ch in ov.blocked: L.append(f"- {STATUS_ICON[ch.stage]} [{md_cell(ch.title)}]({ch.repo.web_url}) " f"({ch.stage}): " + "; ".join(ch.blockers)) L.append("") L += ["## Status by category", "", "| Category | " + " | ".join(f"{STATUS_ICON[s]} {s or '?'}" for s in stages) + " | total |", "| --- |" + " ---: |" * (len(stages) + 1)] for cat in ov.categories: chs = groups.get(cat, []) cnt = Counter(ch.stage for ch in chs) L.append(f"| {cat} | " + " | ".join(str(cnt[s] or "·") for s in stages) + f" | **{len(chs)}** |") L.append("| **total** | " + " | ".join(f"**{total[s]}**" for s in stages) + f" | **{len(ov.challenges)}** |") diffs = ov.difficulties L += ["", "## Category × difficulty", "", "*one dot per challenge, coloured by status: " + " ".join(f"{STATUS_ICON[s]} {s}" for s in STATUS_ORDER) + f" {BLOCKED_ICON} blocked*", "", "| Category | " + " | ".join(diffs) + " | total |", "| --- |" + " --- |" * len(diffs) + " ---: |"] for cat in ov.categories: chs = groups.get(cat, []) cells = [icons([ch for ch in chs if (ch.difficulty or "?") == d]) or "·" for d in diffs] L.append(f"| {cat} | " + " | ".join(cells) + f" | **{len(chs)}** |") dtot = Counter(ch.difficulty or "?" for ch in ov.challenges) L.append("| **total** | " + " | ".join(f"**{dtot[d]}**" for d in diffs) + f" | **{len(ov.challenges)}** |") L += ["", "## Challenges"] for cat in ov.categories: chs = groups.get(cat) if not chs: continue L += ["", f"### {cat} ({len(chs)})", "", "| | Challenge | Difficulty | Status | Event | Author | Tester | Last activity |", "| --- | --- | --- | --- | --- | --- | --- | --- |"] for ch in chs: status = ch.stage or "?" if ch.status_raw.lower() != status.lower(): status += f" *({ch.status_raw})*" cat_raw = ch.get("Category", "") name = f"[{md_cell(ch.title)}]({ch.repo.web_url})" if cat_raw.lower() != cat: name += f" *({cat_raw})*" L.append("| " + " | ".join(md_cell(x) for x in ( ch.icon, name, ch.difficulty or ch.get("Difficulty"), status, ch.get("Event"), ch.get("Author"), ch.get("Tester") if ch.has_tester else "—", ch.repo.last_activity)) + " |") L += ["", "## Needs attention", ""] issues = [ch for ch in sorted(ov.challenges, key=lambda c: c.repo.path.lower()) if ch.problems] for ch in issues: L.append(f"- [{md_cell(ch.title)}]({ch.repo.web_url}): " + "; ".join(ch.problems)) for ch in ov.blank: why = "no README" if ch.repo.readme is None else "README is still the blank template" L.append(f"- [{ch.repo.path}]({ch.repo.web_url}): {why}") for path in ov.unknown: L.append(f"- {path}: in challenges.yml, but no such repo") if not issues and not ov.blank and not ov.unknown: L.append("*nothing — every README is filled in. suspicious.*") return "\n".join(L) + "\n"