""" Sopel plugin: coffee logger Commands (owner only): !coffee - show usage !coffee stats - today / week / streak !coffee n - set today to n cups !coffee +n - add n cups to today (warns if > 5) !coffee -n - remove n cups from today (warns if hits 0) !coffee n yyyy-mm-dd - set a past date to n cups !coffee +n yyyy-mm-dd - add n cups to a past date !coffee -n yyyy-mm-dd - remove n cups from a past date !coffee yes/no - confirm or cancel a pending action Data: {"entries": {"YYYY-MM-DD": N, ...}} at coffee.data_path """ import json import os from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo from sopel import plugin from sopel.config.types import FilenameAttribute, StaticSection, ValidatedAttribute import os as _os, sys as _sys _alfred_dir = _os.path.dirname(_os.path.abspath(__file__)) if _alfred_dir not in _sys.path: _sys.path.insert(0, _alfred_dir) import helpstrings as h WARN_HIGH = 5 BAR_CHARS = " ▁▂▃▄▅▆▇█" _pending = {} class CoffeeSection(StaticSection): data_path = FilenameAttribute("data_path", relative=False) timezone = ValidatedAttribute("timezone", default="UTC") def setup(bot): bot.settings.define_section("coffee", CoffeeSection) def _load(bot): path = bot.settings.coffee.data_path if os.path.exists(path): with open(path) as f: return json.load(f).get("entries", {}) return {} def _save(bot, entries): path = bot.settings.coffee.data_path os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as f: json.dump({"entries": entries}, f, indent=2, sort_keys=True) f.write("\n") def _today(bot): tz = ZoneInfo(bot.settings.coffee.timezone) return datetime.now(tz).date() def _is_owner(bot, trigger): return trigger.nick == bot.settings.core.owner def _bar(values): max_v = max(values) if any(values) else 1 chars = [] for v in values: if v == 0: chars.append(" ") else: idx = max(1, round(v / max_v * (len(BAR_CHARS) - 1))) chars.append(BAR_CHARS[idx]) return "".join(chars) def _streak(entries, today): count = 0 d = today while True: if entries.get(d.isoformat(), 0) > 0: count += 1 d -= timedelta(days=1) else: break return count def _show_stats(bot, entries, today): today_key = today.isoformat() today_count = entries.get(today_key, 0) week_days = [(today - timedelta(days=6 - i)) for i in range(7)] week_values = [entries.get(d.isoformat(), 0) for d in week_days] week_total = sum(week_values) bar = _bar(week_values) streak = _streak(entries, today) all_total = sum(entries.values()) days_logged = sum(1 for v in entries.values() if v > 0) avg = all_total / days_logged if days_logged else 0 bot.say(f"coffee: today {today_count} cups") bot.say(f"coffee: week {bar} total {week_total} cups") bot.say(f"coffee: streak {streak} days") bot.say(f"coffee: total {all_total} cups") bot.say(f"coffee: average {avg:.1f} cups/day") def _apply(bot, nick, key, new_val): entries = _load(bot) entries[key] = new_val _save(bot, entries) cups = "cup" if new_val == 1 else "cups" bot.say(f"coffee: {key}: {new_val} {cups}") del _pending[nick] @plugin.command("coffee") def cmd_coffee(bot, trigger): if not _is_owner(bot, trigger): return arg = (trigger.group(2) or "").strip() nick = str(trigger.nick) # Confirmation flow if arg.lower() in ("yes", "y"): if nick not in _pending: bot.say("coffee: nothing pending") return _apply(bot, nick, _pending[nick]["key"], _pending[nick]["value"]) return if arg.lower() in ("no", "n"): _pending.pop(nick, None) bot.say("coffee: cancelled") return if not arg: bot.say(h.COFFEE_TOPIC) return # Stats subcommand if arg == "stats": entries = _load(bot) _show_stats(bot, entries, _today(bot)) return # Parse: [yyyy-mm-dd] parts = arg.split() op_str = parts[0] target = _today(bot) if len(parts) == 2: try: target = date.fromisoformat(parts[1]) except ValueError: bot.say("coffee: invalid date - use yyyy-mm-dd") return elif len(parts) > 2: bot.say("coffee: too many arguments") return target_key = target.isoformat() entries = _load(bot) current = entries.get(target_key, 0) if op_str.startswith("+"): try: delta = int(op_str[1:]) except ValueError: bot.say("coffee: usage: !coffee [+/-]n [yyyy-mm-dd]") return new_val = current + delta if new_val > WARN_HIGH: _pending[nick] = {"key": target_key, "value": new_val} bot.say(f"coffee: that brings {target_key} to {new_val} cups (>{WARN_HIGH}). confirm? (!coffee yes/no)") return elif op_str.startswith("-"): try: delta = int(op_str[1:]) except ValueError: bot.say("coffee: usage: !coffee [+/-]n [yyyy-mm-dd]") return new_val = max(0, current - delta) if new_val == 0: _pending[nick] = {"key": target_key, "value": 0} bot.say(f"coffee: that brings {target_key} to 0 cups. confirm? (!coffee yes/no)") return else: try: new_val = int(op_str) except ValueError: bot.say("coffee: usage: !coffee [+/-]n [yyyy-mm-dd]") return if new_val < 0: bot.say("coffee: count can't be negative") return entries[target_key] = new_val _save(bot, entries) cups = "cup" if new_val == 1 else "cups" bot.say(f"coffee: {target_key} set to {new_val} {cups}")