This commit is contained in:
Franz Rolfsvaag 2025-08-10 20:53:09 +02:00
parent 27cc972f19
commit 377586e6e7

82
bot.py
View File

@ -2,35 +2,28 @@ import os, signal, asyncio, pathlib
import discord import discord
from discord.ext import commands from discord.ext import commands
from dotenv import load_dotenv from dotenv import load_dotenv
from configparser import ConfigParser
from data_manager import DataManager from data_manager import DataManager
from modules.common.settings import cfg as cfg_helper
from modules.common.boot_notice import post_boot_notice from modules.common.boot_notice import post_boot_notice
# Version consists of: # Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update) # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.2.a5" VERSION = "0.3.9.2.a6"
# ---------- Env & config loading ---------- # ---------- Env loading ----------
load_dotenv() load_dotenv()
def _get_env(name: str, default: str = "") -> str: def _get_env(name: str, default: str = "") -> str:
v = os.getenv(name, "") v = os.getenv(name, "")
# normalize stray quotes/whitespace Portainer sometimes injects return (v or "").strip().strip('"').strip("'") or default
v = (v or "").strip().strip('"').strip("'")
return v if v else default
TOKEN = _get_env("DISCORD_TOKEN") TOKEN = _get_env("DISCORD_TOKEN")
DATA_FILE = _get_env("SHAI_DATA") or _get_env("SHAI_DATA_FILE") or "/data/data.json" DATA_FILE = _get_env("SHAI_DATA") or _get_env("SHAI_DATA_FILE") or "/data/data.json"
CHECK_TIME_UTC = _get_env("CHECK_TIME_UTC", "03:00")
IGNORE_TEST_LEVEL = int(_get_env("IGNORE_TEST_LEVEL", "1"))
SHAI_HOME_GUILD_ID = _get_env("SHAI_HOME_GUILD_ID")
print("[Config] DISCORD_TOKEN set:", bool(TOKEN)) print("[Config] DISCORD_TOKEN set:", bool(TOKEN))
print("[Config] DATA_FILE:", DATA_FILE) print("[Config] DATA_FILE:", DATA_FILE)
print("[Config] CHECK_TIME_UTC:", CHECK_TIME_UTC)
print("[Config] IGNORE_TEST_LEVEL:", IGNORE_TEST_LEVEL)
print("[Config] SHAI_HOME_GUILD_ID:", SHAI_HOME_GUILD_ID or "(unset)")
# ---------- Discord intents ---------- # ---------- Discord intents ----------
@ -43,33 +36,27 @@ intents.emojis_and_stickers = True
intents.voice_states = True intents.voice_states = True
# ---------- Bot + DataManager ---------- # ---------- Bot + DataManager ----------
if not TOKEN: if not TOKEN:
print("[Config] WARNING: DISCORD_TOKEN is empty. The bot will fail to log in.") print("[Config] WARNING: DISCORD_TOKEN is empty. The bot will fail to log in.")
bot = commands.Bot(command_prefix='!', intents=intents) bot = commands.Bot(command_prefix="!", intents=intents)
# If you still want to pass “config” around, put a tiny dict on bot: # Ensure data path exists and is seeded
bot.config = { os.makedirs(os.path.dirname(DATA_FILE) or ".", exist_ok=True)
"check_time_utc": CHECK_TIME_UTC,
"ignore_test_level": IGNORE_TEST_LEVEL,
"home_guild_id": SHAI_HOME_GUILD_ID,
}
# Ensure the data path exists and file is seeded if missing
os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True)
if not os.path.exists(DATA_FILE): if not os.path.exists(DATA_FILE):
with open(DATA_FILE, "w", encoding="utf-8") as f: with open(DATA_FILE, "w", encoding="utf-8") as f:
f.write("{}") f.write("{}")
bot.data_manager = DataManager(DATA_FILE) bot.data_manager = DataManager(DATA_FILE)
# ---------- Self-check helpers ---------- # ---------- Self-check (env-aware via cfg_helper) ----------
async def _guild_selfcheck(g: discord.Guild, cfg): async def _guild_selfcheck(g: discord.Guild, c):
problems = [] problems = []
def _need_channel(id_key, *perms): def _need_channel(id_key, *perms):
raw = cfg.get(id_key) raw = c.get(id_key)
if not raw: if not raw:
problems.append(f"Missing config key: {id_key}") problems.append(f"Missing config key: {id_key}")
return return
@ -88,9 +75,9 @@ async def _guild_selfcheck(g: discord.Guild, cfg):
if not getattr(p, perm, False): if not getattr(p, perm, False):
problems.append(f"Missing permission on #{ch.name}: {perm}") problems.append(f"Missing permission on #{ch.name}: {perm}")
_need_channel('mod_channel_id', 'read_messages', 'send_messages', 'add_reactions', 'read_message_history') _need_channel("mod_channel_id", "read_messages", "send_messages", "add_reactions", "read_message_history")
_need_channel('modlog_channel_id', 'read_messages', 'send_messages') _need_channel("modlog_channel_id", "read_messages", "send_messages")
_need_channel('pirates_list_channel_id', 'read_messages', 'send_messages') _need_channel("pirates_list_channel_id", "read_messages", "send_messages")
if problems: if problems:
print(f"[SelfCheck:{g.name}]") print(f"[SelfCheck:{g.name}]")
@ -106,11 +93,14 @@ async def on_ready():
"/ message_content:", bot.intents.message_content, "/ message_content:", bot.intents.message_content,
"/ voice_states:", bot.intents.voice_states) "/ voice_states:", bot.intents.voice_states)
await asyncio.gather(*[_guild_selfcheck(g, bot.config) for g in bot.guilds]) env_cfg = cfg_helper(bot)
# Slash sync — now reading from dict # Per-guild permission sanity checks (env-aware)
await asyncio.gather(*[_guild_selfcheck(g, env_cfg) for g in bot.guilds])
# Slash command sync (env-aware dev guild)
try: try:
dev_gid = bot.config.get('dev_guild_id') dev_gid = env_cfg.get("dev_guild_id")
if dev_gid: if dev_gid:
guild = bot.get_guild(int(dev_gid)) guild = bot.get_guild(int(dev_gid))
if guild: if guild:
@ -125,6 +115,7 @@ async def on_ready():
except Exception as e: except Exception as e:
print("[Slash] Sync failed:", repr(e)) print("[Slash] Sync failed:", repr(e))
# Post boot status message (wrapper/env-driven or RSS fallback)
try: try:
await post_boot_notice(bot) await post_boot_notice(bot)
except Exception as e: except Exception as e:
@ -132,29 +123,30 @@ async def on_ready():
# ---------- Auto-discover extensions ---------- # ---------- Auto-discover extensions ----------
modules_path = pathlib.Path(__file__).parent / 'modules' modules_path = pathlib.Path(__file__).parent / "modules"
extensions = [] extensions = []
for folder in modules_path.iterdir(): for folder in modules_path.iterdir():
if folder.is_dir(): if not folder.is_dir():
# skip non-cog helpers under modules/common continue
if folder.name == 'common': # skip non-cog helpers under modules/common
if folder.name == "common":
continue
for file in folder.glob("*.py"):
if file.name == "__init__.py":
continue continue
for file in folder.glob('*.py'): extensions.append(f"modules.{folder.name}.{file.stem}")
if file.name == '__init__.py':
continue
extensions.append(f"modules.{folder.name}.{file.stem}")
def _install_signal_handlers(loop, bot): def _install_signal_handlers(loop, bot_obj):
def _graceful(*_): def _graceful(*_):
loop.create_task(bot.close()) loop.create_task(bot_obj.close())
for s in (signal.SIGTERM, signal.SIGINT): for s in (signal.SIGTERM, signal.SIGINT):
try: try:
loop.add_signal_handler(s, _graceful) loop.add_signal_handler(s, _graceful)
except NotImplementedError: except NotImplementedError:
pass pass # Windows
async def main(): async def main():
print(f"[STARTUP] ShaiWatcher booting to v.{VERSION}") print(f"[STARTUP] ShaiWatcher booting v{VERSION}")
async with bot: async with bot:
for ext in extensions: for ext in extensions:
try: try:
@ -166,5 +158,5 @@ async def main():
_install_signal_handlers(loop, bot) _install_signal_handlers(loop, bot)
await bot.start(TOKEN) await bot.start(TOKEN)
if __name__ == '__main__': if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())