115 lines
3.1 KiB
Python
115 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
|
|
|
|
DEFAULT_TEMPLATES: dict[str, dict] = {
|
|
"420": {
|
|
"text": "Blaze it!",
|
|
"color": 0x2ECC71,
|
|
"image_url": "https://copyparty.allucanget.biz/img/weed.png",
|
|
},
|
|
"reminder": {
|
|
"text": "This is your 5 minute reminder to 420!",
|
|
"color": 0xE67E22,
|
|
},
|
|
"halftime": {
|
|
"text": "Half-time!",
|
|
"color": 0x2ECC71,
|
|
"image_url": "https://copyparty.allucanget.biz/img/weed.png",
|
|
},
|
|
"reminder_halftime": {
|
|
"text": "Half-time in 5 minutes!",
|
|
"color": 0xE67E22,
|
|
},
|
|
"test": {
|
|
"text": "This is a test notification.",
|
|
"color": 0x3498DB,
|
|
},
|
|
}
|
|
|
|
|
|
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
|
|
elif isinstance(color, str):
|
|
try:
|
|
out[key]["color"] = parse_color(color)
|
|
except ValueError:
|
|
pass
|
|
|
|
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)
|
|
serialized = deepcopy(normalized)
|
|
|
|
for tpl in serialized.values():
|
|
color = tpl.get("color")
|
|
if isinstance(color, int):
|
|
tpl["color"] = f"#{color:06X}"
|
|
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = p.with_suffix(p.suffix + ".tmp")
|
|
tmp.write_text(json.dumps(serialized, 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
|