aboutsummaryrefslogtreecommitdiffstats
path: root/jeeves/jv_core.py
diff options
context:
space:
mode:
Diffstat (limited to 'jeeves/jv_core.py')
-rw-r--r--jeeves/jv_core.py147
1 files changed, 147 insertions, 0 deletions
diff --git a/jeeves/jv_core.py b/jeeves/jv_core.py
new file mode 100644
index 0000000..b8bba33
--- /dev/null
+++ b/jeeves/jv_core.py
@@ -0,0 +1,147 @@
+"""
+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)