diff --git a/README.md b/README.md index 2bcd27d..2ecea04 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ https://git.epicsparrow.com/Anselme/SparrowBot/src/branch/master/app/fourasmodul `/fouras` pour demander une énigme `/indice` pour demander un indice sur l'énigme courante -`/fouras_requete` pour demander une énigme précise +`/requete` pour demander une énigme précise `/repete` pour que le père fouras répète l'énigme (pratique si vous avez la flemme de scroller quand il y a eu plein de messages) # Pour les admins @@ -18,6 +18,8 @@ https://git.epicsparrow.com/Anselme/SparrowBot/src/branch/master/app/fourasmodul Un administrateur du serveur doit cliquer sur ce lien : https://discord.com/api/oauth2/authorize?client_id=1110208055171367014&permissions=274877975552&scope=bot +## Comment configurer ou désactiver les "poil au" +`/config [ratio]` ratio est multiplié au cooldown aléatoire des "poil au", un ratio négatif désactive complètement la feature # Pour les développeurs diff --git a/database.py b/database.py index 737aab9..b847efd 100644 --- a/database.py +++ b/database.py @@ -1,11 +1,43 @@ # database.py import sqlite3 -from datetime import datetime +import random +from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, Tuple DB_FILE = "data/database.db" +# guild-state queries +CREATE_GUILD_STATE_QUERY = """ +INSERT INTO guild_state (guild_id, cooldown_until, cooldown_ratio, self_control, last_updated) +VALUES (?, datetime('now'), 1.0, 1.0, datetime('now')) +""" + +GET_GUILD_STATE_QUERY = """ +SELECT cooldown_until, cooldown_ratio, self_control, last_updated +FROM guild_state +WHERE guild_id = ? +""" + +CONFIGURE_COOLDOWN_RATIO_QUERY = """ +UPDATE guild_state +SET cooldown_ratio = ? +WHERE guild_id = ? +""" + +DAMAGE_SELF_CONTROL_QUERY = """ +UPDATE guild_state +SET self_control = self_control * 0.9 +WHERE guild_id = ? +""" + +RESET_COOLDOWN_QUERY = """ +UPDATE guild_state +SET cooldown_until = ?, + self_control = self_control + 1.0, + last_updated = datetime('now') + WHERE guild_id = ? +""" def ensure_db() -> None: """Initialize SQLite database and create tables if needed.""" @@ -16,7 +48,6 @@ def ensure_db() -> None: cursor.execute(""" CREATE TABLE IF NOT EXISTS guild_state ( guild_id TEXT PRIMARY KEY, - guild_name TEXT NOT NULL DEFAULT '', cooldown_until TEXT NOT NULL DEFAULT '1970-01-01T00:00:00', cooldown_ratio REAL NOT NULL DEFAULT 1.0, self_control REAL NOT NULL DEFAULT 1.0, @@ -45,96 +76,61 @@ def get_connection() -> sqlite3.Connection: def get_guild_state(guild_id: str) -> Dict[str, Any]: - """Retrieve guild state from database.""" with get_connection() as conn: cursor = conn.cursor() - cursor.execute( - """ - SELECT guild_id, guild_name, cooldown_until, cooldown_ratio, self_control, last_updated - FROM guild_state WHERE guild_id = ? - """, - (guild_id,), - ) + cursor.execute(GET_GUILD_STATE_QUERY,(guild_id,)) row = cursor.fetchone() + if not row: + cursor.execute(CREATE_GUILD_STATE_QUERY, (guild_id,)) + conn.commit() + + cursor.execute(GET_GUILD_STATE_QUERY,(guild_id,)) + row = cursor.fetchone() + if row: return { - "guild_id": row["guild_id"], - "guild_name": row["guild_name"], "cooldown_until": row["cooldown_until"], "cooldown_ratio": row["cooldown_ratio"], "self_control": row["self_control"], "last_updated": row["last_updated"], } - else: - return { - "guild_id": guild_id, - "guild_name": "", - "cooldown_until": "1970-01-01T00:00:00", - "cooldown_ratio": 1.0, - "self_control": 1.0, - "last_updated": datetime.now().isoformat(), - } + return {} -def update_guild_state( - guild_id: str, guild_name: str, cooldown_until: str, cooldown_ratio: float, self_control: float +def rhyme_configure_cooldown_ratio(guild_id: str, cooldown_ratio: float) -> bool: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(CONFIGURE_COOLDOWN_RATIO_QUERY, (cooldown_ratio, guild_id,)) + conn.commit() + + +def rhyme_damage_self_control(guild_id: str) -> None: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(DAMAGE_SELF_CONTROL_QUERY, (guild_id,)) + conn.commit() + + +def rhyme_reset_cooldown( + guild_id: str, + ratio: float ) -> None: - """Update guild state in database.""" with get_connection() as conn: + # Calculate new cooldown duration + wait_time = random.randint(900, 10800) if bool(random.getrandbits(1)) else random.randint(0, 900) + wait_time = 0 if ratio < 0 else int(wait_time * ratio) + cooldown_until = datetime.now().replace(second=0, microsecond=0) + timedelta(seconds=wait_time) + cursor = conn.cursor() - cursor.execute( - """ - INSERT OR REPLACE INTO guild_state (guild_id, guild_name, cooldown_until, cooldown_ratio, self_control, last_updated) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - guild_id, - guild_name, - cooldown_until, - cooldown_ratio, - self_control, - datetime.now().isoformat(), - ), - ) + cursor.execute(RESET_COOLDOWN_QUERY, (cooldown_until.isoformat(), guild_id,)) conn.commit() -def delete_guild_state(guild_id: str) -> bool: - """Delete guild state from database.""" - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute("DELETE FROM guild_state WHERE guild_id = ?", (guild_id,)) - conn.commit() - return cursor.rowcount > 0 - - -def get_all_guild_states() -> Dict[str, Dict[str, Any]]: - """Retrieve all guild states (for debug purposes).""" - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "SELECT guild_id, guild_name, cooldown_until, cooldown_ratio, self_control, last_updated FROM guild_state" - ) - return { - row["guild_id"]: { - "guild_name": row["guild_name"], - "cooldown_until": row["cooldown_until"], - "cooldown_ratio": row["cooldown_ratio"], - "self_control": row["self_control"], - "last_updated": row["last_updated"], - } - for row in cursor.fetchall() - } - - def get_active_riddle(channel_id: str): - """Récupérer une énigme active pour un canal spécifique.""" with get_connection() as conn: cursor = conn.cursor() - cursor.execute( - "SELECT * FROM active_riddles WHERE channel_id = ?", (str(channel_id),) - ) + cursor.execute("SELECT * FROM active_riddles WHERE channel_id = ?", (str(channel_id),)) row = cursor.fetchone() if row: @@ -157,7 +153,6 @@ def save_active_riddle( message_id: int, solver_id: str = None, ): - """Sauvegarder ou mettre à jour une énigme active.""" with get_connection() as conn: cursor = conn.cursor() # Si solver_id existe, on considère que c'est résolu et on garde l'entrée pour historique @@ -169,7 +164,7 @@ def save_active_riddle( (channel_id, riddle_index, nb_clues, message_id, solver_id, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now')) """, - (str(channel_id), riddle_index, nb_clues, message_id, solver_id), + (str(channel_id), riddle_index, nb_clues, message_id, solver_id) ) conn.commit() @@ -178,9 +173,7 @@ def remove_active_riddle(channel_id: str) -> bool: """Supprimer une énigme active du suivi.""" with get_connection() as conn: cursor = conn.cursor() - cursor.execute( - "DELETE FROM active_riddles WHERE channel_id = ?", (str(channel_id),) - ) + cursor.execute("DELETE FROM active_riddles WHERE channel_id = ?", (str(channel_id),)) conn.commit() return cursor.rowcount > 0 diff --git a/fouras.py b/fouras.py index 8fd3612..c24d8da 100644 --- a/fouras.py +++ b/fouras.py @@ -3,7 +3,7 @@ import json import os import random import re -from typing import Any, Dict, Optional +from typing import Any, Dict import discord from discord import Interaction, app_commands @@ -30,6 +30,14 @@ Code Source : https://git.epicsparrow.com/Anselme/perefouras Ajouter ce bot à votre serveur : {url} """ +BUG_REPORT = """ +BUG REPORT from {user} (`{user_id}`) in channel {channel} (`{channel_id}`) : +> {message} + +History : +```json\n{history}``` +""" + SUCCESS = """ Bravo {user} ! La réponse était bien `{answer}`. """ @@ -146,42 +154,6 @@ async def handle_debug_commands(message, client: discord.Client) -> bool: """Handle debug commands (debug, save, load, broadcast). Returns True if handled.""" message_content = message.content.lower() - if message_content == "debug fouras": - if message.author.id == MAINTAINER_ID: - # Récupération depuis la DB au lieu d'une variable globale - db_riddles = database.get_all_active_riddles() - - dump = {} - for riddle_data in db_riddles: - try: - channel = await client.fetch_channel(int(riddle_data["channel_id"])) - channel_name = await get_channel_name(channel, client) - - # Reconstruction partielle pour l'affichage - riddle_info = { - "riddle_index": riddle_data["riddle_index"], - "nb_clues": riddle_data["nb_clues"], - "message_id": riddle_data["message_id"], - "solver_id": riddle_data["solver_id"], - "created_at": riddle_data["created_at"], - "updated_at": riddle_data["updated_at"], - } - - # On ajoute le texte de l'énigme si disponible - if 0 <= riddle_data["riddle_index"] < len(riddles): - riddle_info["text"] = ( - riddles[riddle_data["riddle_index"]][:50] + "..." - ) - - dump[channel_name] = riddle_info - except Exception as e: - dump[f"Channel_{riddle_data['channel_id']}"] = {"error": str(e)} - - await message.author.send( - "```json\n{0}```".format(json.dumps(dump, ensure_ascii=False, indent=4)) - ) - return True - broadcast_match = re.match(r"^broadcast\s+(\d+) (.*)", message.content) if broadcast_match and message.author.id == MAINTAINER_ID: index = int(broadcast_match.group(1)) @@ -293,7 +265,7 @@ async def handle_bug_report(message, client: discord.Client) -> bool: channel_id=message.channel.id, message=message.content, history=messages_json, - state=state_json, + # state=state_json, ) ) await message.channel.send( @@ -322,18 +294,6 @@ async def handle_message(message, client: discord.Client) -> bool: async def new_riddle(interaction: Interaction, index: int) -> None: - class ConfirmRiddle(discord.ui.View): - def __init__(self): - super().__init__(timeout=60) - - @discord.ui.button(label="Oui", style=discord.ButtonStyle.success) - async def yes(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_message("Confirmé.", ephemeral=True) - - @discord.ui.button(label="Non", style=discord.ButtonStyle.danger) - async def no(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_message("Annulé.", ephemeral=True) - if random.random() <= 0.03: await interaction.response.send_message("Non") else: @@ -375,11 +335,11 @@ def setup_commands(client: discord.Client, tree) -> None: @tree.command( - name="fouras_requete", + name="requete", description="Demander une énigme spécifique au Père Fouras" ) @app_commands.describe(index="Numéro de l'énigme") - async def fouras_requete( + async def requete( interaction: discord.Interaction, index: int, ): @@ -413,15 +373,15 @@ def setup_commands(client: discord.Client, tree) -> None: ) - @tree.command( - name="about", - description="Qui est le Père Fouras ?" - ) - async def about( - interaction: discord.Interaction, - ): - author_user = await client.fetch_user(AUTHOR_ID) - await interaction.response.send_message(ABOUT.format(user=author_user.mention, url=API_URL), ephemeral=True) + # @tree.command( + # name="about", + # description="Qui est le Père Fouras ?" + # ) + # async def about( + # interaction: discord.Interaction, + # ): + # author_user = await client.fetch_user(AUTHOR_ID) + # await interaction.response.send_message(ABOUT.format(user=author_user.mention, url=API_URL), ephemeral=True) @tree.command( @@ -459,28 +419,3 @@ def setup_commands(client: discord.Client, tree) -> None: if(not await update_riddle_message(interaction.channel, current_riddle)): return await interaction.response.send_message("Nouvel indice : `{0}`".format(clue_string(answer, nb_clues))) - - @tree.command( - name="testmodal" - ) - async def testmodal( - interaction: Interaction - ): - class Confirm(discord.ui.View): - def __init__(self): - super().__init__(timeout=60) - - @discord.ui.button(label="Oui", style=discord.ButtonStyle.success) - async def yes(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_message("✅ Confirmé.", ephemeral=True) - - @discord.ui.button(label="Non", style=discord.ButtonStyle.danger) - async def no(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_message("❌ Annulé.", ephemeral=True) - - view = Confirm() - await interaction.response.send_message( - "Es-tu sûr ?", - view=view, - ephemeral=True - ) diff --git a/main.py b/main.py index 891dbca..bbb8fc8 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ async def on_ready(): """Initialize bot state and sync commands.""" # Import initialization functions from fouras import load_riddles, setup_commands - from rhymes import ensure_log_file, load_rhymes + from rhymes import ensure_log_file, load_rhymes, setup_rhymes_commands # Ensure directories exist ensure_log_file() @@ -36,6 +36,7 @@ async def on_ready(): # register fouras commands setup_commands(client, tree) + setup_rhymes_commands(client, tree) # Load rhymes success, msg = load_rhymes() diff --git a/resources/riddles.txt b/resources/riddles.txt index 268f725..99486f5 100644 --- a/resources/riddles.txt +++ b/resources/riddles.txt @@ -932,7 +932,7 @@ Qui parlent souvent d'amour D'une grande dureté est le bois de son cœur Son nom témoigne de sa couleur -Taillé, sculpté elle est exotique +Taillée, sculptée elle est exotique Elle fait l'objet d'un certain trafic Elle plonge dans un bruit diff --git a/rhymes.py b/rhymes.py index 7a938ac..841251c 100644 --- a/rhymes.py +++ b/rhymes.py @@ -4,6 +4,8 @@ import random from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, Tuple +import discord +from discord import Interaction, app_commands import database @@ -74,71 +76,6 @@ async def get_guild_name(guildId, client) -> str: return "[Server={0}]".format(guild.name) -async def handle_debug_commands(message, client) -> bool: - """Handle debug commands (debug, save, load). Returns True if handled.""" - message_content = message.content.lower() - - if message_content == "debug poilau": - if message.author.id == 151626081458192384: - all_states = database.get_all_guild_states() - dump = {} - for guild_id, state in all_states.items(): - channel_name = await get_guild_name(guild_id, client) - cooldown_dt = datetime.fromisoformat(state["cooldown_until"]) - time_remaining = max(0, (cooldown_dt - datetime.now()).total_seconds()) - sleeping_time = "{:.2f} s".format(time_remaining) - dump[channel_name] = { - "cooldown_until": state["cooldown_until"], - "cooldown_ratio": state["cooldown_ratio"], - "cooldown_remaining": sleeping_time, - "self-control": state["self_control"], - "last_updated": state["last_updated"], - } - await message.author.send( - "```json\n{0}```".format(json.dumps(dump, ensure_ascii=False, indent=2)) - ) - return True - - if message_content == "save poilau": - if message.author.id == 151626081458192384: - all_states = database.get_all_guild_states() - json_str = "```json\n{0}```".format( - json.dumps(all_states, ensure_ascii=False, indent=2) - ) - await message.author.send("State persisted in SQLite database") - await message.author.send(json_str) - return True - - if message_content == "load poilau": - if message.author.id == 151626081458192384: - success, msg = load_rhymes() - all_states = database.get_all_guild_states() - json_str = "```json\n{0}```".format( - json.dumps(all_states, ensure_ascii=False, indent=2) - ) - await message.author.send(msg) - await message.author.send(json_str) - return True - - if message_content == "tg fouras" and message.guild: - # Disable cooldown for this server (set to far future) - cooldown_date = datetime.now().replace( - hour=0, minute=0, second=0, microsecond=0 - ) - cooldown_date = cooldown_date.replace(day=cooldown_date.day + 10000) - database.update_guild_state( - str(message.guild.id), - guild_name=message.guild.name, - cooldown_until=cooldown_date.isoformat(), - cooldown_ratio=1.0, - self_control=2.0, - ) - await message.channel.send("ok :'(") - return True - - return False - - async def handle_rhyme_logic(message, client) -> bool: """Main rhyme detection logic. Returns True if rhyme was triggered.""" message_content = message.content.lower() @@ -147,57 +84,28 @@ async def handle_rhyme_logic(message, client) -> bool: if message.author != client.user and message.guild and last_word: rhyme = find_rhyme(last_word) guild_id = str(message.guild.id) - guild_name = message.guild.name if rhyme: guild_state = database.get_guild_state(guild_id) + cooldown_ratio = guild_state["cooldown_ratio"] - # Update guild name if changed - if guild_state["guild_name"] != guild_name: - database.update_guild_state( - guild_id, - guild_name=guild_name, - cooldown_until=guild_state["cooldown_until"], - cooldown_ratio=guild_state["cooldown_ratio"], - self_control=guild_state["self_control"], - ) + if cooldown_ratio >= 0: + # Check cooldown + cooldown_dt = datetime.fromisoformat(guild_state["cooldown_until"]) + now_dt = datetime.now() - # Check cooldown - cooldown_dt = datetime.fromisoformat(guild_state["cooldown_until"]) - now_dt = datetime.now() + if now_dt >= cooldown_dt: + self_control = guild_state["self_control"] - if now_dt >= cooldown_dt: - self_control = guild_state["self_control"] + # Probability check + if random.random() < self_control: + database.rhyme_damage_self_control(guild_id) + return False - # Probability check - if random.random() < self_control: - new_self_control = self_control * 0.9 - database.update_guild_state( - guild_id, - guild_name=guild_name, - cooldown_until=now_dt.isoformat(), - cooldown_ratio=1.0, - self_control=new_self_control, - ) - return False + database.rhyme_reset_cooldown(guild_id, cooldown_ratio) - # Calculate new cooldown duration - wait_time = random.randint(0, 900) - if bool(random.getrandbits(1)): - wait_time = random.randint(900, 10800) - - new_cooldown_dt = now_dt.replace(second=0, microsecond=0) + timedelta(seconds=wait_time) - - database.update_guild_state( - guild_id, - guild_name=guild_name, - cooldown_until=new_cooldown_dt.isoformat(), - cooldown_ratio=1.0, - self_control=self_control + 1.0, - ) - - await message.channel.send(rhyme) - return True + await message.channel.send(rhyme) + return True return False @@ -208,9 +116,27 @@ async def handle_message(message, client) -> bool: database.ensure_db() ensure_log_file() - # Handle debug commands first - if await handle_debug_commands(message, client): - return True - # Process rhyme logic return await handle_rhyme_logic(message, client) + + +def setup_rhymes_commands(client: discord.Client, tree: discord.app_commands.CommandTree) -> None: + @tree.command( + name="config", + description="Permet de configurer à quel point le Père Fouras est relou" + ) + @app_commands.default_permissions(manage_guild=True) + @app_commands.checks.has_permissions(manage_guild=True) + @app_commands.describe(ratio="ratio de cooldown (0 = )") + async def config( + interaction: discord.Interaction, + ratio: float + ): + guild_id: str = str(interaction.guild_id) + guild_state = database.get_guild_state(guild_id) + database.rhyme_configure_cooldown_ratio(guild_id, ratio) + msg = f"Le ratio de cooldown passe de {guild_state["cooldown_ratio"]:.2f} à {ratio:.2f} pour le serveur {interaction.guild.name}" + if ratio < 0: + await interaction.response.send_message(f"{msg}\nLes \"poil au\" sont désormais désactivés.", ephemeral=True) + else: + await interaction.response.send_message(f"{msg}\nLes \"poil au\" ont désormais un cooldown entre 0 et {int(180*ratio)} minutes.", ephemeral=True)