feat: add multi-chat routing and bill-specific dashboard countdown
Test blackout notifier / test (push) Failing after 4s
Hourly blackout check / check (push) Successful in 3s

This commit is contained in:
Meghdad
2026-07-22 02:14:45 +03:30
parent 6c3e146234
commit 42687ee651
12 changed files with 372 additions and 100 deletions
+55 -14
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
import os
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType
from dotenv import load_dotenv
@@ -15,13 +17,16 @@ class ConfigurationError(ValueError):
class Config:
bargheman_token: str
eitaayar_token: str
chat_id: str
bill_ids: tuple[str, ...]
bill_chat_ids: Mapping[str, tuple[str, ...]]
state_file: Path
lookahead_days: int = 5
request_timeout_seconds: float = 15.0
max_events_per_bill: int = 25
@property
def bill_ids(self) -> tuple[str, ...]:
return tuple(self.bill_chat_ids)
@classmethod
def from_env(cls) -> Config:
load_dotenv(override=False)
@@ -29,20 +34,27 @@ class Config:
values = {
"BARGHEMAN_TOKEN": os.getenv("BARGHEMAN_TOKEN", "").strip(),
"EITAAYAR_TOKEN": os.getenv("EITAAYAR_TOKEN", "").strip(),
"CHAT_ID": os.getenv("CHAT_ID", "").strip(),
}
missing = [name for name, value in values.items() if not value]
raw_bill_ids = os.getenv("BILL_IDS", "")
bill_ids = tuple(
dict.fromkeys(part.strip() for part in raw_bill_ids.split(",") if part.strip())
)
if not bill_ids:
missing.append("BILL_IDS")
elif invalid := [bill_id for bill_id in bill_ids if not bill_id.isdecimal()]:
raise ConfigurationError(
f"BILL_IDS must contain only decimal IDs: {', '.join(invalid)}"
raw_routes = os.getenv("BILL_CHAT_MAP", "").strip()
if raw_routes:
bill_chat_ids = _bill_chat_map(raw_routes)
else:
chat_id = os.getenv("CHAT_ID", "").strip()
raw_bill_ids = os.getenv("BILL_IDS", "")
bill_ids = tuple(
dict.fromkeys(part.strip() for part in raw_bill_ids.split(",") if part.strip())
)
if not chat_id:
missing.append("CHAT_ID")
if not bill_ids:
missing.append("BILL_IDS")
elif invalid := [bill_id for bill_id in bill_ids if not bill_id.isdecimal()]:
raise ConfigurationError(
f"BILL_IDS must contain only decimal IDs: {', '.join(invalid)}"
)
bill_chat_ids = {bill_id: (chat_id,) for bill_id in bill_ids}
if missing:
raise ConfigurationError(
@@ -52,8 +64,7 @@ class Config:
return cls(
bargheman_token=values["BARGHEMAN_TOKEN"],
eitaayar_token=values["EITAAYAR_TOKEN"],
chat_id=values["CHAT_ID"],
bill_ids=bill_ids,
bill_chat_ids=MappingProxyType(bill_chat_ids),
state_file=Path(os.getenv("STATE_FILE", "state/outages.json").strip()),
lookahead_days=_integer_setting("LOOKAHEAD_DAYS", 5, minimum=1, maximum=30),
request_timeout_seconds=_float_setting(
@@ -63,6 +74,36 @@ class Config:
)
def _bill_chat_map(raw_routes: str) -> dict[str, tuple[str, ...]]:
routes: dict[str, list[str]] = {}
malformed: list[str] = []
for raw_route in raw_routes.split(","):
route = raw_route.strip()
if not route:
continue
bill_id, separator, chat_id = route.partition(":")
bill_id = bill_id.strip()
chat_id = chat_id.strip()
if not separator or not bill_id or not chat_id:
malformed.append(route)
continue
if not bill_id.isdecimal():
raise ConfigurationError(
f"BILL_CHAT_MAP bill IDs must be decimal: {bill_id}"
)
chat_ids = routes.setdefault(bill_id, [])
if chat_id not in chat_ids:
chat_ids.append(chat_id)
if malformed:
raise ConfigurationError(
"BILL_CHAT_MAP entries must use BILL_ID:CHAT_ID format: "
+ ", ".join(malformed)
)
if not routes:
raise ConfigurationError("BILL_CHAT_MAP must contain at least one route")
return {bill_id: tuple(chat_ids) for bill_id, chat_ids in routes.items()}
def _integer_setting(name: str, default: int, *, minimum: int, maximum: int) -> int:
raw_value = os.getenv(name, str(default)).strip()
try: