Merge pull request #4 from Fretuks/remove-chat-revive-functionality

Remove chat-revive subsystem and update AI access role to 1430704600645898250
This commit is contained in:
2026-02-28 11:47:37 +01:00
committed by GitHub
2 changed files with 4 additions and 233 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ T.A.R.S. is a Discord bot for a private server with moderation, utility, and lig
## Features ## Features
- Moderation and safety: auto-moderation hooks, reports, lock/slowmode tools, and a configurable banned-word list. - Moderation and safety: auto-moderation hooks, reports, lock/slowmode tools, and a configurable banned-word list.
- Utility automation: reminders, message quoting, and server/user diagnostics. - Utility automation: reminders, message quoting, and server/user diagnostics.
- Community engagement: chat revive prompts, MOTD rotation, and fun commands like dice and 8-ball. - Community engagement: MOTD rotation and fun commands like dice and 8-ball.
- Boost points: reward boosters and let them redeem perks through a simple shop flow. - Boost points: reward boosters and let them redeem perks through a simple shop flow.
## Quick Start ## Quick Start
@@ -16,7 +16,7 @@ T.A.R.S. is a Discord bot for a private server with moderation, utility, and lig
- `DISCORD_TOKEN` - `DISCORD_TOKEN`
- `OWNER_ID` - `OWNER_ID`
- `GUILD_ID` - `GUILD_ID`
- `OPENAI_API_KEY` (optional, required for AI replies and revive prompts) - `OPENAI_API_KEY` (optional, required for AI replies)
3. Run the bot: 3. Run the bot:
```bash ```bash
python tars_bot.py python tars_bot.py
+2 -231
View File
@@ -23,16 +23,11 @@ DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
OWNER_ID = int(os.getenv("OWNER_ID", "0")) OWNER_ID = int(os.getenv("OWNER_ID", "0"))
GUILD_ID = int(os.getenv("GUILD_ID", "0")) if os.getenv("GUILD_ID") else None GUILD_ID = int(os.getenv("GUILD_ID", "0")) if os.getenv("GUILD_ID") else None
openai_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) openai_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
REVIVE_CHANNEL_ID = 1424038714266357886
REVIVE_ROLE_ID = 1430336620447535156
REVIVE_INTERVAL = 24 * 60 * 60
CHECK_INTERVAL = 600 CHECK_INTERVAL = 600
AI_ACCESS_ROLE_ID = 1430704668773716008 AI_ACCESS_ROLE_ID = 1430704600645898250
MESSAGE_LIMIT = 10 MESSAGE_LIMIT = 10
RATE_LIMIT_WINDOW = timedelta(hours=1) RATE_LIMIT_WINDOW = timedelta(hours=1)
user_message_log = defaultdict(list) user_message_log = defaultdict(list)
last_activity_time = None
revive_sent = False
OBSERVING_ID = 1003470446517301288 OBSERVING_ID = 1003470446517301288
QUIET_HOUR_MULTIPLIER = 2.5 QUIET_HOUR_MULTIPLIER = 2.5
@@ -45,10 +40,8 @@ BOT_VERSION = "6.0.1"
BOT_START_TIME = datetime.now(timezone.utc) BOT_START_TIME = datetime.now(timezone.utc)
LAST_ERROR_TIME: datetime | None = None LAST_ERROR_TIME: datetime | None = None
LAST_REVIVE_TIME: datetime | None = None
FEATURE_FLAGS = { FEATURE_FLAGS = {
"ai_enabled": True, "ai_enabled": True,
"revive_enabled": True,
} }
ERROR_WINDOW = timedelta(seconds=60) ERROR_WINDOW = timedelta(seconds=60)
ERROR_THRESHOLD = 5 ERROR_THRESHOLD = 5
@@ -100,7 +93,6 @@ def record_error():
if len(ERROR_LOG) >= ERROR_THRESHOLD and not COOLDOWN_UNTIL: if len(ERROR_LOG) >= ERROR_THRESHOLD and not COOLDOWN_UNTIL:
COOLDOWN_UNTIL = now + COOLDOWN_PERIOD COOLDOWN_UNTIL = now + COOLDOWN_PERIOD
FEATURE_FLAGS["ai_enabled"] = False FEATURE_FLAGS["ai_enabled"] = False
FEATURE_FLAGS["revive_enabled"] = False
logger.error("Circuit breaker triggered. Features temporarily disabled.") logger.error("Circuit breaker triggered. Features temporarily disabled.")
@@ -108,7 +100,6 @@ def check_circuit_recovery():
global COOLDOWN_UNTIL global COOLDOWN_UNTIL
if COOLDOWN_UNTIL and datetime.now(timezone.utc) >= COOLDOWN_UNTIL: if COOLDOWN_UNTIL and datetime.now(timezone.utc) >= COOLDOWN_UNTIL:
FEATURE_FLAGS["ai_enabled"] = True FEATURE_FLAGS["ai_enabled"] = True
FEATURE_FLAGS["revive_enabled"] = True
COOLDOWN_UNTIL = None COOLDOWN_UNTIL = None
ERROR_LOG.clear() ERROR_LOG.clear()
logger.info("Circuit breaker reset. Features re-enabled.") logger.info("Circuit breaker reset. Features re-enabled.")
@@ -155,10 +146,8 @@ async def tars_command_help(interaction: discord.Interaction, command_name: str)
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
"revive_interval": int,
"motd_channel_id": (int, type(None)), "motd_channel_id": (int, type(None)),
"ai_enabled": bool, "ai_enabled": bool,
"revive_enabled": bool,
} }
AI_USAGE = { AI_USAGE = {
@@ -169,42 +158,6 @@ AI_USAGE = {
} }
def get_time_of_day() -> str:
hour = datetime.now(timezone.utc).hour
if 5 <= hour < 12:
return "morning"
if 12 <= hour < 18:
return "afternoon"
if 18 <= hour < 23:
return "evening"
return "late_night"
def get_season() -> str:
month = datetime.now(timezone.utc).month
if month in (12, 1, 2):
return "winter"
if month in (3, 4, 5):
return "spring"
if month in (6, 7, 8):
return "summer"
return "autumn"
REVIVE_STYLE_PROMPTS = {
"morning": "Friendly morning icebreaker that encourages people to start chatting.",
"afternoon": "Casual, light discussion starter to revive mid-day conversation.",
"evening": "Relaxed, social question suitable for evening community chat.",
"late_night": "Low-pressure, reflective question suitable for late-night lurkers."
}
SEASONAL_PROMPTS = {
"winter": "Seasonal tone: cozy, reflective, or end-of-year energy.",
"spring": "Seasonal tone: fresh ideas, plans, and motivation.",
"summer": "Seasonal tone: relaxed, fun, or social energy.",
"autumn": "Seasonal tone: thoughtful, nostalgic, or analytical.",
}
def decay_topics(): def decay_topics():
for k in list(TOPIC_COUNTER.keys()): for k in list(TOPIC_COUNTER.keys()):
@@ -234,7 +187,7 @@ intents.guilds = True
intents.reactions = True intents.reactions = True
bot = commands.Bot(command_prefix="/", intents=intents) bot = commands.Bot(command_prefix="/", intents=intents)
tree = bot.tree tree = bot.tree
from config import DB_FILE, recent_joins, recent_message_history, BANNED_WORDS, AI_PROHIBITED_PATTERNS, CHANNEL_THEMES from config import DB_FILE, recent_joins, recent_message_history, BANNED_WORDS, AI_PROHIBITED_PATTERNS
from tars import tars_text from tars import tars_text
import random import random
@@ -398,91 +351,6 @@ async def init_db():
time time
TEXT TEXT
)""") )""")
await db.execute("""
CREATE TABLE IF NOT EXISTS revive_history
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
question
TEXT
NOT
NULL,
time
TEXT
NOT
NULL
)
""")
await db.commit()
async def get_quiet_settings():
percentile = await get_config("quiet_hours_percentile", QUIET_PERCENTILE)
multiplier = await get_config("quiet_hours_multiplier", QUIET_HOUR_MULTIPLIER)
fb_start = await get_config("quiet_hours_fallback_start", QUIET_FALLBACK_START)
fb_end = await get_config("quiet_hours_fallback_end", QUIET_FALLBACK_END)
min_days = await get_config("quiet_hours_min_days", 7)
return {
"percentile": float(percentile),
"multiplier": float(multiplier),
"fallback_start": int(fb_start),
"fallback_end": int(fb_end),
"min_days": int(min_days),
}
def _is_in_fallback_quiet_window(hour: int, start: int, end: int) -> bool:
# Handles ranges that may wrap around midnight (e.g., 22 -> 6)
if start == end:
return False
if start < end:
return start <= hour < end
# wrapped
return hour >= start or hour < end
async def is_quiet_now() -> tuple[bool, float]:
settings = await get_quiet_settings()
hour = datetime.now(timezone.utc).hour
quiet = _is_in_fallback_quiet_window(hour, settings["fallback_start"], settings["fallback_end"])
mult = settings["multiplier"] if quiet else 1.0
try:
mult = max(1.0, float(mult))
except Exception as e:
logger.warning(f"{e}, Invalid quiet multiplier: {mult}. Using default 1.0.")
mult = 1.0
return quiet, mult
async def get_recent_revives(limit: int = 10) -> list[str]:
async with aiosqlite.connect(DB_FILE) as db:
cur = await db.execute(
"SELECT question FROM revive_history ORDER BY id DESC LIMIT ?",
(limit,)
)
rows = await cur.fetchall()
return [r[0] for r in rows]
async def save_revive_question(question: str):
async with aiosqlite.connect(DB_FILE) as db:
await db.execute(
"INSERT INTO revive_history (question, time) VALUES (?, ?)",
(question, datetime.now(timezone.utc).isoformat())
)
await db.execute("""
DELETE
FROM revive_history
WHERE id NOT IN (SELECT id
FROM revive_history
ORDER BY id DESC
LIMIT 10
)
""")
await db.commit() await db.commit()
@@ -491,20 +359,6 @@ MOTD_LIST = []
motd_index = 0 motd_index = 0
async def get_effective_revive_interval() -> float:
"""Return the base revive interval in seconds, considering live config overrides.
Ensures it's at least the compiled default to avoid accidental speed-ups.
"""
cfg_val = await get_config("revive_interval", REVIVE_INTERVAL)
try:
base = float(cfg_val)
except Exception as e:
logger.warning(f"{e}, Invalid revive interval: {cfg_val}. Using default {REVIVE_INTERVAL}.")
base = float(REVIVE_INTERVAL)
return max(float(REVIVE_INTERVAL), base)
async def get_config(key: str, default=None): async def get_config(key: str, default=None):
async with aiosqlite.connect(DB_FILE) as db: async with aiosqlite.connect(DB_FILE) as db:
cur = await db.execute("SELECT value FROM config WHERE key = ?", (key,)) cur = await db.execute("SELECT value FROM config WHERE key = ?", (key,))
@@ -532,13 +386,6 @@ async def on_ready():
scheduler.add_job(check_circuit_recovery, "interval", minutes=1) scheduler.add_job(check_circuit_recovery, "interval", minutes=1)
scheduler.add_job(decay_topics, "interval", hours=1) scheduler.add_job(decay_topics, "interval", hours=1)
scheduler.add_job(prune_hourly_activity, "interval", hours=1) scheduler.add_job(prune_hourly_activity, "interval", hours=1)
scheduler.add_job(
lambda: bot.loop.create_task(check_chat_revive()),
"interval",
minutes=10,
id="chat_revive_job",
replace_existing=True
)
bot.loop.create_task(update_presence()) bot.loop.create_task(update_presence())
motd = await get_config("motd_list", []) motd = await get_config("motd_list", [])
global MOTD_LIST, motd_index global MOTD_LIST, motd_index
@@ -839,15 +686,11 @@ async def on_raw_reaction_remove(payload: discord.RawReactionActionEvent):
@bot.event @bot.event
async def on_message(message: discord.Message): async def on_message(message: discord.Message):
global last_activity_time, revive_sent
if message.author.bot: if message.author.bot:
return return
if message.guild is None or not isinstance(message.author, discord.Member): if message.guild is None or not isinstance(message.author, discord.Member):
await bot.process_commands(message) await bot.process_commands(message)
return return
if message.channel.id == REVIVE_CHANNEL_ID:
last_activity_time = datetime.now(timezone.utc)
revive_sent = False
channel_id = str(message.channel.id) channel_id = str(message.channel.id)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
if channel_id not in recent_message_history: if channel_id not in recent_message_history:
@@ -905,7 +748,6 @@ async def on_message(message: discord.Message):
async def slash_tars(interaction: discord.Interaction, command: str | None = None): async def slash_tars(interaction: discord.Interaction, command: str | None = None):
version = BOT_VERSION version = BOT_VERSION
ai_status = "ONLINE" if FEATURE_FLAGS["ai_enabled"] else "OFFLINE" ai_status = "ONLINE" if FEATURE_FLAGS["ai_enabled"] else "OFFLINE"
revive_status = "ONLINE" if FEATURE_FLAGS["revive_enabled"] else "OFFLINE"
if command: if command:
await tars_command_help(interaction, command) await tars_command_help(interaction, command)
return return
@@ -914,7 +756,6 @@ async def slash_tars(interaction: discord.Interaction, command: str | None = Non
f"**Version:** `{version}`\n\n" f"**Version:** `{version}`\n\n"
f"**Subsystem Status**\n" f"**Subsystem Status**\n"
f"� AI: `{ai_status}`\n" f"� AI: `{ai_status}`\n"
f"� Chat Revive: `{revive_status}`\n\n"
"Use `/tars help <command>` for detailed command info.", "Use `/tars help <command>` for detailed command info.",
"info" "info"
) )
@@ -1088,70 +929,6 @@ async def send_reminder(user_id, channel_id, text):
await handle_error(e) await handle_error(e)
async def check_chat_revive():
global last_activity_time, revive_sent
channel = bot.get_channel(REVIVE_CHANNEL_ID)
if not channel:
logger.error("Revive channel not found.")
return
time_style = get_time_of_day()
season = get_season()
style_prompt = REVIVE_STYLE_PROMPTS.get(time_style, "")
season_prompt = SEASONAL_PROMPTS.get(season, "")
channel_theme = CHANNEL_THEMES.get(channel.id, "Open-ended discussion starter")
role = channel.guild.get_role(REVIVE_ROLE_ID)
if not role:
logger.error("Revive role not found.")
return
if not FEATURE_FLAGS["revive_enabled"]:
return
if last_activity_time is None:
try:
async for msg in channel.history(limit=1):
last_activity_time = msg.created_at
break
except Exception as e:
logger.error(f"History read failed: {e}")
return
if last_activity_time is None:
last_activity_time = datetime.now(timezone.utc)
inactivity = (datetime.now(timezone.utc) - last_activity_time).total_seconds()
# Use effective base interval (min 24h) and slow down during quiet hours
base_interval = await get_effective_revive_interval()
quiet, multiplier = await is_quiet_now()
dynamic_interval = base_interval * multiplier
if inactivity < dynamic_interval or revive_sent:
return
logger.info(
f"[Revive Check] inactivity={inactivity:.0f}s revive_sent={revive_sent} "
f"quiet={quiet} multiplier={multiplier} interval={dynamic_interval:.0f}s"
)
recent_questions = await get_recent_revives()
avoid_text = ""
if recent_questions:
avoid_text = (
"Avoid reusing or closely paraphrasing these recent questions:\n"
+ "\n".join(f"- {q}" for q in recent_questions)
)
prompt = (
f"{style_prompt}\n"
f"{season_prompt}\n"
f"Theme: {channel_theme}\n\n"
"Generate ONE single-sentence question.\n"
"No emojis. No lists. No meta commentary.\n"
"Be natural, engaging, and non-repetitive.\n\n"
f"{avoid_text}"
)
question = await tars_ai_respond(prompt, "T.A.R.S.")
global LAST_REVIVE_TIME
LAST_REVIVE_TIME = datetime.now(timezone.utc)
await save_revive_question(question)
safe_question = sanitize_discord_mentions(question)
await channel.send(f"{role.mention} � {safe_question}")
revive_sent = True
logger.info("Chat revive sent successfully.")
@tree.command(name="reactionrole", description="Create a reaction role (admin only)") @tree.command(name="reactionrole", description="Create a reaction role (admin only)")
@app_commands.describe(message_id="ID of message to attach", emoji="Emoji", role="Role to give") @app_commands.describe(message_id="ID of message to attach", emoji="Emoji", role="Role to give")
async def slash_reactionrole(interaction: discord.Interaction, message_id: str, emoji: str, role: discord.Role): async def slash_reactionrole(interaction: discord.Interaction, message_id: str, emoji: str, role: discord.Role):
@@ -1574,10 +1351,6 @@ async def slash_status(interaction: discord.Interaction):
scheduler_running = scheduler.running scheduler_running = scheduler.running
openai_ok = await check_openai_health() openai_ok = await check_openai_health()
db_ok = await check_db_health() db_ok = await check_db_health()
revive_time = (
LAST_REVIVE_TIME.isoformat(timespec="seconds")
if LAST_REVIVE_TIME else "Never"
)
error_time = ( error_time = (
LAST_ERROR_TIME.isoformat(timespec="seconds") LAST_ERROR_TIME.isoformat(timespec="seconds")
if LAST_ERROR_TIME else "None" if LAST_ERROR_TIME else "None"
@@ -1600,8 +1373,6 @@ async def slash_status(interaction: discord.Interaction):
inline=True inline=True
) )
embed.add_field(name="AI Enabled", value=str(FEATURE_FLAGS["ai_enabled"]), inline=True) embed.add_field(name="AI Enabled", value=str(FEATURE_FLAGS["ai_enabled"]), inline=True)
embed.add_field(name="Revive Enabled", value=str(FEATURE_FLAGS["revive_enabled"]), inline=True)
embed.add_field(name="Last Revive Sent", value=revive_time, inline=False)
embed.add_field(name="Last Error", value=error_time, inline=False) embed.add_field(name="Last Error", value=error_time, inline=False)
embed.set_footer(text="� T.A.R.S. Diagnostics") embed.set_footer(text="� T.A.R.S. Diagnostics")
await interaction.response.send_message(embed=embed, ephemeral=True) await interaction.response.send_message(embed=embed, ephemeral=True)