0.3.9.6.a1

Added `/bot_restart {reason}` command for moderators to restart the bot from Discord
This commit is contained in:
Franz Rolfsvaag 2025-08-11 02:07:34 +02:00
parent b780c4069e
commit a25dca76e7
2 changed files with 158 additions and 1 deletions

2
bot.py
View File

@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
# Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.5.a4"
VERSION = "0.3.9.6.a1"
# ---------- Env loading ----------

View File

@ -0,0 +1,157 @@
# modules/common/bot_restart.py
import os
import re
import time
import asyncio
from datetime import datetime, timezone
import discord
from discord.ext import commands
from discord import app_commands
from modules.common.settings import cfg
from mod_perms import require_mod_interaction
WEAK_REASONS = {
"stuck", "idk", "dont know", "don't know", "unknown", "?", "lag",
"restart", "restarting", "update", "updating", "bug", "crash", "crashed"
}
def _now_utc_str() -> str:
return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
def _reason_ok(s: str) -> tuple[bool, str | None]:
"""
Enforce a descriptive reason:
- >= 20 characters
- >= 4 words
- at least 3 words with length >= 3
- reject trivial/weak phrases
"""
if not s:
return False, "Reason is required."
s = s.strip()
if len(s) < 20:
return False, "Please provide a more descriptive reason (≥ 20 characters)."
words = re.findall(r"[A-Za-z0-9'-]+", s)
if len(words) < 4:
return False, "Please include at least 4 words."
longish = [w for w in words if len(w) >= 3]
if len(longish) < 3:
return False, "Add more detail—use at least three meaningful words (≥3 letters)."
low = s.lower()
if low in WEAK_REASONS or any(low == w for w in WEAK_REASONS):
return False, "Reason is too vague. Please explain what happened."
return True, None
class BotRestartCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self._restarting = False
r = cfg(bot)
self.modlog_channel_id = r.int('modlog_channel_id', 0)
async def _send_modlog(self, guild: discord.Guild, content: str):
if not self.modlog_channel_id:
print("[bot_restart] modlog_channel_id not configured; skipping modlog message.")
return
ch = guild.get_channel(self.modlog_channel_id)
if not ch:
# fallback: global fetch
ch = self.bot.get_channel(self.modlog_channel_id)
if ch:
try:
await ch.send(content, allowed_mentions=discord.AllowedMentions.none())
except Exception as e:
print("[bot_restart] failed to send modlog:", repr(e))
else:
print(f"[bot_restart] channel id {self.modlog_channel_id} not found.")
def _current_version(self) -> str | None:
"""Best-effort: read last detected version from boot_state."""
dm = getattr(self.bot, "data_manager", None)
if not dm:
return None
try:
st = (dm.get('boot_state') or [{}])[-1] if dm.get('boot_state') else {}
v = st.get('last_version')
return v if v else None
except Exception:
return None
async def _do_restart(self, delay: float = 2.0):
if self._restarting:
return
self._restarting = True
try:
await asyncio.sleep(delay)
# Graceful close first
try:
await self.bot.close()
except Exception as e:
print("[bot_restart] bot.close() raised:", repr(e))
# Hard exit so container/wrapper restarts us
os._exit(0) # noqa: E999
except Exception as e:
print("[bot_restart] restart sequence failed:", repr(e))
# As a last resort, still try to exit
try:
os._exit(1)
except Exception:
pass
@app_commands.command(name="bot_restart", description="(Mod-only) Restart the bot with a descriptive reason.")
@app_commands.describe(reason="Explain why a restart is necessary (be specific).")
async def bot_restart(self, interaction: discord.Interaction, reason: str):
# Mods only
if not await require_mod_interaction(interaction):
return
ok, err = _reason_ok(reason)
if not ok:
return await interaction.response.send_message(f"{err}", ephemeral=True)
# Ack immediately to avoid 'interaction failed'
await interaction.response.send_message("🔁 Restart requested — posting to modlog and restarting shortly…", ephemeral=True)
guild = interaction.guild
stamp = _now_utc_str()
ver = self._current_version() or "unknown"
log = (
"🔁 **Bot Restart Requested**\n"
f"**By:** {interaction.user.mention}\n"
f"**When:** {stamp}\n"
f"**Running version:** `{ver}`\n"
f"**Reason:** {reason.strip()}"
)
if guild:
await self._send_modlog(guild, log)
else:
print("[bot_restart] no guild found on interaction; modlog not sent.")
# small delay to let the modlog flush across the gateway
await self._do_restart(delay=2.0)
async def setup(bot: commands.Bot):
cog = BotRestartCog(bot)
await bot.add_cog(cog)
# Idempotent (re)registration following your existing pattern
home_gid = cfg(bot).int('home_guild_id', 0)
guild_obj = discord.Object(id=home_gid) if home_gid else None
def _rm(name: str):
try:
bot.tree.remove_command(name, guild=guild_obj)
except Exception:
try:
bot.tree.remove_command(name, guild=None)
except Exception:
pass
_rm("bot_restart")
if home_gid:
bot.tree.add_command(cog.bot_restart, guild=guild_obj)
else:
bot.tree.add_command(cog.bot_restart)