added config command to disable rhymes

This commit is contained in:
Anselme 2026-08-06 15:26:57 +02:00
parent b38d596bca
commit 4e966eb821
6 changed files with 133 additions and 276 deletions

View File

@ -9,7 +9,7 @@ https://git.epicsparrow.com/Anselme/SparrowBot/src/branch/master/app/fourasmodul
`/fouras` pour demander une énigme `/fouras` pour demander une énigme
`/indice` pour demander un indice sur l'énigme courante `/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) `/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 # 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 : Un administrateur du serveur doit cliquer sur ce lien :
https://discord.com/api/oauth2/authorize?client_id=1110208055171367014&permissions=274877975552&scope=bot 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 # Pour les développeurs

View File

@ -1,11 +1,43 @@
# database.py # database.py
import sqlite3 import sqlite3
from datetime import datetime import random
from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Tuple from typing import Any, Dict, Tuple
DB_FILE = "data/database.db" 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: def ensure_db() -> None:
"""Initialize SQLite database and create tables if needed.""" """Initialize SQLite database and create tables if needed."""
@ -16,7 +48,6 @@ def ensure_db() -> None:
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS guild_state ( CREATE TABLE IF NOT EXISTS guild_state (
guild_id TEXT PRIMARY KEY, guild_id TEXT PRIMARY KEY,
guild_name TEXT NOT NULL DEFAULT '',
cooldown_until TEXT NOT NULL DEFAULT '1970-01-01T00:00:00', cooldown_until TEXT NOT NULL DEFAULT '1970-01-01T00:00:00',
cooldown_ratio REAL NOT NULL DEFAULT 1.0, cooldown_ratio REAL NOT NULL DEFAULT 1.0,
self_control 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]: def get_guild_state(guild_id: str) -> Dict[str, Any]:
"""Retrieve guild state from database."""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(GET_GUILD_STATE_QUERY,(guild_id,))
"""
SELECT guild_id, guild_name, cooldown_until, cooldown_ratio, self_control, last_updated
FROM guild_state WHERE guild_id = ?
""",
(guild_id,),
)
row = cursor.fetchone() 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: if row:
return { return {
"guild_id": row["guild_id"],
"guild_name": row["guild_name"],
"cooldown_until": row["cooldown_until"], "cooldown_until": row["cooldown_until"],
"cooldown_ratio": row["cooldown_ratio"], "cooldown_ratio": row["cooldown_ratio"],
"self_control": row["self_control"], "self_control": row["self_control"],
"last_updated": row["last_updated"], "last_updated": row["last_updated"],
} }
else: return {}
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(),
}
def update_guild_state( def rhyme_configure_cooldown_ratio(guild_id: str, cooldown_ratio: float) -> bool:
guild_id: str, guild_name: str, cooldown_until: str, cooldown_ratio: float, self_control: float 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: ) -> None:
"""Update guild state in database."""
with get_connection() as conn: 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 = conn.cursor()
cursor.execute( cursor.execute(RESET_COOLDOWN_QUERY, (cooldown_until.isoformat(), guild_id,))
"""
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(),
),
)
conn.commit() 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): def get_active_riddle(channel_id: str):
"""Récupérer une énigme active pour un canal spécifique."""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute("SELECT * FROM active_riddles WHERE channel_id = ?", (str(channel_id),))
"SELECT * FROM active_riddles WHERE channel_id = ?", (str(channel_id),)
)
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@ -157,7 +153,6 @@ def save_active_riddle(
message_id: int, message_id: int,
solver_id: str = None, solver_id: str = None,
): ):
"""Sauvegarder ou mettre à jour une énigme active."""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Si solver_id existe, on considère que c'est résolu et on garde l'entrée pour historique # 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) (channel_id, riddle_index, nb_clues, message_id, solver_id, updated_at)
VALUES (?, ?, ?, ?, ?, datetime('now')) 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() conn.commit()
@ -178,9 +173,7 @@ def remove_active_riddle(channel_id: str) -> bool:
"""Supprimer une énigme active du suivi.""" """Supprimer une énigme active du suivi."""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute("DELETE FROM active_riddles WHERE channel_id = ?", (str(channel_id),))
"DELETE FROM active_riddles WHERE channel_id = ?", (str(channel_id),)
)
conn.commit() conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0

107
fouras.py
View File

@ -3,7 +3,7 @@ import json
import os import os
import random import random
import re import re
from typing import Any, Dict, Optional from typing import Any, Dict
import discord import discord
from discord import Interaction, app_commands from discord import Interaction, app_commands
@ -30,6 +30,14 @@ Code Source : https://git.epicsparrow.com/Anselme/perefouras
Ajouter ce bot à votre serveur : {url} 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 = """ SUCCESS = """
Bravo {user} ! La réponse était bien `{answer}`. 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.""" """Handle debug commands (debug, save, load, broadcast). Returns True if handled."""
message_content = message.content.lower() 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) broadcast_match = re.match(r"^broadcast\s+(\d+) (.*)", message.content)
if broadcast_match and message.author.id == MAINTAINER_ID: if broadcast_match and message.author.id == MAINTAINER_ID:
index = int(broadcast_match.group(1)) 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, channel_id=message.channel.id,
message=message.content, message=message.content,
history=messages_json, history=messages_json,
state=state_json, # state=state_json,
) )
) )
await message.channel.send( 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: 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: if random.random() <= 0.03:
await interaction.response.send_message("Non") await interaction.response.send_message("Non")
else: else:
@ -375,11 +335,11 @@ def setup_commands(client: discord.Client, tree) -> None:
@tree.command( @tree.command(
name="fouras_requete", name="requete",
description="Demander une énigme spécifique au Père Fouras" description="Demander une énigme spécifique au Père Fouras"
) )
@app_commands.describe(index="Numéro de l'énigme") @app_commands.describe(index="Numéro de l'énigme")
async def fouras_requete( async def requete(
interaction: discord.Interaction, interaction: discord.Interaction,
index: int, index: int,
): ):
@ -413,15 +373,15 @@ def setup_commands(client: discord.Client, tree) -> None:
) )
@tree.command( # @tree.command(
name="about", # name="about",
description="Qui est le Père Fouras ?" # description="Qui est le Père Fouras ?"
) # )
async def about( # async def about(
interaction: discord.Interaction, # interaction: discord.Interaction,
): # ):
author_user = await client.fetch_user(AUTHOR_ID) # author_user = await client.fetch_user(AUTHOR_ID)
await interaction.response.send_message(ABOUT.format(user=author_user.mention, url=API_URL), ephemeral=True) # await interaction.response.send_message(ABOUT.format(user=author_user.mention, url=API_URL), ephemeral=True)
@tree.command( @tree.command(
@ -459,28 +419,3 @@ def setup_commands(client: discord.Client, tree) -> None:
if(not await update_riddle_message(interaction.channel, current_riddle)): if(not await update_riddle_message(interaction.channel, current_riddle)):
return return
await interaction.response.send_message("Nouvel indice : `{0}`".format(clue_string(answer, nb_clues))) 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
)

View File

@ -26,7 +26,7 @@ async def on_ready():
"""Initialize bot state and sync commands.""" """Initialize bot state and sync commands."""
# Import initialization functions # Import initialization functions
from fouras import load_riddles, setup_commands 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 directories exist
ensure_log_file() ensure_log_file()
@ -36,6 +36,7 @@ async def on_ready():
# register fouras commands # register fouras commands
setup_commands(client, tree) setup_commands(client, tree)
setup_rhymes_commands(client, tree)
# Load rhymes # Load rhymes
success, msg = load_rhymes() success, msg = load_rhymes()

View File

@ -932,7 +932,7 @@ Qui parlent souvent d'amour
D'une grande dureté est le bois de son cœur D'une grande dureté est le bois de son cœur
Son nom témoigne de sa couleur 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 fait l'objet d'un certain trafic
Elle plonge dans un bruit Elle plonge dans un bruit

150
rhymes.py
View File

@ -4,6 +4,8 @@ import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Tuple from typing import Any, Dict, Tuple
import discord
from discord import Interaction, app_commands
import database import database
@ -74,71 +76,6 @@ async def get_guild_name(guildId, client) -> str:
return "[Server={0}]".format(guild.name) 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: async def handle_rhyme_logic(message, client) -> bool:
"""Main rhyme detection logic. Returns True if rhyme was triggered.""" """Main rhyme detection logic. Returns True if rhyme was triggered."""
message_content = message.content.lower() 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: if message.author != client.user and message.guild and last_word:
rhyme = find_rhyme(last_word) rhyme = find_rhyme(last_word)
guild_id = str(message.guild.id) guild_id = str(message.guild.id)
guild_name = message.guild.name
if rhyme: if rhyme:
guild_state = database.get_guild_state(guild_id) guild_state = database.get_guild_state(guild_id)
cooldown_ratio = guild_state["cooldown_ratio"]
# Update guild name if changed if cooldown_ratio >= 0:
if guild_state["guild_name"] != guild_name: # Check cooldown
database.update_guild_state( cooldown_dt = datetime.fromisoformat(guild_state["cooldown_until"])
guild_id, now_dt = datetime.now()
guild_name=guild_name,
cooldown_until=guild_state["cooldown_until"],
cooldown_ratio=guild_state["cooldown_ratio"],
self_control=guild_state["self_control"],
)
# Check cooldown if now_dt >= cooldown_dt:
cooldown_dt = datetime.fromisoformat(guild_state["cooldown_until"]) self_control = guild_state["self_control"]
now_dt = datetime.now()
if now_dt >= cooldown_dt: # Probability check
self_control = guild_state["self_control"] if random.random() < self_control:
database.rhyme_damage_self_control(guild_id)
return False
# Probability check database.rhyme_reset_cooldown(guild_id, cooldown_ratio)
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
# Calculate new cooldown duration await message.channel.send(rhyme)
wait_time = random.randint(0, 900) return True
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
return False return False
@ -208,9 +116,27 @@ async def handle_message(message, client) -> bool:
database.ensure_db() database.ensure_db()
ensure_log_file() ensure_log_file()
# Handle debug commands first
if await handle_debug_commands(message, client):
return True
# Process rhyme logic # Process rhyme logic
return await handle_rhyme_logic(message, client) 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)