""" Sopel plugin: twtxt feed Commands (owner only): !twt - show usage !twt last [n] - show last n twts, most recent first (default 5) !twt count - total number of twts !twt delete - delete the most recent twt (asks yes/no) !twt yes / !twt no - confirm or cancel a pending delete !twt - post a twt Config ([twt] section in sopel config): twtxt_path - path to twtxt.txt (default /var/www/twt.gumx.cc/twtxt.txt) timezone - timezone name for timestamps (default UTC) nick - nick value written in file header (default gumx) url - url value written in file header """ import os from datetime import datetime 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 _pending_delete = {} _SUBCOMMANDS = {"last", "count", "delete", "yes", "no"} class TwtSection(StaticSection): twtxt_path = FilenameAttribute("twtxt_path", relative=False, default="/var/www/twt.gumx.cc/twtxt.txt") timezone = ValidatedAttribute("timezone", default="UTC") nick = ValidatedAttribute("nick", default="gumx") url = ValidatedAttribute("url", default="https://twt.gumx.cc/twtxt.txt") def setup(bot): bot.settings.define_section("twt", TwtSection) def _is_owner(bot, trigger): return trigger.nick == bot.settings.core.owner def _path(bot): return bot.settings.twt.twtxt_path def _read(bot): p = _path(bot) if not os.path.exists(p): return [] with open(p) as f: lines = f.readlines() twts = [] for line in lines: line = line.rstrip("\n") if not line or line.startswith("#"): continue tab = line.find("\t") if tab == -1: continue twts.append((line[:tab], line[tab + 1:])) return twts def _write(bot, twts): p = _path(bot) nick = bot.settings.twt.nick url = bot.settings.twt.url with open(p, "w") as f: f.write(f"# nick = {nick}\n") f.write(f"# url = {url}\n") for ts, text in twts: f.write(f"{ts}\t{text}\n") def _append(bot, text): tz = ZoneInfo(bot.settings.twt.timezone) now = datetime.now(tz) offset = now.strftime("%z") offset = offset[:3] + ":" + offset[3:] ts = now.strftime("%Y-%m-%dT%H:%M:%S") + offset p = _path(bot) nick = bot.settings.twt.nick url = bot.settings.twt.url if not os.path.exists(p): os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w") as f: f.write(f"# nick = {nick}\n") f.write(f"# url = {url}\n") with open(p, "a") as f: f.write(f"{ts}\t{text}\n") return ts @plugin.commands("twt") def cmd_twt(bot, trigger): if not _is_owner(bot, trigger): return raw = (trigger.group(2) or "").strip() args = raw.split() sub = args[0].lower() if args else "" if not sub: bot.say(h.TWF_TOPIC) return # --- last --- if sub == "last": try: n = int(args[1]) if len(args) > 1 else 5 n = max(1, min(n, 10)) except ValueError: bot.say("usage: !twt last [n]") return twts = _read(bot) if not twts: bot.say("no twts yet.") return for ts, text in reversed(twts[-n:]): bot.say(f"[{ts[:10]}] {text}") return # --- count --- if sub == "count": n = len(_read(bot)) bot.say(f"{n} twt{'s' if n != 1 else ''}") return # --- delete --- if sub == "delete": twts = _read(bot) if not twts: bot.say("nothing to delete.") return ts, text = twts[-1] _pending_delete[trigger.nick] = True bot.say(f"delete [{ts[:10]}] {text!r}? !twt yes / !twt no") return # --- yes/no (confirm delete) --- if sub == "yes": if not _pending_delete.pop(trigger.nick, False): bot.say("nothing pending.") return twts = _read(bot) if not twts: bot.say("nothing to delete.") return removed = twts.pop() _write(bot, twts) bot.say(f"deleted [{removed[0][:10]}] {removed[1]!r}") return if sub == "no": if _pending_delete.pop(trigger.nick, False): bot.say("cancelled.") return # --- post (anything that's not a subcommand) --- ts = _append(bot, raw) bot.say(f"twted [{ts[:10]}]: {raw}")