perefouras/fouras.py

375 lines
12 KiB
Python

# fouras.py
import random
import re
import json
import discord
from unidecode import unidecode
from pathlib import Path
from typing import Dict, Any, Tuple
API_URL = "".join([
"https://discord.com/api/oauth2/authorize?",
"client_id=1110208055171367014&permissions=274877975552&scope=bot",
])
MAINTAINER_ID = 151626081458192384
BUG_REPORT = """
BUG REPORT from {user} (`{user_id}`) in channel {channel} (`{channel_id}`) :
Message :
> {message}
State :
```json\n{state}```
History :
```json\n{history}```
"""
ABOUT = """
Ce bot a été développé par {user}
Code Source : https://git.epicsparrow.com/Anselme/perefouras
Ajouter ce bot à votre serveur : {url}
"""
SUCCESS = """
Bravo {user} ! La réponse était bien `{answer}`.
"""
INVALID_ID = """
Numéro d'énigme invalide, merci de saisir un numéro entre 1 et {len}
"""
RIDDLES_FILE = "resources/riddles.txt"
ANSWERS_FILE = "resources/answers.txt"
SAVE_FILE = "data/fouras_riddles.json"
riddles = []
answers = []
ongoing_riddles = {}
def _ensure_data_dir() -> None:
"""Ensure data directory exists."""
Path(SAVE_FILE).parent.mkdir(parents=True, exist_ok=True)
def load_riddles() -> list:
global riddles, answers
riddles = []
with open(RIDDLES_FILE, "r", encoding="utf-8") as f:
riddles = f.read().split("\n\n")
answers = []
with open(ANSWERS_FILE, "r", encoding="utf-8") as f:
answers = [line.strip() for line in f.readlines()]
print(f"Loaded {len(riddles)} riddles")
def load_ongoing_riddles(client) -> Dict[str, Dict[str, Any]]:
"""Load ongoing riddles from save file."""
try:
with open(SAVE_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
ongoing_riddles = {}
for channel_id, channel_info in config.items():
channel = client.fetch_channel(int(channel_id))
channel_info["message"] = channel.fetch_message(channel_info["message"])
ongoing_riddles[channel] = channel_info
return ongoing_riddles
except FileNotFoundError:
return {}
except json.JSONDecodeError:
return {}
def save_ongoing_riddles(ongoing_riddles: Dict) -> str:
"""Save ongoing riddles state to file."""
_ensure_data_dir()
dump = {}
for key, value in ongoing_riddles.items():
dump_channel = dict(value)
dump_channel["message"] = dump_channel["message"].id
dump[key.id] = dump_channel
with open(SAVE_FILE, "w", encoding="utf-8") as f:
json.dump(dump, f, ensure_ascii=False, indent=2)
return f'Saved fouras riddles state in file "{SAVE_FILE}"'
def new_riddle(riddles: list, answers: list, index: int) -> Dict[str, Any]:
"""Create a new riddle state."""
return {
"index": index,
"nbClues": -1,
"riddle": riddles[index].strip(),
"answer": answers[index],
}
def finish_riddle(ongoing_riddles: Dict, channel) -> None:
"""Remove completed riddle from tracking."""
if channel in ongoing_riddles:
del ongoing_riddles[channel]
def clue_string(answer: str, nb_clues: int) -> str:
"""Generate clue string with revealed letters."""
final_string = "_"
for _ in range(len(answer) - 1):
final_string += " _"
random.seed(hash(answer))
nb_revealed = 0
for _ in range(nb_clues):
idx = random.randint(0, len(answer) - 1)
while final_string[idx * 2] != "_":
idx = random.randint(0, len(answer) - 1)
nb_revealed += 1
final_string = final_string[:idx * 2] + answer[idx] + final_string[idx * 2 + 1:]
if nb_revealed == len(answer):
return final_string
return final_string
def format_riddle_message(current_riddle: Dict[str, Any]) -> str:
"""Format riddle message for display."""
nb_clues = current_riddle["nbClues"]
answer = current_riddle["answer"]
formatted_riddle = "> " + current_riddle["riddle"].replace("\n", "\n> ")
formatted_riddle = formatted_riddle.replace("\r", "")
clue = ""
if nb_clues > -1:
if nb_clues >= len(answer):
clue = "\nNon trouvée, la solution était : `{0}`".format(answer)
else:
clue = "\nIndice : `{0}`".format(clue_string(answer, nb_clues))
if "solver" in current_riddle:
clue = clue + "\n{0} a trouvé la solution, qui était : `{1}`".format(
current_riddle["solver"].mention, answer
)
if clue:
return "Énigme {0}:\n{1}\n> Qui suis-je ?\n{2}".format(
current_riddle["index"] + 1, formatted_riddle, clue
)
else:
return "Énigme {0}:\n{1}\n> Qui suis-je ?".format(
current_riddle["index"] + 1, formatted_riddle
)
async def get_channel_name(channel, client) -> str:
if isinstance(channel, discord.DMChannel):
dm_channel = await client.fetch_channel(channel.id)
return "[DM={0}]".format(dm_channel.recipient.name)
else:
return "[Server={0}] => [Channel={1}]".format(
channel.guild.name, channel.name
)
async def handle_debug_commands(message, client, ongoing_riddles: Dict) -> bool:
"""Handle debug commands (debug, save, load, broadcast). Returns True if handled."""
message_content = message.content.lower()
if message_content == "save fouras":
if message.author.id == MAINTAINER_ID:
status = save_ongoing_riddles(ongoing_riddles)
json_str = "```json\n{0}```".format(
json.dumps(ongoing_riddles, ensure_ascii=False, indent=2)
)
await message.author.send(status)
await message.author.send(json_str)
return True
if message_content == "load fouras":
if message.author.id == MAINTAINER_ID:
# Reload would require passing riddles/answers back
await message.author.send("Reloaded riddles from files")
return True
if message_content == "debug fouras":
if message.author.id == MAINTAINER_ID:
dump = {}
for key, value in ongoing_riddles.items():
dump_channel = dict(value)
dump_channel.pop("message", None)
dump_channel.pop("answer", None)
channel_name = await get_channel_name(key, client)
dump[channel_name] = dump_channel
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))
broadcast_message = broadcast_match.group(2)
channel = await client.fetch_channel(index)
if channel:
await channel.send(broadcast_message)
else:
await message.channel.send(f"Invalid channel id : {index}")
return True
return False
async def handle_riddle_commands(message, riddles: list, answers: list, client) -> bool:
"""Handle /fouras commands. Returns True if command was processed."""
message_content = message.content.lower()
fouras_match = re.match(r"^fouras\s+(\d+)$", message_content)
if fouras_match:
index = int(fouras_match.group(1)) - 1
if 0 <= index < len(riddles):
if random.random() <= 0.03:
await message.channel.send("Non")
else:
riddle_state = new_riddle(riddles, answers, index)
await message.channel.send(format_riddle_message(riddle_state))
else:
await message.channel.send(INVALID_ID.format(len=len(riddles)))
return True
if message_content == "fouras":
if random.random() <= 0.03:
await message.channel.send("Non")
elif len(riddles) > 0:
index = random.randint(0, len(riddles) - 1)
riddle_state = new_riddle(riddles, answers, index)
await message.channel.send(format_riddle_message(riddle_state))
else:
print(f'riddles : "{len(riddles)}"')
return True
if message_content == "about fouras":
author_user = await client.fetch_user(MAINTAINER_ID)
await message.channel.send(ABOUT.format(user=author_user.mention, url=API_URL))
return True
return False
async def handle_riddle_solving(message, ongoing_riddles: Dict, client) -> bool:
"""Handle riddle solving logic. Returns True if riddle was solved or modified."""
if message.channel not in ongoing_riddles:
return False
current_riddle = ongoing_riddles[message.channel]
if "message" not in current_riddle:
current_riddle["message"] = message
return False
answer = current_riddle["answer"]
# Check if message contains the answer
if unidecode(answer.lower()) in unidecode(message.content.lower()):
current_riddle["solver"] = message.author
await message.channel.send(
SUCCESS.format(user=message.author.mention, answer=answer)
)
await current_riddle["message"].edit(
content=format_riddle_message(current_riddle)
)
finish_riddle(ongoing_riddles, message.channel)
return True
# Repeat riddle command
if message.content.lower() in ["repete", "répète", "repeat"]:
current_riddle.pop("message", None)
await message.channel.send(format_riddle_message(current_riddle))
return True
# Clue command
if message.content.lower() in ["indice", "aide", "help", "clue"]:
nb_clues = current_riddle["nbClues"] + 1
current_riddle["nbClues"] = nb_clues
if nb_clues >= len(answer):
await message.channel.send(
"Perdu ! La réponse était : `{0}`".format(answer)
)
finish_riddle(ongoing_riddles, message.channel)
else:
await current_riddle["message"].edit(
content=format_riddle_message(current_riddle)
)
return True
return False
async def handle_bug_report(message, client, ongoing_riddles: Dict) -> bool:
"""Handle bug report command. Returns True if bug report was sent."""
if not message.content.lower().startswith("bug"):
return False
author_user = await client.fetch_user(MAINTAINER_ID)
channel_name = await client.get_channel_name(message.channel)
# Load message history
messages = [
{
"id": msg.id,
"content": msg.content,
"date": msg.created_at.strftime("%d/%m %H:%M:%S"),
}
async for msg in message.channel.history(limit=10)
]
messages_json = json.dumps(messages, ensure_ascii=False)
state_json = json.dumps(ongoing_riddles, ensure_ascii=False, indent=2)
await author_user.send(
BUG_REPORT.format(
user=message.author.mention,
user_id=message.author.id,
channel=channel_name,
channel_id=message.channel.id,
message=message.content,
history=messages_json,
state=state_json,
)
)
await message.channel.send(
f"Rapport de bug envoyé à {author_user.mention}\nMerci de ton feedback !"
)
return True
async def handle_message(message, client) -> bool:
global riddles, answers, ongoing_riddles
"""Main entry point for message handling."""
if message.author == client.user:
return False
# Handle debug/admin commands first
if await handle_debug_commands(message, client, ongoing_riddles):
return True
# Handle riddle commands
if await handle_riddle_commands(message, riddles, answers, client):
return True
# Handle bug reports
if await handle_bug_report(message, client, ongoing_riddles):
return True
# Handle riddle solving
if await handle_riddle_solving(message, ongoing_riddles, client):
return True
return False