diff options
| author | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
|---|---|---|
| committer | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
| commit | 5a8d568931d9b23ce0df1265d05259a7012081c9 (patch) | |
| tree | 4ea19b8bb1763a38246f342f172c285f6579e09b /alfred/coffee.py | |
init: mostly vibed
Diffstat (limited to 'alfred/coffee.py')
| -rw-r--r-- | alfred/coffee.py | 212 |
1 files changed, 212 insertions, 0 deletions
diff --git a/alfred/coffee.py b/alfred/coffee.py new file mode 100644 index 0000000..cb4f138 --- /dev/null +++ b/alfred/coffee.py @@ -0,0 +1,212 @@ +""" +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: <op> [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}") |
