feat: Add dashboard support and enhance Discord webhook functionality
- Updated docker-compose.yml to expose dashboard on port 8080. - Enhanced main.py with timezone database caching and improved state management. - Introduced a minimal dashboard using Flask to display webhook status and notifications. - Added templates.json for customizable embed messages in Discord notifications. - Created templates.py for loading and saving notification templates. - Implemented tests for dashboard rendering and main functionality. - Added requirements for Flask and tzdata to support new features. - Included test cases for timezone handling and template management.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_TEMPLATES: dict[str, dict] = {
|
||||
"reminder_halftime": {
|
||||
"text": "Half-time in 5 minutes!",
|
||||
"color": 0xE67E22,
|
||||
},
|
||||
"halftime": {
|
||||
"text": "Half-time!",
|
||||
"color": 0x2ECC71,
|
||||
"image_url": "https://copyparty.allucanget.biz/img/weed.png",
|
||||
},
|
||||
"reminder": {
|
||||
"text": "This is your 5 minute reminder to 420!",
|
||||
"color": 0xE67E22,
|
||||
},
|
||||
"420": {
|
||||
"text": "Blaze it!",
|
||||
"color": 0x2ECC71,
|
||||
"image_url": "https://copyparty.allucanget.biz/img/weed.png",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_templates(raw: dict) -> dict[str, dict]:
|
||||
out: dict[str, dict] = deepcopy(DEFAULT_TEMPLATES)
|
||||
|
||||
if not isinstance(raw, dict):
|
||||
return out
|
||||
|
||||
for key, default in DEFAULT_TEMPLATES.items():
|
||||
incoming = raw.get(key)
|
||||
if not isinstance(incoming, dict):
|
||||
continue
|
||||
|
||||
text = incoming.get("text")
|
||||
if isinstance(text, str):
|
||||
out[key]["text"] = text
|
||||
|
||||
color = incoming.get("color")
|
||||
if isinstance(color, int):
|
||||
out[key]["color"] = color
|
||||
|
||||
image_url = incoming.get("image_url")
|
||||
if isinstance(image_url, str) and image_url.strip():
|
||||
out[key]["image_url"] = image_url.strip()
|
||||
elif "image_url" in default and image_url in (None, ""):
|
||||
# Allow clearing image_url only if explicitly set to empty.
|
||||
out[key].pop("image_url", None)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def load_templates(path: str | Path) -> dict[str, dict]:
|
||||
p = Path(path)
|
||||
try:
|
||||
if not p.exists():
|
||||
return deepcopy(DEFAULT_TEMPLATES)
|
||||
raw = json.loads(p.read_text(encoding="utf-8"))
|
||||
return _normalize_templates(raw)
|
||||
except Exception:
|
||||
return deepcopy(DEFAULT_TEMPLATES)
|
||||
|
||||
|
||||
def save_templates(path: str | Path, templates: dict) -> None:
|
||||
p = Path(path)
|
||||
normalized = _normalize_templates(templates)
|
||||
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = p.with_suffix(p.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(normalized, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
tmp.replace(p)
|
||||
|
||||
|
||||
def parse_color(value: str) -> int:
|
||||
"""Parse color from '#RRGGBB', 'RRGGBB', '0xRRGGBB', or decimal."""
|
||||
s = (value or "").strip().lower()
|
||||
if not s:
|
||||
raise ValueError("color is required")
|
||||
|
||||
if s.startswith("#"):
|
||||
s = s[1:]
|
||||
|
||||
base = 16
|
||||
if s.startswith("0x"):
|
||||
s = s[2:]
|
||||
elif all(c.isdigit() for c in s):
|
||||
base = 10
|
||||
|
||||
color = int(s, base)
|
||||
if color < 0 or color > 0xFFFFFF:
|
||||
raise ValueError("color must be between 0 and 0xFFFFFF")
|
||||
return color
|
||||
Reference in New Issue
Block a user