Compare commits
15 Commits
929b9af254
...
2a52d31295
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a52d31295 | ||
|
|
51496646f4 | ||
|
|
4e966eb821 | ||
|
|
b38d596bca | ||
|
|
bc688c0f62 | ||
|
|
37416eca80 | ||
|
|
56bb781791 | ||
|
|
3b2633f780 | ||
|
|
45b6b52d10 | ||
|
|
6c8a729f73 | ||
|
|
b836d3fceb | ||
|
|
343eecb4d8 | ||
|
|
400dcecf13 | ||
|
|
8b2681eee5 | ||
|
|
78827521d5 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
.env
|
||||
*.pyc
|
||||
data
|
||||
__pycache__
|
||||
venv
|
||||
|
||||
@ -10,6 +10,9 @@ RUN apt-get update -q && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create data directory with proper permissions
|
||||
RUN mkdir -p /app/data && chmod 755 /app/data
|
||||
|
||||
# Clone the repository
|
||||
RUN git clone --single-branch --branch master https://git.epicsparrow.com/Anselme/perefouras.git .
|
||||
|
||||
@ -19,5 +22,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
# give exec permissions to main
|
||||
RUN chmod +x main.py
|
||||
|
||||
# Set data directory as volume mount point
|
||||
VOLUME /app/data
|
||||
|
||||
# Command to update the repo and run the script
|
||||
CMD ["sh", "-c", "cd /app && git pull origin master && python main.py"]
|
||||
46
README.md
46
README.md
@ -1,29 +1,41 @@
|
||||
Pour ajouter ce bot à votre serveur discord :
|
||||
https://discord.com/api/oauth2/authorize?client_id=1110208055171367014&permissions=274877975552&scope=bot
|
||||
BOT Discord de l'authentique Père Fouras sur votre channel discord, avec plus de 300 de ses fameuses énigmes à vous poser.
|
||||
|
||||
Merci à ChatGPT pour m'avoir aidé à porter mon vieux bot IRC en C++, en un bot discord en python
|
||||
|
||||
Lien du vieux bot IRC que j'avais codé en 2016 :
|
||||
Ce bot est une refonte totale d'un vieux bot IRC que j'avais développé en C++ en 2016 :
|
||||
https://git.epicsparrow.com/Anselme/SparrowBot/src/branch/master/app/fourasmodule.cpp
|
||||
|
||||
|
||||
# How to run the bot
|
||||
# Pour les utilisateurs
|
||||
|
||||
## Setup env var for the bot
|
||||
`/fouras` pour demander une énigme
|
||||
`/indice` pour demander un indice sur l'énigme courante
|
||||
|
||||
- Create a `.env` file at the root of the project
|
||||
- Add the following content to the file:
|
||||
- `DISCORD_TOKEN` : the token of the bot
|
||||
- `GITEA_API_KEY` : the api key to interact with gitea
|
||||
- `GUILD_ID` : the id of the guild where the bot offer slash commands
|
||||
`/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
|
||||
|
||||
## Comment ajouter le bot à un serveur
|
||||
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
|
||||
|
||||
## Setup python
|
||||
```bash
|
||||
python -m main
|
||||
python -m venv "venv"
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Setup discord
|
||||
Créer un bot et générer un TOKEN sur le portail développeurs :
|
||||
https://discord.com/developers/applications
|
||||
|
||||
## EXPERIMENTAL GITEA API
|
||||
Permission needed:
|
||||
- repo: read
|
||||
- issues: read & write
|
||||
## Setup l'environnement
|
||||
Créer un fichier `.env` à la source du projet avec les entrées suivantes :
|
||||
- `DISCORD_TOKEN` : the token of the bot
|
||||
- `MAINTAINER_ID` : optional discord user id, if you want to receive the bug reports associated to your instance of the bot
|
||||
|
||||
19
client.py
19
client.py
@ -1,19 +0,0 @@
|
||||
import discord
|
||||
from discord import app_commands
|
||||
|
||||
from modules import FourasModule, RhymesModule
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
MODULES = FourasModule, RhymesModule
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
intents.presences = True
|
||||
intents.guilds = True
|
||||
intents.messages = True
|
||||
intents.message_content = True
|
||||
client = discord.Client(intents=intents)
|
||||
|
||||
tree = app_commands.CommandTree(client)
|
||||
185
database.py
Normal file
185
database.py
Normal file
@ -0,0 +1,185 @@
|
||||
# database.py
|
||||
import sqlite3
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
DB_FILE = "data/database.db"
|
||||
|
||||
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 = ?
|
||||
"""
|
||||
|
||||
SAVE_RIDDLE_QUERY = """
|
||||
INSERT OR REPLACE INTO active_riddles
|
||||
(channel_id, riddle_index, nb_clues, message_id, solver_id, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||
"""
|
||||
|
||||
INCREMENT_CLUE_QUERY = """
|
||||
UPDATE active_riddles
|
||||
SET nb_clues = nb_clues + 1
|
||||
WHERE channel_id = ?
|
||||
"""
|
||||
|
||||
DELETE_RIDDLE_QUERY = "DELETE FROM active_riddles WHERE channel_id = ?"
|
||||
|
||||
def ensure_db() -> None:
|
||||
"""Initialize SQLite database and create tables if needed."""
|
||||
Path(DB_FILE).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS guild_state (
|
||||
guild_id TEXT PRIMARY KEY,
|
||||
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,
|
||||
last_updated TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS active_riddles (
|
||||
channel_id TEXT PRIMARY KEY,
|
||||
riddle_index INTEGER NOT NULL,
|
||||
nb_clues INTEGER NOT NULL DEFAULT -1,
|
||||
message_id INTEGER NOT NULL,
|
||||
solver_id TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_connection() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def get_guild_state(guild_id: str) -> Dict[str, Any]:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
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 {
|
||||
"cooldown_until": row["cooldown_until"],
|
||||
"cooldown_ratio": row["cooldown_ratio"],
|
||||
"self_control": row["self_control"],
|
||||
"last_updated": row["last_updated"],
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
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:
|
||||
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(RESET_COOLDOWN_QUERY, (cooldown_until.isoformat(), guild_id,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_active_riddle(channel_id: str) -> Any:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM active_riddles WHERE channel_id = ?", (str(channel_id),))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
"channel_id": row["channel_id"],
|
||||
"riddle_index": row["riddle_index"],
|
||||
"nb_clues": row["nb_clues"],
|
||||
"message_id": row["message_id"],
|
||||
"solver_id": row["solver_id"],
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def save_active_riddle(
|
||||
channel_id: str,
|
||||
riddle_index: int,
|
||||
nb_clues: int,
|
||||
message_id: int,
|
||||
solver_id: str = "",
|
||||
) -> None:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(SAVE_RIDDLE_QUERY, (channel_id, riddle_index, nb_clues, message_id, solver_id))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def increment_clue(channel_id: str) -> None:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(INCREMENT_CLUE_QUERY, (channel_id,),)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def remove_active_riddle(channel_id: str) -> None:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(DELETE_RIDDLE_QUERY, (str(channel_id),))
|
||||
conn.commit()
|
||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
services:
|
||||
perefouras:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- DISCORD_TOKEN=${FOURAS_DISCORD_TOKEN}
|
||||
- MAINTAINER_ID=${FOURAS_MAINTAINER_ID:151626081458192384}
|
||||
restart: always
|
||||
volumes:
|
||||
- perefouras-data:/app/data
|
||||
|
||||
volumes:
|
||||
perefouras-data
|
||||
398
fouras.py
Normal file
398
fouras.py
Normal file
@ -0,0 +1,398 @@
|
||||
# Remplacez les imports globaux et initialisations
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
import discord
|
||||
from discord import Interaction, app_commands
|
||||
from unidecode import unidecode
|
||||
|
||||
import database
|
||||
|
||||
riddles = []
|
||||
answers = []
|
||||
|
||||
API_URL = "".join(
|
||||
[
|
||||
"https://discord.com/api/oauth2/authorize?",
|
||||
"client_id=1110208055171367014&permissions=274877975552&scope=bot",
|
||||
]
|
||||
)
|
||||
|
||||
AUTHOR_ID = 151626081458192384
|
||||
MAINTAINER_ID = os.getenv("MAINTAINER_ID", AUTHOR_ID)
|
||||
|
||||
ABOUT = """
|
||||
Ce bot a été développé par {user}
|
||||
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}`.
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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 new_riddle_state(index: int) -> Dict[str, Any]:
|
||||
return {
|
||||
"index": index,
|
||||
"nbClues": -1,
|
||||
"riddle": riddles[index].strip(),
|
||||
"answer": answers[index],
|
||||
}
|
||||
|
||||
|
||||
async def finish_riddle(channel, client: discord.Client) -> None:
|
||||
database.remove_active_riddle(str(channel.id))
|
||||
|
||||
|
||||
def clue_string(answer: str, nb_clues: int) -> str:
|
||||
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], solved: bool = False, solver_mention: str = ""
|
||||
) -> str:
|
||||
nb_clues = current_riddle.get("nbClues", -1)
|
||||
answer = current_riddle.get("answer", "")
|
||||
|
||||
solver = None
|
||||
if solved and solver_mention:
|
||||
solver = type("obj", (object,), {"mention": solver_mention})()
|
||||
|
||||
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 solved and solver:
|
||||
clue = clue + "\n{0} a trouvé la solution, qui était : `{1}`".format(
|
||||
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: discord.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: discord.Client) -> bool:
|
||||
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)
|
||||
try:
|
||||
channel = await client.fetch_channel(index)
|
||||
if channel:
|
||||
await channel.send(broadcast_message)
|
||||
else:
|
||||
await message.channel.send(f"Invalid channel id : {index}")
|
||||
except discord.errors.NotFound:
|
||||
await message.channel.send(f"Channel not found : {index}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_current_riddle(channel_id):
|
||||
riddle_data = database.get_active_riddle(str(channel_id))
|
||||
|
||||
if not riddle_data:
|
||||
return None
|
||||
|
||||
if 0 <= riddle_data["riddle_index"] < len(riddles):
|
||||
return {
|
||||
"index": riddle_data["riddle_index"],
|
||||
"nbClues": riddle_data["nb_clues"],
|
||||
"riddle": riddles[riddle_data["riddle_index"]].strip(),
|
||||
"answer": answers[riddle_data["riddle_index"]],
|
||||
"data": riddle_data
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
async def handle_riddle_solving(message, client: discord.Client) -> bool:
|
||||
current_riddle = get_current_riddle(message.channel.id)
|
||||
if(not current_riddle):
|
||||
return False
|
||||
answer = current_riddle["answer"]
|
||||
riddle_data = current_riddle["data"]
|
||||
|
||||
if unidecode(answer.lower()) in unidecode(message.content.lower()):
|
||||
solver_id = str(message.author.id)
|
||||
|
||||
database.save_active_riddle(
|
||||
channel_id=str(message.channel.id),
|
||||
riddle_index=riddle_data["riddle_index"],
|
||||
nb_clues=current_riddle["nbClues"],
|
||||
message_id=riddle_data["message_id"],
|
||||
solver_id=solver_id,
|
||||
)
|
||||
|
||||
await message.channel.send(
|
||||
SUCCESS.format(user=message.author.mention, answer=answer)
|
||||
)
|
||||
|
||||
try:
|
||||
original_msg = await message.channel.fetch_message(
|
||||
riddle_data["message_id"]
|
||||
)
|
||||
await original_msg.edit(
|
||||
content=format_riddle_message(
|
||||
current_riddle, solved=True, solver_mention=message.author.mention
|
||||
)
|
||||
)
|
||||
except discord.errors.NotFound:
|
||||
pass
|
||||
|
||||
await finish_riddle(message.channel, client)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def handle_bug_report(message, client: discord.Client) -> bool:
|
||||
if not message.content.lower().startswith("bug"):
|
||||
return False
|
||||
|
||||
author_user = await client.fetch_user(MAINTAINER_ID)
|
||||
channel_name = await get_channel_name(message.channel, client)
|
||||
|
||||
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=6)
|
||||
]
|
||||
messages_json = json.dumps(messages, ensure_ascii=False)
|
||||
|
||||
active_riddle = database.get_active_riddle(message.channel.id)
|
||||
state_json = json.dumps(active_riddle, 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: discord.Client) -> bool:
|
||||
database.ensure_db()
|
||||
|
||||
if await handle_debug_commands(message, client):
|
||||
return True
|
||||
|
||||
if await handle_bug_report(message, client):
|
||||
return True
|
||||
|
||||
if await handle_riddle_solving(message, client):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def new_riddle(interaction: Interaction, index: int) -> None:
|
||||
if random.random() <= 0.03:
|
||||
await interaction.response.send_message("Non")
|
||||
else:
|
||||
riddle_state = new_riddle_state(index)
|
||||
msg = await interaction.response.send_message(
|
||||
format_riddle_message({**riddle_state, "index": index})
|
||||
)
|
||||
database.save_active_riddle(
|
||||
channel_id=str(interaction.channel.id),
|
||||
riddle_index=index,
|
||||
nb_clues=-1,
|
||||
message_id=msg.message_id,
|
||||
solver_id=None
|
||||
)
|
||||
|
||||
|
||||
async def update_riddle_message(channel, current_riddle):
|
||||
try:
|
||||
original_msg = await channel.fetch_message(current_riddle["data"]["message_id"])
|
||||
await original_msg.edit(content=format_riddle_message(current_riddle))
|
||||
return True
|
||||
except discord.errors.NotFound:
|
||||
return False
|
||||
|
||||
|
||||
def setup_commands(client: discord.Client, tree) -> None:
|
||||
@tree.command(
|
||||
name="fouras",
|
||||
description="Demander une énigme au Père Fouras"
|
||||
)
|
||||
async def fouras(
|
||||
interaction: discord.Interaction,
|
||||
):
|
||||
if len(riddles) == 0:
|
||||
await interaction.response.send_message("Désolé, mais j'ai oublié mes énigmes", ephemeral=True)
|
||||
else:
|
||||
index = random.randint(0, len(riddles) - 1)
|
||||
await new_riddle(interaction, index)
|
||||
|
||||
|
||||
@tree.command(
|
||||
name="requete",
|
||||
description="Demander une énigme spécifique au Père Fouras"
|
||||
)
|
||||
@app_commands.describe(index="Numéro de l'énigme")
|
||||
async def requete(
|
||||
interaction: discord.Interaction,
|
||||
index: int,
|
||||
):
|
||||
index = index - 1
|
||||
if not (0 <= index < len(riddles)):
|
||||
await interaction.response.send_message(INVALID_ID.format(len=len(riddles)+1), ephemeral=True)
|
||||
return
|
||||
await new_riddle(interaction, index)
|
||||
|
||||
|
||||
@tree.command(
|
||||
name="repete",
|
||||
description="Demander au Père Fouras de répéter l'énigme en cours"
|
||||
)
|
||||
async def repete(
|
||||
interaction: discord.Interaction,
|
||||
):
|
||||
current_riddle = get_current_riddle(interaction.channel.id)
|
||||
if(not current_riddle):
|
||||
await interaction.response.send_message("Aucune énigme en cours", ephemeral=True)
|
||||
return
|
||||
riddle_data = current_riddle["data"]
|
||||
msg = await interaction.response.send_message(
|
||||
format_riddle_message(current_riddle)
|
||||
)
|
||||
database.save_active_riddle(
|
||||
channel_id=str(interaction.channel.id),
|
||||
riddle_index=riddle_data["riddle_index"],
|
||||
nb_clues=current_riddle["nbClues"],
|
||||
message_id=msg.message_id,
|
||||
solver_id=riddle_data["solver_id"],
|
||||
)
|
||||
|
||||
|
||||
# @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="indice",
|
||||
description="Demander un indice pour l'énigme en cours"
|
||||
)
|
||||
async def indice(
|
||||
interaction: discord.Interaction,
|
||||
):
|
||||
current_riddle = get_current_riddle(interaction.channel.id)
|
||||
if(not current_riddle):
|
||||
await interaction.response.send_message("Aucune énigme en cours", ephemeral=True)
|
||||
return
|
||||
answer = current_riddle["answer"]
|
||||
riddle_data = current_riddle["data"]
|
||||
nb_clues = current_riddle["nbClues"] + 1
|
||||
current_riddle["nbClues"] = nb_clues
|
||||
|
||||
if nb_clues >= len(answer):
|
||||
await interaction.response.send_message(
|
||||
"Perdu ! La réponse était : `{0}`".format(answer)
|
||||
)
|
||||
if(not await update_riddle_message(interaction.channel, current_riddle)):
|
||||
return
|
||||
await finish_riddle(interaction.channel, client)
|
||||
else:
|
||||
database.save_active_riddle(
|
||||
channel_id=str(interaction.channel.id),
|
||||
riddle_index=riddle_data["riddle_index"],
|
||||
nb_clues=nb_clues,
|
||||
message_id=riddle_data["message_id"],
|
||||
solver_id=riddle_data["solver_id"],
|
||||
)
|
||||
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)))
|
||||
73
main.py
73
main.py
@ -1,50 +1,63 @@
|
||||
import discord
|
||||
import os
|
||||
from modules.base import BaseModule
|
||||
|
||||
from client import client, MODULES, tree
|
||||
from modules import gitea
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from dotenv import load_dotenv
|
||||
|
||||
token = os.getenv("DISCORD_TOKEN", "NO_TOKEN")
|
||||
load_dotenv()
|
||||
|
||||
client.riddles = []
|
||||
client.answers = []
|
||||
client.rhyme_keys = {}
|
||||
client.rhyme_strings = {}
|
||||
client.cooldown = {}
|
||||
client.ongoing_riddles = {}
|
||||
client.modules: list[BaseModule] = []
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
intents.presences = True
|
||||
intents.guilds = True
|
||||
intents.messages = True
|
||||
intents.message_content = True
|
||||
|
||||
client = discord.Client(intents=intents)
|
||||
tree = app_commands.CommandTree(client)
|
||||
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
client.modules = [m(client) for m in MODULES]
|
||||
for m in client.modules:
|
||||
await m.load()
|
||||
global client, tree
|
||||
from fouras import load_riddles, setup_commands
|
||||
from rhymes import ensure_log_file, load_rhymes, setup_rhymes_commands
|
||||
|
||||
ensure_log_file()
|
||||
|
||||
load_riddles()
|
||||
|
||||
setup_commands(client, tree)
|
||||
setup_rhymes_commands(client, tree)
|
||||
|
||||
success, msg = load_rhymes()
|
||||
print(msg)
|
||||
|
||||
async for guild in client.fetch_guilds():
|
||||
tree.copy_global_to(guild=guild)
|
||||
await tree.sync(guild=guild)
|
||||
|
||||
print(f"Logged in as {client.user} on {len(client.guilds)} servers!")
|
||||
|
||||
|
||||
@client.event
|
||||
async def on_message(message):
|
||||
# don't answer to self
|
||||
if (
|
||||
message.author == client.user and message.channel in client.ongoing_riddles
|
||||
): # need to move a part of that block in FourasModule
|
||||
current_riddle = client.ongoing_riddles[message.channel]
|
||||
if "message" not in current_riddle:
|
||||
current_riddle["message"] = message
|
||||
global client
|
||||
if message.author == client.user:
|
||||
return
|
||||
|
||||
if isinstance(
|
||||
message.channel, (discord.DMChannel, discord.TextChannel, discord.Thread)
|
||||
):
|
||||
handled = False
|
||||
for m in client.modules:
|
||||
if not handled:
|
||||
handled = await m.handle_message(message)
|
||||
from fouras import handle_message as handle_fouras
|
||||
|
||||
# Initialise le client
|
||||
await handle_fouras(message, client)
|
||||
|
||||
from rhymes import handle_message as handle_rhymes
|
||||
|
||||
await handle_rhymes(message, client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
token = os.getenv("DISCORD_TOKEN")
|
||||
if not token:
|
||||
print("Error: DISCORD_TOKEN not found in environment variables")
|
||||
exit(1)
|
||||
client.run(token)
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
from .fouras import FourasModule
|
||||
from .rhymes import RhymesModule
|
||||
|
||||
ALL = [FourasModule, RhymesModule]
|
||||
@ -1,44 +0,0 @@
|
||||
import discord
|
||||
import json
|
||||
|
||||
ENCODING = "utf-8"
|
||||
|
||||
|
||||
class BaseModule:
|
||||
_client = None
|
||||
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
|
||||
async def load(self):
|
||||
raise NotImplementedError
|
||||
|
||||
async def save(self, save_to_file=True):
|
||||
raise NotImplementedError
|
||||
|
||||
async def handle_message(self, message) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
async def load_history(self, channel):
|
||||
messages = [
|
||||
{
|
||||
"id": message.id,
|
||||
"content": message.content,
|
||||
"date": message.created_at.strftime("%d/%m %H:%M:%S"),
|
||||
}
|
||||
async for message in channel.history(limit=10)
|
||||
]
|
||||
return json.dumps(messages, ensure_ascii=False)
|
||||
|
||||
async def get_guild_name(self, guildId) -> str:
|
||||
guild = await self._client.fetch_guild(guildId)
|
||||
return "[Server={0}]".format(guild.name)
|
||||
|
||||
async def get_channel_name(self, channel) -> str:
|
||||
if isinstance(channel, discord.DMChannel):
|
||||
dm_channel = await self._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
|
||||
)
|
||||
@ -1,295 +0,0 @@
|
||||
from .base import BaseModule, ENCODING
|
||||
import random
|
||||
import re
|
||||
import json
|
||||
from unidecode import unidecode
|
||||
import appdirs
|
||||
import os
|
||||
|
||||
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 = "riddles.txt"
|
||||
ANSWERS_FILE = "answers.txt"
|
||||
SAVE_FILE = appdirs.user_data_dir() + "/PereFouras/fouras_riddles.json"
|
||||
|
||||
|
||||
class FourasModule(BaseModule):
|
||||
async def load(self):
|
||||
with open(RIDDLES_FILE, "r", encoding=ENCODING) as r_file:
|
||||
self._client.riddles = r_file.read().split("\n\n")
|
||||
with open(ANSWERS_FILE, "r", encoding=ENCODING) as a_file:
|
||||
self._client.answers = [line.strip() for line in a_file.readlines()]
|
||||
str = f"Loaded {len(self._client.riddles)} riddles"
|
||||
|
||||
try:
|
||||
with open(SAVE_FILE, "r") as file:
|
||||
config = json.load(file)
|
||||
ongoing_riddles = dict()
|
||||
for k, v in config.items():
|
||||
channel = await self._client.fetch_channel(int(k))
|
||||
channel_info = v
|
||||
channel_info["message"] = await channel.fetch_message(
|
||||
channel_info["message"]
|
||||
)
|
||||
ongoing_riddles[channel] = channel_info
|
||||
self._client.ongoing_riddles = ongoing_riddles
|
||||
str = str + 'Loaded fouras save file "{0}"'.format(SAVE_FILE)
|
||||
except FileNotFoundError:
|
||||
str = str + 'No previous "{0}" save file found'.format(SAVE_FILE)
|
||||
except json.JSONDecodeError:
|
||||
str = str + '"{0}" is an invalid JSON file.'.format(SAVE_FILE)
|
||||
|
||||
print(str)
|
||||
return str
|
||||
|
||||
async def save(self, save_to_file=True):
|
||||
dump = {}
|
||||
for key, value in self._client.ongoing_riddles.items():
|
||||
dump_channel = dict(value)
|
||||
dump_channel["message"] = dump_channel["message"].id
|
||||
dump[key.id] = dump_channel
|
||||
os.makedirs(os.path.dirname(SAVE_FILE), exist_ok=True)
|
||||
with open(SAVE_FILE, "w") as file:
|
||||
json.dump(dump, file, ensure_ascii=False)
|
||||
print('Saved fouras riddles state in file "{0}"'.format(SAVE_FILE))
|
||||
return dump
|
||||
|
||||
def new_riddle(self, channel, index):
|
||||
current_riddle = dict(
|
||||
index=index,
|
||||
nbClues=-1,
|
||||
riddle=self._client.riddles[index].strip(),
|
||||
answer=self._client.answers[index],
|
||||
)
|
||||
self._client.ongoing_riddles[channel] = current_riddle
|
||||
return self.format_message(current_riddle)
|
||||
|
||||
def finish_riddle(self, channel):
|
||||
del self._client.ongoing_riddles[channel]
|
||||
|
||||
def clue_string(self, answer, nbClues):
|
||||
finalString = "_"
|
||||
for i in range(len(answer) - 1):
|
||||
finalString += " _"
|
||||
random.seed(hash(answer))
|
||||
nbRevealed = 0
|
||||
for i in range(nbClues):
|
||||
id = random.randint(0, len(answer) - 1)
|
||||
while finalString[id * 2] != "_":
|
||||
id = random.randint(0, len(answer) - 1)
|
||||
|
||||
nbRevealed += 1
|
||||
finalString = finalString[: id * 2] + answer[id] + finalString[id * 2 + 1 :]
|
||||
|
||||
if nbRevealed == len(answer):
|
||||
return finalString
|
||||
return finalString
|
||||
|
||||
def format_message(self, current_riddle):
|
||||
nbClues = current_riddle["nbClues"]
|
||||
answer = current_riddle["answer"]
|
||||
|
||||
formatted_riddle = "> " + current_riddle["riddle"].replace("\n", "\n> ")
|
||||
formatted_riddle = formatted_riddle.replace("\r", "")
|
||||
|
||||
clue = ""
|
||||
if nbClues > -1:
|
||||
if nbClues >= len(answer):
|
||||
clue = clue + "\nNon trouvée, la solution était : `{0}`".format(answer)
|
||||
else:
|
||||
clue = clue + "\nIndice : `{0}`".format(
|
||||
self.clue_string(answer, nbClues)
|
||||
)
|
||||
|
||||
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 handle_message(self, message) -> bool:
|
||||
if message.author == self._client.user:
|
||||
return False
|
||||
message_content = message.content.lower()
|
||||
|
||||
# command fouras
|
||||
fouras_match = re.match(r"^fouras\s+(\d+)$", message_content)
|
||||
if fouras_match:
|
||||
index = int(fouras_match.group(1)) - 1
|
||||
if index >= 0 and index < len(self._client.riddles):
|
||||
if random.random() <= 0.03:
|
||||
await message.channel.send("Non")
|
||||
else:
|
||||
await message.channel.send(self.new_riddle(message.channel, index))
|
||||
else:
|
||||
await message.channel.send(
|
||||
INVALID_ID.format(len=len(self._client.riddles))
|
||||
)
|
||||
return True
|
||||
if message_content == "fouras":
|
||||
if random.random() <= 0.03:
|
||||
await message.channel.send("Non")
|
||||
elif len(self._client.riddles) > 0:
|
||||
index = random.randint(0, len(self._client.riddles) - 1)
|
||||
await message.channel.send(self.new_riddle(message.channel, index))
|
||||
return True
|
||||
|
||||
if message_content.startswith("bug"):
|
||||
author_user = await self._client.fetch_user(MAINTAINER_ID)
|
||||
channel_name = await self.get_channel_name(message.channel)
|
||||
messages_json = await self.load_history(message.channel)
|
||||
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=self.save(False),
|
||||
)
|
||||
)
|
||||
await message.channel.send(
|
||||
f"Rapport de bug envoyé à {author_user.mention}\nMerci de ton feedback !"
|
||||
)
|
||||
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 self._client.fetch_channel(index)
|
||||
if channel:
|
||||
await channel.send(broadcast_message)
|
||||
else:
|
||||
await message.channel.send(f"Invalid channel id : {index}")
|
||||
return True
|
||||
|
||||
if message_content == "about fouras":
|
||||
author_user = await self._client.fetch_user(MAINTAINER_ID)
|
||||
await message.channel.send(
|
||||
ABOUT.format(user=author_user.mention, url=API_URL)
|
||||
)
|
||||
return True
|
||||
|
||||
if message_content == "save fouras":
|
||||
if message.author.id == 151626081458192384:
|
||||
json_str = "```json\n{0}```".format(
|
||||
json.dumps(self.save(), ensure_ascii=False, indent=2)
|
||||
)
|
||||
await message.author.send(json_str)
|
||||
return True
|
||||
|
||||
if message_content == "load fouras":
|
||||
if message.author.id == 151626081458192384:
|
||||
await self.load()
|
||||
await message.author.send(
|
||||
"Loaded {0} riddles".format(len(self._client.riddles))
|
||||
)
|
||||
return True
|
||||
|
||||
if message_content == "debug fouras":
|
||||
if message.author.id == 151626081458192384:
|
||||
dump = {}
|
||||
for key, value in self._client.ongoing_riddles.items():
|
||||
dump_channel = dict(value)
|
||||
dump_channel.pop("message", None)
|
||||
dump_channel.pop("answer", None)
|
||||
channel_name = await self.get_channel_name(key)
|
||||
dump[channel_name] = dump_channel
|
||||
await message.author.send(
|
||||
"```json\n{0}```".format(
|
||||
json.dumps(dump, ensure_ascii=False, indent=4)
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
# if current channel has ongoing riddle
|
||||
if message.channel in self._client.ongoing_riddles:
|
||||
current_riddle = self._client.ongoing_riddles[message.channel]
|
||||
|
||||
if "message" in current_riddle:
|
||||
answer = current_riddle["answer"]
|
||||
if unidecode(answer.lower()) in unidecode(message_content):
|
||||
current_riddle["solver"] = message.author
|
||||
|
||||
await message.channel.send(
|
||||
SUCCESS.format(user=message.author.mention, answer=answer)
|
||||
)
|
||||
await current_riddle["message"].edit(
|
||||
content=self.format_message(current_riddle)
|
||||
)
|
||||
self.finish_riddle(message.channel)
|
||||
return True
|
||||
|
||||
if (
|
||||
message_content == "repete"
|
||||
or message_content == "répète"
|
||||
or message_content == "repeat"
|
||||
):
|
||||
current_riddle.pop("message")
|
||||
await message.channel.send(self.format_message(current_riddle))
|
||||
return True
|
||||
|
||||
# Commande /clue : révèle une lettre au hasard de la réponse attendue
|
||||
if (
|
||||
message_content == "indice"
|
||||
or message_content == "aide"
|
||||
or message_content == "help"
|
||||
or message_content == "clue"
|
||||
):
|
||||
nbClues = current_riddle["nbClues"] + 1
|
||||
current_riddle["nbClues"] = nbClues
|
||||
if nbClues >= len(answer):
|
||||
reply = "Perdu ! La réponse était : `{0}`".format(answer)
|
||||
await message.channel.send(reply)
|
||||
# else:
|
||||
# reply = "Indice : `{0}`".format(clue_string(answer, nbClues))
|
||||
await current_riddle["message"].edit(
|
||||
content=self.format_message(current_riddle)
|
||||
)
|
||||
if nbClues >= len(answer):
|
||||
self.finish_riddle(message.channel)
|
||||
return True
|
||||
return False
|
||||
@ -1,69 +0,0 @@
|
||||
import os
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
|
||||
import httpx
|
||||
from client import client, tree
|
||||
|
||||
|
||||
GUILD_ID = os.getenv("GUILD_ID")
|
||||
GITEA_API_KEY = os.getenv("GITEA_API_KEY")
|
||||
|
||||
gitea_url = "https://git.epicsparrow.com/api/v1"
|
||||
|
||||
GITEA_PROJECTS = {}
|
||||
|
||||
auth_headers = {"Authorization": f"token {GITEA_API_KEY}"}
|
||||
|
||||
|
||||
def init_gitea_projects():
|
||||
res = httpx.get(gitea_url + "/repos/search", headers=auth_headers)
|
||||
if res.status_code == 200:
|
||||
GITEA_PROJECTS.update(
|
||||
{
|
||||
str(project["id"]): {
|
||||
"name": project["name"],
|
||||
"owner": project["owner"]["login"],
|
||||
}
|
||||
for project in res.json()["data"]
|
||||
}
|
||||
)
|
||||
return [(project["name"], project["id"]) for project in res.json()["data"]]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
init_gitea_projects()
|
||||
|
||||
|
||||
@tree.command(
|
||||
name="gitea-issue",
|
||||
description="Create issues to gitea",
|
||||
)
|
||||
@app_commands.describe(
|
||||
title="Issue title", project="The project where the issue is created"
|
||||
)
|
||||
@app_commands.choices(
|
||||
project=[
|
||||
app_commands.Choice(name=project["name"], value=id_)
|
||||
for id_, project in GITEA_PROJECTS.items()
|
||||
]
|
||||
)
|
||||
async def gitea(interaction: discord.Interaction, project: str, title: str):
|
||||
embed = discord.Embed(title="Gitea issue")
|
||||
embed.add_field(name="Project", value=GITEA_PROJECTS[project]["name"])
|
||||
embed.add_field(name="Title", value=title)
|
||||
embed.add_field(name="Created by", value=interaction.user.mention)
|
||||
|
||||
creation_url = f"{gitea_url}/repos/{GITEA_PROJECTS[project]['owner']}/{GITEA_PROJECTS[project]['name']}/issues"
|
||||
creation_data = {
|
||||
"title": title,
|
||||
"body": f"Created by {interaction.user.nick or interaction.user.name} from Discord.",
|
||||
}
|
||||
res = httpx.post(creation_url, headers=auth_headers, data=creation_data)
|
||||
if res.status_code == 201:
|
||||
embed.add_field(name="Issue created", value=res.json()["html_url"])
|
||||
else:
|
||||
embed.add_field(name="Error", value=res.text)
|
||||
await interaction.response.send_message(embed=embed)
|
||||
@ -1,136 +0,0 @@
|
||||
from .base import BaseModule
|
||||
import random
|
||||
import time
|
||||
import json
|
||||
import appdirs
|
||||
import os
|
||||
|
||||
RHYMES_FILE = "rhymes.json"
|
||||
SAVE_FILE = appdirs.user_data_dir() + "/PereFouras/poilau_save.json"
|
||||
|
||||
# CONFIG_TEXT = """
|
||||
# Ce bot a été développé par {user}
|
||||
# Code Source : https://git.epicsparrow.com/Anselme/perefouras
|
||||
# Ajouter ce bot à votre serveur : {url}
|
||||
# """
|
||||
|
||||
|
||||
class RhymesModule(BaseModule):
|
||||
rhymes: list = []
|
||||
guild_config: dict = {}
|
||||
|
||||
async def load(self):
|
||||
str = ""
|
||||
with open(RHYMES_FILE, "r") as f:
|
||||
self.rhymes = json.load(f)
|
||||
|
||||
try:
|
||||
with open(SAVE_FILE, "r") as file:
|
||||
self.guild_config = json.load(file)
|
||||
str = 'Loaded poilau save file "{0}"'.format(SAVE_FILE)
|
||||
except FileNotFoundError:
|
||||
str = 'No previous "{0}" save file found'.format(SAVE_FILE)
|
||||
except json.JSONDecodeError:
|
||||
str = '"{0}" is an invalid JSON file.'.format(SAVE_FILE)
|
||||
print(str)
|
||||
return str
|
||||
|
||||
async def save(self, save_to_file=True):
|
||||
os.makedirs(os.path.dirname(SAVE_FILE), exist_ok=True)
|
||||
with open(SAVE_FILE, "w") as file:
|
||||
json.dump(self.guild_config, file, ensure_ascii=False, indent=2)
|
||||
print('Saved poilau state in file "{0}"'.format(SAVE_FILE))
|
||||
|
||||
def get_last_word(self, ch: str) -> str:
|
||||
truncated = ch
|
||||
while True:
|
||||
if len(truncated) < 2 or truncated[-1].isnumeric():
|
||||
return ""
|
||||
if truncated[-1].isalpha() and truncated[-2].isalpha():
|
||||
break
|
||||
truncated = truncated[:-1]
|
||||
truncated = truncated.split(" ")[-1]
|
||||
if truncated.isalpha():
|
||||
return truncated
|
||||
else:
|
||||
return ""
|
||||
|
||||
def poil_auquel(self, ch: str) -> str:
|
||||
for rhyme in self.rhymes:
|
||||
if ch in rhyme["blacklist"]:
|
||||
return ""
|
||||
if ch.endswith(tuple(rhyme["keys"])):
|
||||
return random.choice(rhyme["rhymes"])
|
||||
return ""
|
||||
|
||||
async def handle_message(self, message) -> bool:
|
||||
message_content = message.content.lower()
|
||||
|
||||
if message_content == "debug poilau":
|
||||
if message.author.id == 151626081458192384:
|
||||
dump = {}
|
||||
for key, value in self.guild_config.items():
|
||||
channel_name = await self.get_guild_name(key)
|
||||
sleeping_time = "{:.2f} s".format(
|
||||
max(0, value["cooldown"] - time.time())
|
||||
)
|
||||
dump[channel_name] = {
|
||||
"cooldown": sleeping_time,
|
||||
"self-control": value["self-control"],
|
||||
}
|
||||
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:
|
||||
self.save()
|
||||
json_str = "```json\n{0}```".format(
|
||||
json.dumps(self.guild_config, ensure_ascii=False, indent=2)
|
||||
)
|
||||
await message.author.send(json_str)
|
||||
return True
|
||||
|
||||
if message_content == "load poilau":
|
||||
if message.author.id == 151626081458192384:
|
||||
await message.author.send(await self.load())
|
||||
json_str = "```json\n{0}```".format(
|
||||
json.dumps(self.guild_config, ensure_ascii=False, indent=2)
|
||||
)
|
||||
await message.author.send(json_str)
|
||||
return True
|
||||
|
||||
if message_content == "tg fouras" and message.guild:
|
||||
self.guild_config[str(message.guild.id)] = {
|
||||
"cooldown": time.time() + 40000,
|
||||
"self-control": 2.0,
|
||||
}
|
||||
await message.channel.send("ok :'(")
|
||||
return True
|
||||
|
||||
last_word = self.get_last_word(message_content)
|
||||
if message.author != self._client.user and message.guild and last_word:
|
||||
poil = self.poil_auquel(last_word)
|
||||
guildId = str(message.guild.id)
|
||||
guild_config = self.guild_config.get(
|
||||
guildId, {"cooldown": 0, "self-control": 1.0}
|
||||
)
|
||||
if poil and time.time() - guild_config["cooldown"] > 0:
|
||||
self_control = guild_config["self-control"]
|
||||
if random.random() < self_control:
|
||||
guild_config["self-control"] = self_control * 0.9
|
||||
self.guild_config[guildId] = guild_config
|
||||
return False
|
||||
wait_time = random.randint(0, 900)
|
||||
if bool(random.getrandbits(1)):
|
||||
wait_time = random.randint(900, 10800)
|
||||
self.guild_config[guildId] = {
|
||||
"cooldown": time.time() + wait_time,
|
||||
"self-control": self_control + 1.0,
|
||||
}
|
||||
await message.channel.send(poil)
|
||||
return True
|
||||
return False
|
||||
@ -258,7 +258,7 @@ marguerite
|
||||
orange
|
||||
amande
|
||||
couleurs
|
||||
montages
|
||||
montagnes
|
||||
ficelle
|
||||
raie
|
||||
cape
|
||||
@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"sound": "E",
|
||||
"blacklist": ["cheveux", "cheveu", "queue", "queues"],
|
||||
"blacklist": ["cheveux", "cheveu", "queue", "queues", "eu"],
|
||||
"keys": [
|
||||
"eu",
|
||||
"eus",
|
||||
@ -27,7 +27,7 @@
|
||||
},
|
||||
{
|
||||
"sound": "ETTE",
|
||||
"blacklist": ["quéquette", "quéquettes"],
|
||||
"blacklist": ["quéquette", "quéquettes", "zigounette", "zigounettes", "zézette", "zézettes"],
|
||||
"keys": [
|
||||
"ette",
|
||||
"ett",
|
||||
@ -42,7 +42,9 @@
|
||||
"aites"
|
||||
],
|
||||
"rhymes": [
|
||||
"Poil à la quéquette."
|
||||
"Poil à la quéquette.",
|
||||
"Poil à la zigounette.",
|
||||
"Poil à la zézette."
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -73,7 +75,7 @@
|
||||
},
|
||||
{
|
||||
"sound": "OU",
|
||||
"blacklist": ["cou", "genou", "genous"],
|
||||
"blacklist": ["cou", "genou", "genous", "minou", "minous"],
|
||||
"keys": [
|
||||
"ou",
|
||||
"où",
|
||||
@ -81,12 +83,13 @@
|
||||
],
|
||||
"rhymes": [
|
||||
"Poil au cou.",
|
||||
"Poil au genou."
|
||||
"Poil au genou.",
|
||||
"Poil au minou."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "O",
|
||||
"blacklist": ["dos"],
|
||||
"blacklist": ["dos", "bot"],
|
||||
"keys": [
|
||||
"au",
|
||||
"aux",
|
||||
@ -101,6 +104,17 @@
|
||||
"Poil au dos."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "OTTE",
|
||||
"blacklist": ["glotte"],
|
||||
"keys": [
|
||||
"otte",
|
||||
"ott"
|
||||
],
|
||||
"rhymes": [
|
||||
"Poil à la glotte."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "OUL",
|
||||
"blacklist": ["moule", "moules", "boule", "boules", "alcool"],
|
||||
@ -186,7 +200,7 @@
|
||||
},
|
||||
{
|
||||
"sound": "AI",
|
||||
"blacklist": ["raie", "raies"],
|
||||
"blacklist": ["raie", "raies", "wait"],
|
||||
"keys": [
|
||||
"ai",
|
||||
"ait",
|
||||
@ -336,7 +350,7 @@
|
||||
},
|
||||
{
|
||||
"sound": "IS",
|
||||
"blacklist": ["pénis", "penis"],
|
||||
"blacklist": ["pénis", "penis", "jolis"],
|
||||
"keys": [
|
||||
"is",
|
||||
"isse",
|
||||
@ -346,6 +360,20 @@
|
||||
"Poil au pénis."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "I",
|
||||
"blacklist": ["zizi", "zizis"],
|
||||
"keys": [
|
||||
"i",
|
||||
"ea",
|
||||
"ee",
|
||||
"it",
|
||||
"ie"
|
||||
],
|
||||
"rhymes": [
|
||||
"Poil au zizi."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "A",
|
||||
"blacklist": ["bras"],
|
||||
@ -359,19 +387,6 @@
|
||||
"Poil au bras."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "I",
|
||||
"blacklist": ["zizi", "zizis"],
|
||||
"keys": [
|
||||
"i",
|
||||
"ee",
|
||||
"it",
|
||||
"ie"
|
||||
],
|
||||
"rhymes": [
|
||||
"Poil au zizi."
|
||||
]
|
||||
},
|
||||
{
|
||||
"sound": "U",
|
||||
"blacklist": ["cul", "culs"],
|
||||
@ -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
|
||||
@ -1166,23 +1166,23 @@ Secret, il l'est pour ceux qui aiment se taire.
|
||||
|
||||
Souvent causé par la distraction,
|
||||
Cette maladresse peut avoir des répercutions.
|
||||
Cette perche sert a manœuvrée,
|
||||
Un bateau ou a sortir un poisson de l'eau.
|
||||
Cette perche sert à manœuvrer un bateau,
|
||||
Ou à sortir un poisson de l'eau.
|
||||
|
||||
Gage d'amitié,
|
||||
Sa valeur est sans importance.
|
||||
Coutume de civilité,
|
||||
Il n'est que convenance.
|
||||
|
||||
Pour la police, ils sont un clé
|
||||
Pour la police, ils sont une clé
|
||||
Lorsqu'ils se font tirer le portrait.
|
||||
Ils sont nombreux dans les cuisines
|
||||
Et ne lésinent pas dans les usines.
|
||||
|
||||
Du fond de la Provence,
|
||||
Sous le mistral elle danse,
|
||||
De son cœur, née l'essence
|
||||
A une couleur elle a donnée naissance.
|
||||
De son cœur, nait l'essence
|
||||
À une couleur elle a donné naissance.
|
||||
|
||||
On s'appuie très souvent dessus,
|
||||
Il a régulièrement des frais
|
||||
@ -1196,7 +1196,7 @@ Elle devient un commandement.
|
||||
|
||||
Il peut s'agir d'un soupçon
|
||||
Car c'est une petite quantité
|
||||
On aime cette peau si parfumé
|
||||
On aime cette peau si parfumée
|
||||
Provenant de l'orange ou du citron
|
||||
|
||||
Au balcon vous l'apercevez,
|
||||
@ -1214,7 +1214,7 @@ Ce sobriquet est peut flatteur
|
||||
Il hiberne au fond d'un terrier
|
||||
Et le savon il fait mousser.
|
||||
|
||||
Autrefois elle acheminée bien du courrier
|
||||
Autrefois elle acheminait bien du courrier
|
||||
Elle est souvent synonyme de voyages
|
||||
Parfois au fond d'un grenier
|
||||
S'y entasse des souvenirs sans âge.
|
||||
@ -1364,7 +1364,7 @@ N'est pas celle blindée
|
||||
C'est une opération,
|
||||
Qui aide à mieux régner
|
||||
|
||||
Parfois mauvaise et redouté
|
||||
Parfois mauvaise et redoutée
|
||||
Elle peut faire des blessés
|
||||
Qu'elle soit d'eau ou de reins
|
||||
Elle est aussi mot de la fin
|
||||
@ -1407,7 +1407,7 @@ Les paresseux le trouvent tellement pratique.
|
||||
C'est le synonyme de "gros",
|
||||
Par qui on jauge les bateaux.
|
||||
Mais si vous êtes dans l'auto,
|
||||
Moins vous en faites et mieux çà vaut.
|
||||
Moins vous en faites et mieux ça vaut.
|
||||
|
||||
Qu'il soit décoré ou peint,
|
||||
Chaque noble a le sien.
|
||||
132
rhymes.py
Normal file
132
rhymes.py
Normal file
@ -0,0 +1,132 @@
|
||||
# rhymes.py
|
||||
import json
|
||||
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
|
||||
|
||||
RHYMES_FILE = "resources/rhymes.json"
|
||||
RHYME_LOG_FILE = "data/rhyme_log.csv"
|
||||
|
||||
loaded_rhymes = {}
|
||||
|
||||
|
||||
def ensure_log_file() -> None:
|
||||
Path(RHYME_LOG_FILE).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not Path(RHYME_LOG_FILE).exists():
|
||||
with open(RHYME_LOG_FILE, "w", encoding="utf-8") as f:
|
||||
f.write("timestamp,last_word,rhyme_triggered\n")
|
||||
|
||||
|
||||
def load_rhymes() -> Tuple[bool, str]:
|
||||
global loaded_rhymes
|
||||
try:
|
||||
with open(RHYMES_FILE, "r", encoding="utf-8") as f:
|
||||
loaded_rhymes = json.load(f)
|
||||
return True, f'Loaded rhymes file "{RHYMES_FILE}"'
|
||||
except FileNotFoundError:
|
||||
return False, f'No rhymes file found at "{RHYMES_FILE}"'
|
||||
except json.JSONDecodeError as e:
|
||||
return False, f'Invalid JSON in "{RHYMES_FILE}": {e}'
|
||||
|
||||
|
||||
def log_rhyme(last_word: str, rhyme_triggered: str) -> None:
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
with open(RHYME_LOG_FILE, "a", encoding="utf-8") as f:
|
||||
safe_rhyme = rhyme_triggered.replace(",", ";")
|
||||
f.write(f"{timestamp},{last_word},{safe_rhyme}\n")
|
||||
|
||||
|
||||
def get_last_word(text: str) -> str:
|
||||
truncated = text
|
||||
while True:
|
||||
if len(truncated) < 2 or truncated[-1].isnumeric():
|
||||
return ""
|
||||
if truncated[-1].isalpha() and truncated[-2].isalpha():
|
||||
break
|
||||
truncated = truncated[:-1]
|
||||
truncated = truncated.split(" ")[-1]
|
||||
return truncated if truncated.isalpha() else ""
|
||||
|
||||
|
||||
def find_rhyme(word: str) -> str:
|
||||
global loaded_rhymes
|
||||
|
||||
for rhyme in loaded_rhymes:
|
||||
if word in rhyme["blacklist"]:
|
||||
return ""
|
||||
if word.endswith(tuple(rhyme["keys"])):
|
||||
log_rhyme(word, rhyme["sound"])
|
||||
return random.choice(rhyme["rhymes"])
|
||||
return ""
|
||||
|
||||
|
||||
async def get_guild_name(guildId, client) -> str:
|
||||
guild = await client.fetch_guild(guildId)
|
||||
return "[Server={0}]".format(guild.name)
|
||||
|
||||
|
||||
async def handle_rhyme_logic(message, client) -> bool:
|
||||
message_content = message.content.lower()
|
||||
last_word = get_last_word(message_content)
|
||||
|
||||
if message.author != client.user and message.guild and last_word:
|
||||
rhyme = find_rhyme(last_word)
|
||||
guild_id = str(message.guild.id)
|
||||
|
||||
if rhyme:
|
||||
guild_state = database.get_guild_state(guild_id)
|
||||
cooldown_ratio = guild_state["cooldown_ratio"]
|
||||
|
||||
if cooldown_ratio >= 0:
|
||||
cooldown_dt = datetime.fromisoformat(guild_state["cooldown_until"])
|
||||
now_dt = datetime.now()
|
||||
|
||||
if now_dt >= cooldown_dt:
|
||||
self_control = guild_state["self_control"]
|
||||
|
||||
if random.random() < self_control:
|
||||
database.rhyme_damage_self_control(guild_id)
|
||||
return False
|
||||
|
||||
database.rhyme_reset_cooldown(guild_id, cooldown_ratio)
|
||||
|
||||
await message.channel.send(rhyme)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def handle_message(message, client) -> bool:
|
||||
database.ensure_db()
|
||||
ensure_log_file()
|
||||
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user