# retoor <retoor@molodetz.nl>
import logging
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
from devplacepy.config import BOT_DIR
logger = logging.getLogger(__name__)
MONITOR_DIR: Path = BOT_DIR / "monitor"
FRAME_SUFFIX = ".jpg"
ACTIVE_WINDOW_SECONDS = 90
@dataclass
class BotFrame:
slot: int
username: str = ""
persona: str = ""
action: str = ""
status: str = ""
url: str = ""
captured_at: float = 0.0
has_image: bool = False
def label(self) -> str:
return self.username or f"bot{self.slot}"
def age_seconds(self) -> float:
return max(0.0, time.time() - self.captured_at) if self.captured_at else 0.0
def is_active(self) -> bool:
return self.has_image and self.age_seconds() <= ACTIVE_WINDOW_SECONDS
def as_dict(self) -> dict:
return {
"slot": self.slot,
"username": self.username,
"persona": self.persona,
"action": self.action,
"status": self.status,
"url": self.url,
"label": self.label(),
"captured_at": int(self.captured_at) if self.captured_at else 0,
"age_seconds": int(self.age_seconds()),
"has_image": self.has_image,
"active": self.is_active(),
}
@dataclass
class BotMonitor:
frames: dict[int, BotFrame] = field(default_factory=dict)
def frame_path(self, slot: int) -> Path:
return MONITOR_DIR / f"slot{slot}{FRAME_SUFFIX}"
def update_meta(
self,
slot: int,
*,
username: str = "",
persona: str = "",
action: str = "",
status: str = "",
url: str = "",
) -> None:
frame = self.frames.get(slot) or BotFrame(slot=slot)
if username:
frame.username = username
if persona:
frame.persona = persona
if action:
frame.action = action
if status:
frame.status = status
if url:
frame.url = url
self.frames[slot] = frame
def store_image(self, slot: int, image: bytes) -> None:
if not image:
return
frame = self.frames.get(slot) or BotFrame(slot=slot)
try:
MONITOR_DIR.mkdir(parents=True, exist_ok=True)
path = self.frame_path(slot)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_bytes(image)
tmp.replace(path)
except OSError as e:
logger.debug("monitor store_image slot %s failed: %s", slot, e)
return
frame.has_image = True
frame.captured_at = time.time()
self.frames[slot] = frame
def drop(self, slot: int) -> None:
self.frames.pop(slot, None)
try:
self.frame_path(slot).unlink(missing_ok=True)
except OSError as e:
logger.debug("monitor drop slot %s failed: %s", slot, e)
def read_image(self, slot: int) -> Optional[bytes]:
path = self.frame_path(slot)
if not path.exists():
return None
try:
return path.read_bytes()
except OSError as e:
logger.debug("monitor read_image slot %s failed: %s", slot, e)
return None
def snapshot(self) -> list[dict]:
return [self.frames[slot].as_dict() for slot in sorted(self.frames)]
monitor = BotMonitor()