Compare commits

...

2 Commits

Author SHA1 Message Date
f15b12af26 Ruff pass for code cleanup
All checks were successful
Build and publish Docker image / docker (push) Successful in 14m40s
2026-10-01 15:43:05 +02:00
5f61a45383 Added CI/CD task to automatically build and deploy the docker image 2026-10-01 15:36:24 +02:00
7 changed files with 163 additions and 95 deletions

View File

@ -0,0 +1,33 @@
name: Build and publish Docker image
on:
push:
branches:
- master
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive registry host and image name
id: registry
run: |
host="$(printf '%s' "${{ gitea.server_url }}" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://##; s#/.*$##')"
image="$(printf '%s' "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')"
echo "host=${host}" >> "$GITHUB_OUTPUT"
echo "image=${image}" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ steps.registry.outputs.host }}
username: ${{ gitea.repository_owner }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push image
run: |
docker build -t "${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.image }}:latest" .
docker push "${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.image }}:latest"

View File

@ -4,29 +4,22 @@ FROM python:3.9-slim
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app
# Install git and clean up # Install Python dependencies
RUN apt-get update -q && \ COPY requirements.txt .
apt-get install -y git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create data directory with proper permissions
RUN mkdir -p /app/data && chmod 755 /app/data
# Set safe directory
RUN git config --global --add safe.directory '*'
# Clone the repository
RUN git init /app && git remote add origin https://git.epicsparrow.com/Anselme/perefouras.git && git fetch --depth 1 origin master && git reset --hard FETCH_HEAD
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# give exec permissions to main # Copy application code
RUN chmod +x main.py COPY main.py fouras.py rhymes.py database.py ./
COPY resources/ ./resources/
# Create data directory for persistent state
RUN mkdir -p /app/data && chmod 755 /app/data
# Ensure Python output is not buffered (useful for logs)
ENV PYTHONUNBUFFERED=1
# Set data directory as volume mount point # Set data directory as volume mount point
VOLUME /app/data VOLUME /app/data
# Command to update the repo and run the script # Run the bot
CMD ["sh", "-c", "cd /app && git fetch --depth 1 origin master && git reset --hard FETCH_HEAD && python main.py"] CMD ["python", "main.py"]

View File

@ -3,7 +3,7 @@ import sqlite3
import random 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
DB_FILE = "data/database.db" DB_FILE = "data/database.db"
@ -52,6 +52,7 @@ SET nb_clues = nb_clues + 1
DELETE_RIDDLE_QUERY = "DELETE FROM active_riddles WHERE channel_id = ?" DELETE_RIDDLE_QUERY = "DELETE FROM active_riddles WHERE channel_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."""
Path(DB_FILE).parent.mkdir(parents=True, exist_ok=True) Path(DB_FILE).parent.mkdir(parents=True, exist_ok=True)
@ -113,7 +114,13 @@ def get_guild_state(guild_id: str) -> Dict[str, Any]:
def rhyme_configure_cooldown_ratio(guild_id: str, cooldown_ratio: float) -> bool: def rhyme_configure_cooldown_ratio(guild_id: str, cooldown_ratio: float) -> bool:
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(CONFIGURE_COOLDOWN_RATIO_QUERY, (cooldown_ratio, guild_id,)) cursor.execute(
CONFIGURE_COOLDOWN_RATIO_QUERY,
(
cooldown_ratio,
guild_id,
),
)
conn.commit() conn.commit()
@ -124,25 +131,36 @@ def rhyme_damage_self_control(guild_id: str) -> None:
conn.commit() conn.commit()
def rhyme_reset_cooldown( def rhyme_reset_cooldown(guild_id: str, ratio: float) -> None:
guild_id: str,
ratio: float
) -> None:
with get_connection() as conn: with get_connection() as conn:
# Calculate new cooldown duration # Calculate new cooldown duration
wait_time = random.randint(900, 10800) if bool(random.getrandbits(1)) else random.randint(0, 900) 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) wait_time = 0 if ratio < 0 else int(wait_time * ratio)
cooldown_until = datetime.now().replace(second=0, microsecond=0) + timedelta(seconds=wait_time) cooldown_until = datetime.now().replace(second=0, microsecond=0) + timedelta(
seconds=wait_time
)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(RESET_COOLDOWN_QUERY, (cooldown_until.isoformat(), guild_id,)) cursor.execute(
RESET_COOLDOWN_QUERY,
(
cooldown_until.isoformat(),
guild_id,
),
)
conn.commit() conn.commit()
def get_active_riddle(channel_id: str) -> Any: def get_active_riddle(channel_id: str) -> Any:
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() 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() row = cursor.fetchone()
if row: if row:
@ -167,14 +185,20 @@ def save_active_riddle(
) -> None: ) -> None:
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(SAVE_RIDDLE_QUERY, (channel_id, riddle_index, nb_clues, message_id, solver_id)) cursor.execute(
SAVE_RIDDLE_QUERY,
(channel_id, riddle_index, nb_clues, message_id, solver_id),
)
conn.commit() conn.commit()
def increment_clue(channel_id: str) -> None: def increment_clue(channel_id: str) -> None:
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(INCREMENT_CLUE_QUERY, (channel_id,),) cursor.execute(
INCREMENT_CLUE_QUERY,
(channel_id,),
)
conn.commit() conn.commit()

View File

@ -1,8 +1,6 @@
services: services:
perefouras: perefouras:
build: image: git.epicsparrow.com/anselme/perefouras:latest
context: .
dockerfile: Dockerfile
environment: environment:
- DISCORD_TOKEN=${FOURAS_DISCORD_TOKEN} - DISCORD_TOKEN=${FOURAS_DISCORD_TOKEN}
- MAINTAINER_ID=${FOURAS_MAINTAINER_ID:-151626081458192384} - MAINTAINER_ID=${FOURAS_MAINTAINER_ID:-151626081458192384}

View File

@ -166,7 +166,7 @@ def get_current_riddle(channel_id):
"nbClues": riddle_data["nb_clues"], "nbClues": riddle_data["nb_clues"],
"riddle": riddles[riddle_data["riddle_index"]].strip(), "riddle": riddles[riddle_data["riddle_index"]].strip(),
"answer": answers[riddle_data["riddle_index"]], "answer": answers[riddle_data["riddle_index"]],
"data": riddle_data "data": riddle_data,
} }
else: else:
return None return None
@ -174,7 +174,7 @@ def get_current_riddle(channel_id):
async def handle_riddle_solving(message, client: discord.Client) -> bool: async def handle_riddle_solving(message, client: discord.Client) -> bool:
current_riddle = get_current_riddle(message.channel.id) current_riddle = get_current_riddle(message.channel.id)
if(not current_riddle): if not current_riddle:
return False return False
answer = current_riddle["answer"] answer = current_riddle["answer"]
riddle_data = current_riddle["data"] riddle_data = current_riddle["data"]
@ -190,9 +190,7 @@ async def handle_riddle_solving(message, client: discord.Client) -> bool:
solver_id=solver_id, solver_id=solver_id,
) )
await message.reply( await message.reply(SUCCESS.format(user=message.author.mention, answer=answer))
SUCCESS.format(user=message.author.mention, answer=answer)
)
try: try:
original_msg = await message.channel.fetch_message( original_msg = await message.channel.fetch_message(
@ -278,7 +276,7 @@ async def new_riddle(interaction: Interaction, index: int) -> None:
riddle_index=index, riddle_index=index,
nb_clues=-1, nb_clues=-1,
message_id=msg.message_id, message_id=msg.message_id,
solver_id=None solver_id=None,
) )
@ -292,23 +290,20 @@ async def update_riddle_message(channel, current_riddle):
def setup_commands(client: discord.Client, tree) -> None: def setup_commands(client: discord.Client, tree) -> None:
@tree.command( @tree.command(name="fouras", description="Demander une énigme au Père Fouras")
name="fouras",
description="Demander une énigme au Père Fouras"
)
async def fouras( async def fouras(
interaction: discord.Interaction, interaction: discord.Interaction,
): ):
if len(riddles) == 0: if len(riddles) == 0:
await interaction.response.send_message("Désolé, mais j'ai oublié mes énigmes", ephemeral=True) await interaction.response.send_message(
"Désolé, mais j'ai oublié mes énigmes", ephemeral=True
)
else: else:
index = random.randint(0, len(riddles) - 1) index = random.randint(0, len(riddles) - 1)
await new_riddle(interaction, index) await new_riddle(interaction, index)
@tree.command( @tree.command(
name="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 requete( async def requete(
@ -317,21 +312,24 @@ def setup_commands(client: discord.Client, tree) -> None:
): ):
index = index - 1 index = index - 1
if not (0 <= index < len(riddles)): if not (0 <= index < len(riddles)):
await interaction.response.send_message(INVALID_ID.format(len=len(riddles)+1), ephemeral=True) await interaction.response.send_message(
INVALID_ID.format(len=len(riddles) + 1), ephemeral=True
)
return return
await new_riddle(interaction, index) await new_riddle(interaction, index)
@tree.command( @tree.command(
name="repete", name="repete",
description="Demander au Père Fouras de répéter l'énigme en cours" description="Demander au Père Fouras de répéter l'énigme en cours",
) )
async def repete( async def repete(
interaction: discord.Interaction, interaction: discord.Interaction,
): ):
current_riddle = get_current_riddle(interaction.channel.id) current_riddle = get_current_riddle(interaction.channel.id)
if(not current_riddle): if not current_riddle:
await interaction.response.send_message("Aucune énigme en cours", ephemeral=True) await interaction.response.send_message(
"Aucune énigme en cours", ephemeral=True
)
return return
riddle_data = current_riddle["data"] riddle_data = current_riddle["data"]
msg = await interaction.response.send_message( msg = await interaction.response.send_message(
@ -345,17 +343,17 @@ def setup_commands(client: discord.Client, tree) -> None:
solver_id=riddle_data["solver_id"], solver_id=riddle_data["solver_id"],
) )
@tree.command( @tree.command(
name="indice", name="indice", description="Demander un indice pour l'énigme en cours"
description="Demander un indice pour l'énigme en cours"
) )
async def indice( async def indice(
interaction: discord.Interaction, interaction: discord.Interaction,
): ):
current_riddle = get_current_riddle(interaction.channel.id) current_riddle = get_current_riddle(interaction.channel.id)
if(not current_riddle): if not current_riddle:
await interaction.response.send_message("Aucune énigme en cours", ephemeral=True) await interaction.response.send_message(
"Aucune énigme en cours", ephemeral=True
)
return return
answer = current_riddle["answer"] answer = current_riddle["answer"]
riddle_data = current_riddle["data"] riddle_data = current_riddle["data"]
@ -366,7 +364,7 @@ def setup_commands(client: discord.Client, tree) -> None:
await interaction.response.send_message( await interaction.response.send_message(
"Perdu ! La réponse était : `{0}`".format(answer) "Perdu ! La réponse était : `{0}`".format(answer)
) )
if(not await update_riddle_message(interaction.channel, current_riddle)): if not await update_riddle_message(interaction.channel, current_riddle):
return return
await finish_riddle(interaction.channel, client) await finish_riddle(interaction.channel, client)
else: else:
@ -377,6 +375,8 @@ def setup_commands(client: discord.Client, tree) -> None:
message_id=riddle_data["message_id"], message_id=riddle_data["message_id"],
solver_id=riddle_data["solver_id"], solver_id=riddle_data["solver_id"],
) )
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))
)

View File

@ -76,7 +76,12 @@ async def handle_rhyme_logic(message, client) -> bool:
message_content = message.content.lower() message_content = message.content.lower()
last_word = get_last_word(message_content) last_word = get_last_word(message_content)
if not message.author.bot and message.author.id != client.user.id and message.guild and last_word: if (
not message.author.bot
and message.author.id != client.user.id
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)
@ -110,23 +115,27 @@ async def handle_message(message, client: discord.Client) -> bool:
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: def setup_rhymes_commands(
client: discord.Client, tree: discord.app_commands.CommandTree
) -> None:
@tree.command( @tree.command(
name="config", name="config",
description="Permet de configurer à quel point le Père Fouras est relou" description="Permet de configurer à quel point le Père Fouras est relou",
) )
@app_commands.default_permissions(manage_guild=True) @app_commands.default_permissions(manage_guild=True)
@app_commands.checks.has_permissions(manage_guild=True) @app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(ratio="ratio de cooldown (0 = )") @app_commands.describe(ratio="ratio de cooldown (0 = )")
async def config( async def config(interaction: discord.Interaction, ratio: float):
interaction: discord.Interaction,
ratio: float
):
guild_id: str = str(interaction.guild_id) guild_id: str = str(interaction.guild_id)
guild_state = database.get_guild_state(guild_id) guild_state = database.get_guild_state(guild_id)
database.rhyme_configure_cooldown_ratio(guild_id, ratio) 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}" msg = f"Le ratio de cooldown passe de {guild_state['cooldown_ratio']:.2f} à {ratio:.2f} pour le serveur {interaction.guild.name}"
if ratio < 0: if ratio < 0:
await interaction.response.send_message(f"{msg}\nLes \"poil au\" sont désormais désactivés.", ephemeral=True) await interaction.response.send_message(
f'{msg}\nLes "poil au" sont désormais désactivés.', ephemeral=True
)
else: 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) await interaction.response.send_message(
f'{msg}\nLes "poil au" ont désormais un cooldown entre 0 et {int(180 * ratio)} minutes.',
ephemeral=True,
)

View File

@ -1,47 +1,58 @@
```python
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json import json
riddles = [] riddles = []
def scrape_year_data(scrap_year): def scrape_year_data(scrap_year):
base_url = 'https://www.fan-fortboyard.fr/pages/fanzone/enigmes-du-pere-fouras/' base_url = "https://www.fan-fortboyard.fr/pages/fanzone/enigmes-du-pere-fouras/"
if(scrap_year > 2019): if scrap_year > 2019:
url = f'{base_url}enigmes-{scrap_year}.html' # Replace with the actual URL pattern url = f"{base_url}enigmes-{scrap_year}.html" # Replace with the actual URL pattern
else: else:
url = f'{base_url}{scrap_year}.html' # Replace with the actual URL pattern url = f"{base_url}{scrap_year}.html" # Replace with the actual URL pattern
response = requests.get(url) response = requests.get(url)
if response.status_code == 200: if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser') soup = BeautifulSoup(response.text, "html.parser")
paragraphs = soup.find_all('p') paragraphs = soup.find_all("p")
for paragraph in paragraphs: for paragraph in paragraphs:
span = paragraph.find('span') span = paragraph.find("span")
if span and span.find('strong'): if span and span.find("strong"):
strong_tag = span.find('strong') strong_tag = span.find("strong")
if strong_tag and strong_tag.text.strip().isdigit(): if strong_tag and strong_tag.text.strip().isdigit():
riddle_count = strong_tag.text.strip() riddle_count = strong_tag.text.strip()
answer_span = paragraph.find_all('span', style=True) answer_span = paragraph.find_all("span", style=True)
if answer_span: if answer_span:
answer_text = answer_span[-1].get_text(strip=True) answer_text = answer_span[-1].get_text(strip=True)
riddle_count = strong_tag.text.strip() riddle_count = strong_tag.text.strip()
riddle_text = paragraph.decode_contents() riddle_text = paragraph.decode_contents()
riddle_text = riddle_text.replace('<br/>', '\n').replace('<br>', '\n') riddle_text = riddle_text.replace("<br/>", "\n").replace(
riddle_text = BeautifulSoup(riddle_text, 'html.parser').get_text(strip=True) "<br>", "\n"
riddle_text = riddle_text.split(')', 1)[-1].strip() )
riddle_text = BeautifulSoup(
riddle_text, "html.parser"
).get_text(strip=True)
riddle_text = riddle_text.split(")", 1)[-1].strip()
if "RÉPONSE" in riddle_text: if "RÉPONSE" in riddle_text:
riddle_text = riddle_text.split("RÉPONSE", 1)[0].strip() riddle_text = riddle_text.split("RÉPONSE", 1)[0].strip()
riddles.append({'riddle': riddle_text, 'answer': answer_text, 'count': riddle_count, 'year': scrap_year}) riddles.append(
{
"riddle": riddle_text,
"answer": answer_text,
"count": riddle_count,
"year": scrap_year,
}
)
else: else:
print(f'No answer found : {scrap_year}-{riddle_count}') print(f"No answer found : {scrap_year}-{riddle_count}")
else: else:
print(f'Failed to retrieve data for year {year}: {response.status_code}') print(f"Failed to retrieve data for year {year}: {response.status_code}")
for year in range(2007, 2023): for year in range(2007, 2023):
scrape_year_data(year) scrape_year_data(year)
with open('riddles.json', 'w', encoding='utf-8') as json_file: with open("riddles.json", "w", encoding="utf-8") as json_file:
json.dump(riddles, json_file, indent=4, ensure_ascii=False) json.dump(riddles, json_file, indent=4, ensure_ascii=False)
```