diff --git a/data/discord.example.toml b/data/discord.example.toml index 7d98d07..ea1030e 100644 --- a/data/discord.example.toml +++ b/data/discord.example.toml @@ -15,6 +15,7 @@ id = 1234567891234567890 channel_admin_id = 1234567891234567890 invite_link = "https://discord.gg/" etu_sync = true +anonymous_channels = [1234567891234567890, 1234567891234567890] [guild.special_roles] # id of the main roles that doesn't change diff --git a/etuutt_bot/commands/anon_msg.py b/etuutt_bot/commands/anon_msg.py new file mode 100644 index 0000000..890b1e3 --- /dev/null +++ b/etuutt_bot/commands/anon_msg.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING + +from discord import Interaction, TextChannel, app_commands +from discord.ext import commands + +from etuutt_bot.utils.data_write import data_write + +if TYPE_CHECKING: + from etuutt_bot.bot import EtuUTTBot + + +class AnonMsgCog(commands.GroupCog, group_name="anon"): + """Commandes pour envoyer des messages anonymes.""" + + def __init__(self, bot: EtuUTTBot): + self.bot = bot + + @app_commands.command(name="send") + @app_commands.rename(channel="salon") + async def send(self, interaction: Interaction[EtuUTTBot], channel: TextChannel, message: str): + """Envoie un message anonyme dans le salon demandé s'il le supporte.""" + if channel.id not in self.bot.settings.guild.anonymous_channels: + await interaction.response.send_message( + "Le salon ne supporte pas les messages anonymes" + ) + return + msg = await channel.send(f"[ANONYME] {message}") + await data_write( + f"[{datetime.now().isoformat(' ', 'seconds')}]" + f" {interaction.user.name} alias {interaction.user.display_name}" + f" a envoyé le message {msg.id} sur le salon #{channel.name}.\n", + Path("data", "logs", "anon.log"), + ) + await interaction.response.send_message( + f"Votre message a bien été envoyé : {msg.jump_url}", ephemeral=True + ) + + @app_commands.command(name="liste") + async def list(self, interaction: Interaction[EtuUTTBot]): + """Envoie la liste des salons dans lesquels un message anonyme peut être envoyé.""" + if not self.bot.settings.guild.anonymous_channels: + await interaction.response.send_message( + "Il n'y a pas de salon configuré pour envoyer un message anonyme." + ) + msg = "Les salons dans lesquels un message anonyme peut être envoyé sont :" + for c in self.bot.settings.guild.anonymous_channels: + msg += f"\n- {interaction.guild.get_channel(c).mention}" + await interaction.response.send_message(msg) diff --git a/etuutt_bot/commands_list.py b/etuutt_bot/commands_list.py index bcf29de..e99b4ba 100644 --- a/etuutt_bot/commands_list.py +++ b/etuutt_bot/commands_list.py @@ -5,6 +5,7 @@ import discord from etuutt_bot.commands.admin import AdminCog +from etuutt_bot.commands.anon_msg import AnonMsgCog from etuutt_bot.commands.misc import MiscCog from etuutt_bot.commands.role import RoleCog from etuutt_bot.commands.sync import SyncCog @@ -23,7 +24,7 @@ async def commands_list(bot: EtuUTTBot): await bot.add_cog(cog) # Les cogs contenant les commandes réservées à la guilde gérée - guild_cogs: tuple = (RoleCog(bot), SyncCog(bot)) + guild_cogs: tuple = (AnonMsgCog(bot), RoleCog(bot), SyncCog(bot)) for cog in guild_cogs: await bot.add_cog(cog, guild=bot.watched_guild) diff --git a/etuutt_bot/config.py b/etuutt_bot/config.py index aa7a4a6..308f797 100644 --- a/etuutt_bot/config.py +++ b/etuutt_bot/config.py @@ -65,6 +65,7 @@ class GuildConfig(BaseModel): special_roles: SpecialRolesConfig invite_link: Annotated[HttpUrl, UrlConstraints(default_host="discord.gg")] etu_sync: bool + anonymous_channels: list[ChannelId] class CategoryConfig(BaseModel): diff --git a/etuutt_bot/utils/data_write.py b/etuutt_bot/utils/data_write.py new file mode 100644 index 0000000..ccf9f6e --- /dev/null +++ b/etuutt_bot/utils/data_write.py @@ -0,0 +1,25 @@ +from asyncio import Lock, to_thread +from pathlib import Path + +FILE_LOCKS = {} + + +def _get_lock(file: Path) -> Lock: + """Retourne le verrou associé à un chemin et le crée s'il n'existe pas.""" + for path, lock in FILE_LOCKS.items(): + if path == file: + return lock + FILE_LOCKS.update({file: Lock()}) + return FILE_LOCKS.get(file) + + +def _file_write(data: str, file: Path) -> None: + """Ajoute une chaîne de caractères à la fin d'un fichier.""" + with open(file, "a") as f: + f.write(data) + + +async def data_write(data: str, file: Path) -> None: + """Dans un autre thread, écrit une chaîne de caractères dans un fichier.""" + async with _get_lock(file): + await to_thread(_file_write, data, file)