""" Jeeves core helpers. Not loaded directly by Sopel (not in enable list). All jeeves plugins import this via sys.path insertion. Provides: ensure_setup, save, cfg, cfg_val, chan_db, CONFIG_KEYS. """ import json import os import threading from sopel.config.types import FilenameAttribute, StaticSection # (type, default, global_only, description) CONFIG_KEYS = { "dj_track_duration": (int, 120, True, "seconds per track before auto-advance"), "dj_queue_cap": (int, 50, True, "max tracks in queue"), "art_height": (int, 6, True, "default lines for !draw (4-12)"), "duck_spawn_min": (int, 60, False, "min seconds between duck spawns"), "duck_spawn_max": (int, 300, False, "max seconds between duck spawns"), "duck_flee_time": (int, 45, False, "seconds before duck flees"), "duck_idle_rounds": (int, 3, False, "empty rounds before auto-stop (0=off)"), "news_interval": (int, 90, False, "minutes between news broadcasts"), "news_count": (int, 1, False, "headlines per broadcast cycle"), "quotes_interval": (int, 60, False, "minutes between quote auto-posts"), } _SEEN_MAX = 1000 class JeevesSection(StaticSection): db_path = FilenameAttribute("db_path", relative=False, default="/var/jeeves/db.json") duck_db_path = FilenameAttribute("duck_db_path", relative=False, default="/var/jeeves/scores.db") recipes_path = FilenameAttribute("recipes_path", relative=False, default="/var/jeeves/recipes.json") feeds_path = FilenameAttribute("feeds_path", relative=False, default="/var/jeeves/feeds.json") def _load_feeds(path): try: with open(path) as f: return json.load(f) except Exception: return [] def _load(path, feeds_path): feeds = _load_feeds(feeds_path) if os.path.exists(path): try: with open(path) as f: data = json.load(f) data.setdefault("config", {}) data.setdefault("channels", {}) data.setdefault("quotes", []) data.setdefault("news_feeds", feeds) data.setdefault("news_seen", []) return data except Exception: pass data = { "config": {}, "channels": {}, "quotes": [], "news_feeds": feeds, "news_seen": [], } _save_raw(path, data) return data def _save_raw(path, data): os.makedirs(os.path.dirname(path), exist_ok=True) seen = data.get("news_seen", []) if len(seen) > _SEEN_MAX: data["news_seen"] = seen[-_SEEN_MAX:] with open(path, "w") as f: json.dump(data, f, indent=2) def save(bot): _save_raw(bot.memory["jv_db_path"], bot.memory["jv_db"]) def ensure_setup(bot): if "jv_db" in bot.memory: return try: bot.config.define_section("jeeves", JeevesSection, validate=False) db_path = bot.config.jeeves.db_path duck_db_path = bot.config.jeeves.duck_db_path recipes_path = bot.config.jeeves.recipes_path feeds_path = bot.config.jeeves.feeds_path except Exception: db_path = "/var/jeeves/db.json" duck_db_path = "/var/jeeves/scores.db" recipes_path = "/var/jeeves/recipes.json" feeds_path = "/var/jeeves/feeds.json" bot.memory["jv_db_path"] = db_path bot.memory["jv_duck_db_path"] = duck_db_path bot.memory["jv_recipes_path"] = recipes_path bot.memory["jv_feeds_path"] = feeds_path bot.memory["jv_db"] = _load(db_path, feeds_path) bot.memory["jv_lock"] = threading.Lock() def cfg(bot, channel=None): db = bot.memory.get("jv_db", {}) global_cfg = db.get("config", {}) if channel: chan_cfg = (db.get("channels", {}) .get(channel.lower(), {}) .get("config", {})) return {**global_cfg, **chan_cfg} return dict(global_cfg) def cfg_val(bot, key, channel=None): merged = cfg(bot, channel) default = CONFIG_KEYS[key][1] return type(CONFIG_KEYS[key][0])(merged.get(key, default)) def chan_db(bot, channel): db = bot.memory["jv_db"] db.setdefault("channels", {}) ch = db["channels"].setdefault(channel.lower(), {}) ch.setdefault("config", {}) ch.setdefault("news", {"enabled": True, "last_broadcast": 0}) ch.setdefault("quotes", {"enabled": False, "last_broadcast": 0}) return ch def is_owner(bot, trigger): return trigger.nick == bot.settings.core.owner def is_op(bot, trigger): chan = bot.channels.get(str(trigger.sender)) if not chan: return False from sopel import plugin as _p return chan.privileges.get(trigger.nick, 0) >= _p.OP def is_authorized(bot, trigger): return is_owner(bot, trigger) or is_op(bot, trigger)