diff --git a/cogs/uptime.py b/cogs/uptime.py deleted file mode 100644 index 71945a8..0000000 --- a/cogs/uptime.py +++ /dev/null @@ -1,150 +0,0 @@ -import discord -from discord.ext import commands -from datetime import datetime, timezone -import json -import os - -CONFIG_FILE = "config.json" -STATE_FILE = "uptime_state.json" - -REQUIRED_KEYS = ["labels", "embed", "settings"] -REQUIRED_LABEL_KEYS = ["days", "hours", "minutes", "seconds"] -REQUIRED_EMBED_KEYS = ["session_field", "availability_field"] - -def pluralize(n: int, forms: tuple[str, str, str]) -> str: - if 11 <= n % 100 <= 14: - return f"{n} {forms[2]}" - r = n % 10 - if r == 1: return f"{n} {forms[0]}" - if 2 <= r <= 4: return f"{n} {forms[1]}" - return f"{n} {forms[2]}" - -def format_delta(total_seconds: int, labels: dict, max_units: int) -> str: - minutes, secs = divmod(total_seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - - candidates = [ - (days, labels["days"]), - (hours, labels["hours"]), - (minutes, labels["minutes"]), - (secs, labels["seconds"]), - ] - - parts = [pluralize(v, tuple(f)) for v, f in candidates if v][:max_units] - return " ".join(parts) if parts else pluralize(0, tuple(labels["seconds"])) - -def _now_ts() -> float: - return datetime.now(timezone.utc).timestamp() - -class UptimeSimple(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self.session_start = _now_ts() - self.config = self._load_and_validate_config() - self.state = self._load_state() - - if self.state["last_start"] is not None: - gap = self.session_start - self.state["last_start"] - if gap > 5: - self.state["total_downtime"] += gap - - if self.state["first_seen"] is None: - self.state["first_seen"] = self.session_start - - self.state["last_start"] = self.session_start - self._save_state() - - def _parse_color(self, color_val) -> int: - if isinstance(color_val, str): - return int(color_val.lstrip("#").replace("0x", ""), 16) - return int(color_val) - - def _load_and_validate_config(self) -> dict: - path = os.path.join(os.path.dirname(__file__), CONFIG_FILE) - - if not os.path.exists(path): - raise FileNotFoundError(f"Uptime cog config not found at {path}") - - with open(path, "r", encoding="utf-8") as f: - config = json.load(f) - - missing = [k for k in REQUIRED_KEYS if k not in config] - if missing: - raise KeyError(f"Uptime config is missing required keys: {missing}") - - missing_labels = [k for k in REQUIRED_LABEL_KEYS if k not in config["labels"]] - if missing_labels: - raise KeyError(f"Uptime config 'labels' is missing keys: {missing_labels}") - - missing_embed = [k for k in REQUIRED_EMBED_KEYS if k not in config["embed"]] - if missing_embed: - raise KeyError(f"Uptime config 'embed' is missing keys: {missing_embed}") - - settings = config.get("settings", {}) - settings.setdefault("max_units", 4) - settings.setdefault("show_percent", True) - - color_val = config["embed"].get("color", 0x2ecc71) - config["embed"]["parsed_color"] = discord.Color(self._parse_color(color_val)) - - return config - - def _state_path(self) -> str: - return os.path.join(os.path.dirname(__file__), STATE_FILE) - - def _load_state(self) -> dict: - path = self._state_path() - if os.path.exists(path): - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - return { - "first_seen": None, - "total_downtime": 0.0, - "last_start": None, - } - - def _save_state(self) -> None: - with open(self._state_path(), "w", encoding="utf-8") as f: - json.dump(self.state, f, indent=2) - - def availability_percent(self) -> float: - now = _now_ts() - total_span = now - self.state["first_seen"] - if total_span <= 0: - return 100.0 - return max(0.0, (total_span - self.state["total_downtime"]) / total_span * 100) - - @staticmethod - def _pct_bar(pct: float, width: int = 10) -> str: - filled = round(pct / 100 * width) - return "█" * filled + "░" * (width - filled) - - @commands.command(name="uptime") - async def uptime(self, ctx: commands.Context): - session_seconds = int(_now_ts() - self.session_start) - settings = self.config["settings"] - - embed = discord.Embed(color=self.config["embed"]["parsed_color"]) - embed.add_field( - name = self.config["embed"]["session_field"], - value = format_delta(session_seconds, self.config["labels"], settings["max_units"]), - inline = False, - ) - - if settings["show_percent"]: - pct = self.availability_percent() - embed.add_field( - name = self.config["embed"]["availability_field"], - value = f"{self._pct_bar(pct)} {pct:.2f}%", - inline = False, - ) - - await ctx.send(embed=embed) - - async def cog_unload(self) -> None: - self.state["last_start"] = None - self._save_state() - -async def setup(bot: commands.Bot): - await bot.add_cog(UptimeSimple(bot)) \ No newline at end of file