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
`/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

View File

@ -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

107
fouras.py
View File

@ -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
)

View File

@ -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()

View File

@ -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

150
rhymes.py
View File

@ -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)