diff --git a/requirements.txt b/requirements.txt index 9fabead..5273904 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ psycopg2-binary==2.9.3 python-dotenv==0.20.0 PyYAML==6.0 SQLAlchemy==1.4.37 +requests==2.30.0 \ No newline at end of file diff --git a/vesta/__init__.py b/vesta/__init__.py index b2c5340..18915aa 100644 --- a/vesta/__init__.py +++ b/vesta/__init__.py @@ -7,7 +7,7 @@ from .lang import Lang from yaml import load, Loader with open("vesta/data/lang.yml") as file: - lang = Lang(load(file.read(), Loader), session_maker) + lang_file = Lang(load(file.read(), Loader), session_maker) from .client import Vesta diff --git a/vesta/client.py b/vesta/client.py index 7ab391b..cf8e1cf 100644 --- a/vesta/client.py +++ b/vesta/client.py @@ -6,6 +6,7 @@ from discord import app_commands from . import session_maker +from .services import clash_of_code_helper from .tables import CustomCommand, select logger = logging.getLogger(__name__) @@ -27,6 +28,8 @@ def __init__(self, *, intents: discord.Intents): async def on_ready(self): logger.info(f"Logged on as {self.user}") + clash_of_code_helper.resume_update_loops() + for com in self.tree.get_commands(): logger.debug(f"Globals {com} name : {com.name}") @@ -56,13 +59,21 @@ async def command(interaction: discord.Interaction): if active: await self.tree.sync(guild=guild) - async def on_member_join(self, member): + async def on_member_join(self, member: discord.Member): logger.debug(f"Member joined : {member}!") + if not (await member.guild.fetch_member(self.user.id)).guild_permissions.manage_nicknames: + logger.debug(f"Bot doesn't have manage nicknames permission on {member.guild}") + return + if not re.match(regex_name, member.display_name): await member.edit(nick=f"{random.choice(names).capitalize()}{random.choice(adjectives).capitalize()}") - async def on_member_update(self, before, after): + async def on_member_update(self, before: discord.Member, after: discord.Member): logger.debug(f"Member update : {after}!") + if not (await after.guild.fetch_member(self.user.id)).guild_permissions.manage_nicknames: + logger.debug(f"Bot doesn't have manage nicknames permission on {after.guild}") + return + if not after.guild_permissions.manage_nicknames and not re.match(regex_name, after.display_name): await after.edit(nick=f"{random.choice(names).capitalize()}{random.choice(adjectives).capitalize()}") diff --git a/vesta/commands/__init__.py b/vesta/commands/__init__.py index d64d136..3974c4f 100644 --- a/vesta/commands/__init__.py +++ b/vesta/commands/__init__.py @@ -2,3 +2,4 @@ from . import presentation from . import custom from . import config +from . import clash_of_code \ No newline at end of file diff --git a/vesta/commands/clash_of_code.py b/vesta/commands/clash_of_code.py new file mode 100644 index 0000000..b1b9c90 --- /dev/null +++ b/vesta/commands/clash_of_code.py @@ -0,0 +1,128 @@ +import logging +import re +from typing import Tuple + +import discord +from discord import app_commands +from sqlalchemy import select + +from .. import vesta_client, session_maker, lang_file +from ..exceptions import CommandRuntimeError +from ..services import clash_of_code_helper, State +from ..services.clash_of_code_helper import start_update_loop +from ..tables import ClashOfCodeGuildGame +from ..tables import Guild + +logger = logging.getLogger(__name__) +session = session_maker() + +regex_clash_of_code_game = r"^(https://|)(www.|)codingame.com/clashofcode/clash/[^/]+(/|)$" + + +@vesta_client.tree.command(name="clash-of-code", description="Invites users with the \"Clash of Code\" role to play") +@app_commands.describe(link="The link to the Clash of Code game") +async def clash_of_code(interaction: discord.Interaction, link: str): + if not re.match(regex_clash_of_code_game, link): + await _send_error(interaction, "coc_invalid_link") + return + + try: + (game, game_id) = _get_game(link) + guild_game = _get_guild_game(interaction) + (guild, role, channel) = _get_guild(interaction) + except CommandRuntimeError as e: + await _send_error(interaction, e.message) + return + + view = discord.ui.View() + view.add_item(discord.ui.Button( + label=lang_file.get("coc_game_join", interaction.guild), + url=game.link, + emoji="🎮" + )) + + embed = game.embed(interaction.guild) + embed.set_author(name=interaction.user.name, icon_url=interaction.user.avatar.url) + + announcement_message = await channel.send( + content=f"{role.mention} {lang_file.get('coc_game_invite', interaction.guild)}", + embed=embed, + view=view + ) + + guild_game.last_clash_id = game_id + guild_game.announcement_message_id = announcement_message.id + session.commit() + + await interaction.response.send_message( + lang_file.get("coc_successfully_invited", interaction.guild), + ephemeral=True + ) + + start_update_loop(message=announcement_message, guild=interaction.guild) + + +def _get_game(link: str): + game_id = clash_of_code_helper.game_id_from_link(link) + game = clash_of_code_helper.fetch(game_id) + + if not game: + raise CommandRuntimeError("coc_invalid_link") + if game.state != State.PENDING: + raise CommandRuntimeError("coc_game_already_started") + + return game, game_id + + +def _get_guild_game(interaction: discord.Interaction) -> ClashOfCodeGuildGame: + r = select(ClashOfCodeGuildGame).where(ClashOfCodeGuildGame.guild_id == interaction.guild.id) + guild_game: ClashOfCodeGuildGame = session.scalar(r) + + if guild_game and not guild_game.can_start_new(): + raise CommandRuntimeError("coc_already_in_progress") + + if not guild_game: + logger.debug(f"Creating new guild game for guild {interaction.guild_id}") + guild_game = ClashOfCodeGuildGame(guild_id=interaction.guild_id) + session.add(guild_game) + + return guild_game + + +def _get_guild(interaction: discord.Interaction) -> Tuple[Guild, discord.Role, discord.TextChannel]: + r = select(Guild).where(Guild.id == interaction.guild_id) + guild: Guild = session.scalar(r) + + if not guild: + logger.debug(f"Add guild {interaction.guild_id} to the database") + guild = Guild(id=interaction.guild_id, name=interaction.guild.name) + session.add(guild) + + return guild, _get_role(guild, interaction), _get_channel(guild, interaction) + + +def _get_role(guild: Guild, interaction: discord.Interaction) -> discord.Role: + if not guild.coc_role: + raise CommandRuntimeError("coc_role_not_set") + + role = interaction.guild.get_role(guild.coc_role) + if not role: + raise CommandRuntimeError("coc_role_not_found") + + return role + + +def _get_channel(guild: Guild, interaction: discord.Interaction) -> discord.TextChannel: + if not guild.coc_channel: + raise CommandRuntimeError("coc_channel_not_set") + + channel = interaction.guild.get_channel(guild.coc_channel) + if not channel: + raise CommandRuntimeError("coc_channel_not_found") + + return channel + + +async def _send_error(interaction: discord.Interaction, key: str): + msg = lang_file.get(key, interaction.guild) + await interaction.response.send_message(f"❌ {msg}", ephemeral=True) diff --git a/vesta/commands/config.py b/vesta/commands/config.py index 5816ac8..5d11965 100644 --- a/vesta/commands/config.py +++ b/vesta/commands/config.py @@ -1,9 +1,11 @@ +import typing + import discord from discord import app_commands import logging import traceback -from .. import vesta_client, session_maker, lang +from .. import vesta_client, session_maker, lang_file from ..tables import Guild, select logger = logging.getLogger(__name__) @@ -17,14 +19,14 @@ async def on_error(self, interaction: discord.Interaction, error): logger.debug(f"Error {error} raised") if isinstance(error, app_commands.errors.MissingPermissions): await interaction.response.send_message( - lang.get("permissions_error", interaction.guild), ephemeral=True) + lang_file.get("permissions_error", interaction.guild), ephemeral=True) elif isinstance(error, app_commands.errors.BotMissingPermissions): await interaction.response.send_message( - lang.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", + lang_file.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", ephemeral=True) else: logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) config_manager = ConfigManager() @@ -35,24 +37,11 @@ async def on_error(self, interaction: discord.Interaction, error): async def review(interaction: discord.Interaction, channel: discord.TextChannel): logger.debug(f"Command /config review {channel} used") - r = select(Guild).where(Guild.id == interaction.guild_id) - guild = session.scalar(r) - if not guild: - logger.debug(f"Add guild {interaction.guild_id} to the database") - guild = Guild(id=interaction.guild_id, name=interaction.guild.name) - session.add(guild) + def update(g): + g.review_channel = channel.id + await update_config_element(interaction, update) - guild.review_channel = channel.id - - try: - session.commit() - except: - session.rollback() - - logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) - - await interaction.response.send_message(lang.get("config_review", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("config_review", interaction.guild), ephemeral=True) @config_manager.command(description="Set Projets Channel") @@ -60,33 +49,51 @@ async def review(interaction: discord.Interaction, channel: discord.TextChannel) async def projects(interaction: discord.Interaction, channel: discord.TextChannel): logger.debug(f"Command /config projects {channel} used") - r = select(Guild).where(Guild.id == interaction.guild_id) - guild = session.scalar(r) - if not guild: - logger.debug(f"Add guild {interaction.guild_id} to the database") - guild = Guild(id=interaction.guild_id, name=interaction.guild.name) - session.add(guild) + def update(g): + g.projects_channel = channel.id + await update_config_element(interaction, update) - guild.projects_channel = channel.id + await interaction.response.send_message(lang_file.get("config_projects", interaction.guild), ephemeral=True) - try: - session.commit() - except: - session.rollback() - logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) +@config_manager.command(name="coc-role", description="Set clash of code ping role") +@app_commands.rename(coc_role="role") +@app_commands.describe(coc_role="The role to ping when a game of Clash of Code is launched") +async def change_clash_of_code_role(interaction: discord.Interaction, coc_role: discord.Role): + logger.debug(f"Command /config coc-role {coc_role.name} used") + + def update(g): + g.coc_role = coc_role.id + await update_config_element(interaction, update) + + await interaction.response.send_message(lang_file.get("config_coc_role", interaction.guild), ephemeral=True) + +@config_manager.command(name="coc-channel", description="Set clash of code channel") +@app_commands.rename(coc_channel="channel") +@app_commands.describe(coc_channel="The channel to send clash of code messages") +async def change_clash_of_code_channel(interaction: discord.Interaction, coc_channel: discord.TextChannel): + logger.debug(f"Command /config coc-channel {coc_channel.name} used") - await interaction.response.send_message(lang.get("config_projects", interaction.guild), ephemeral=True) + def update(g): + g.coc_channel = coc_channel.id + await update_config_element(interaction, update) + await interaction.response.send_message(lang_file.get("config_coc_channel", interaction.guild), ephemeral=True) @config_manager.command(name="lang", description="Set Guild Lang") @app_commands.rename(guild_lang='lang') @app_commands.describe(guild_lang="The lang for the bot") -@app_commands.choices(guild_lang=[app_commands.Choice(name=l, value=l) for l in lang.data]) +@app_commands.choices(guild_lang=[app_commands.Choice(name=l, value=l) for l in lang_file.data]) async def change_lang(interaction: discord.Interaction, guild_lang: app_commands.Choice[str]): logger.debug(f"Command /config lang {guild_lang.value} used") + def update(g): + g.lang = guild_lang.value + await update_config_element(interaction, update) + + await interaction.response.send_message(lang_file.get("config_lang", interaction.guild), ephemeral=True) + +async def update_config_element(interaction: discord.Interaction, updater: typing.Callable[[Guild], None]): r = select(Guild).where(Guild.id == interaction.guild_id) guild = session.scalar(r) if not guild: @@ -94,7 +101,7 @@ async def change_lang(interaction: discord.Interaction, guild_lang: app_commands guild = Guild(id=interaction.guild_id, name=interaction.guild.name) session.add(guild) - guild.lang = guild_lang.value + updater(guild) try: session.commit() @@ -102,9 +109,7 @@ async def change_lang(interaction: discord.Interaction, guild_lang: app_commands session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) - - await interaction.response.send_message(lang.get("config_lang", interaction.guild), ephemeral=True) - + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) + pass vesta_client.tree.add_command(config_manager) diff --git a/vesta/commands/custom.py b/vesta/commands/custom.py index 9e5bad7..586ae30 100644 --- a/vesta/commands/custom.py +++ b/vesta/commands/custom.py @@ -4,7 +4,7 @@ import traceback import re -from .. import vesta_client, session_maker, lang +from .. import vesta_client, session_maker, lang_file from ..modals import CustomSlashForm, CustomMenuForm from ..tables import CustomCommand, select @@ -21,14 +21,14 @@ async def on_error(self, interaction: discord.Interaction, error): logger.debug(f"Error {error} raised") if isinstance(error, app_commands.errors.MissingPermissions): await interaction.response.send_message( - lang.get("permissions_error", interaction.guild), ephemeral=True) + lang_file.get("permissions_error", interaction.guild), ephemeral=True) elif isinstance(error, app_commands.errors.BotMissingPermissions): await interaction.response.send_message( - lang.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", + lang_file.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", ephemeral=True) else: logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) custom_manager = CustomManager() @@ -39,22 +39,22 @@ async def on_error(self, interaction: discord.Interaction, error): async def add(interaction: discord.Interaction, keyword: str): keyword = keyword.lower() if not re.match(custom_regex, keyword): - return await interaction.response.send_message(lang.get("invalid_keyword", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("invalid_keyword", interaction.guild), ephemeral=True) logger.debug(f"Command /custom add {keyword} used") if len(keyword) > 32: - return await interaction.response.send_message(lang.get("too_long_keyword", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("too_long_keyword", interaction.guild), ephemeral=True) r = select(CustomCommand).where(CustomCommand.guild_id == interaction.guild_id) r = r.where(CustomCommand.keyword == keyword) command = session.scalar(r) if command: - return await interaction.response.send_message(lang.get("command_already_exist", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("command_already_exist", interaction.guild), ephemeral=True) number = session.query(CustomCommand).where(CustomCommand.guild_id == interaction.guild_id).count() if number > 39: - return await interaction.response.send_message(lang.get("too_much_commands", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("too_much_commands", interaction.guild), ephemeral=True) await interaction.response.send_modal(CustomSlashForm(keyword=keyword, interaction=interaction)) @@ -70,7 +70,7 @@ async def remove(interaction: discord.Interaction, keyword: str): command = session.scalar(r) if not command: - return await interaction.response.send_message(lang.get("command_not_exist", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("command_not_exist", interaction.guild), ephemeral=True) vesta_client.tree.remove_command(keyword, guild=interaction.guild) await vesta_client.tree.sync() session.delete(command) @@ -81,17 +81,17 @@ async def remove(interaction: discord.Interaction, keyword: str): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) - await interaction.response.send_message(lang.get("command_deleted", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("command_deleted", interaction.guild), ephemeral=True) await vesta_client.tree.sync(guild=interaction.guild) @custom_manager.command(name="list", description="List custom commands") async def custom_list(interaction: discord.Interaction): logger.debug(f"Command /custom list used") - list_embed = discord.Embed(title=lang.get("list_commands", interaction.guild)) - list_embed2 = discord.Embed(title=lang.get("list_commands2", interaction.guild)) + list_embed = discord.Embed(title=lang_file.get("list_commands", interaction.guild)) + list_embed2 = discord.Embed(title=lang_file.get("list_commands2", interaction.guild)) r = select(CustomCommand).where(CustomCommand.guild_id == interaction.guild_id) commands = session.scalars(r) @@ -119,7 +119,7 @@ async def create_custom(interaction: discord.Interaction, message: discord.Messa number = session.query(CustomCommand).where(CustomCommand.guild_id == interaction.guild_id).count() if number > 39: - return await interaction.response.send_message(lang.get("too_much_commands", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("too_much_commands", interaction.guild), ephemeral=True) await interaction.response.send_modal(CustomMenuForm(content=message.content, author=message.author, interaction=interaction)) diff --git a/vesta/commands/nickname.py b/vesta/commands/nickname.py index 539d53d..1fd7648 100644 --- a/vesta/commands/nickname.py +++ b/vesta/commands/nickname.py @@ -6,7 +6,7 @@ import discord from discord import app_commands -from .. import vesta_client, session_maker, lang +from .. import vesta_client, session_maker, lang_file from ..tables import select, Ban logger = logging.getLogger(__name__) @@ -26,20 +26,20 @@ async def nickname(interaction: discord.Interaction, name: str): response = session.scalar(r) if response and response.nickname_banned: return await interaction.response.send_message( - lang.get("nickname_banned", interaction.guild), + lang_file.get("nickname_banned", interaction.guild), ephemeral=True) if not interaction.user.guild_permissions.manage_nicknames and not re.match(regex_name, name): - response_embed = discord.Embed(color=int("FF4444", 16), title=lang.get("nickname_incorrect_title", interaction.guild), - description=lang.get("nickname_incorrect_description", interaction.guild) + f"`{regex_name}`") + response_embed = discord.Embed(color=int("FF4444", 16), title=lang_file.get("nickname_incorrect_title", interaction.guild), + description=lang_file.get("nickname_incorrect_description", interaction.guild) + f"`{regex_name}`") return await interaction.response.send_message(embed=response_embed, ephemeral=True) if len(name) > 32: - return await interaction.response.send_message(lang.get("nick_too_long", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("nick_too_long", interaction.guild), ephemeral=True) await interaction.user.edit(nick=name) await interaction.response.send_message( - content=lang.get("nickname_changed", interaction.guild), ephemeral=True) + content=lang_file.get("nickname_changed", interaction.guild), ephemeral=True) @nickname.error @@ -47,11 +47,11 @@ async def nickname_error(interaction: discord.Interaction, error): logger.debug(f"Error {error} raised") if isinstance(error, app_commands.errors.BotMissingPermissions): await interaction.response.send_message( - lang.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", + lang_file.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", ephemeral=True) else: logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) @app_commands.guild_only() @@ -62,14 +62,14 @@ async def on_error(self, interaction: discord.Interaction, error): logger.debug(f"Error {error} raised") if isinstance(error, app_commands.errors.MissingPermissions): await interaction.response.send_message( - lang.get("permissions_error", interaction.guild), ephemeral=True) + lang_file.get("permissions_error", interaction.guild), ephemeral=True) elif isinstance(error, app_commands.errors.BotMissingPermissions): await interaction.response.send_message( - lang.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", + lang_file.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", ephemeral=True) else: logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) nick_manage = NickManage() @@ -96,10 +96,10 @@ async def ban(interaction: discord.Interaction, user: discord.Member): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) await interaction.response.send_message( - content=f"{user} " + lang.get("nickname_ban", interaction.guild)) + content=f"{user} " + lang_file.get("nickname_ban", interaction.guild)) @nick_manage.command(description="Unban a user from using the nickname command") @@ -111,7 +111,7 @@ async def unban(interaction: discord.Interaction, user: discord.Member): response = session.scalar(r) if not (response and response.nickname_banned): return await interaction.response.send_message( - content=f"{user} " + lang.get("nickname_not_banned", interaction.guild)) + content=f"{user} " + lang_file.get("nickname_not_banned", interaction.guild)) response.nickname_banned = False try: @@ -120,10 +120,10 @@ async def unban(interaction: discord.Interaction, user: discord.Member): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) await interaction.response.send_message( - content=f"{user} " + lang.get("nickname_unban", interaction.guild)) + content=f"{user} " + lang_file.get("nickname_unban", interaction.guild)) @nick_manage.command(name="list", description="Show the banlist") @@ -139,8 +139,8 @@ async def banlist(interaction: discord.Interaction, page: Optional[int] = 0): for response in responses: ban_list += f"<@{response.user_id}>\n" - banned_embed = discord.Embed(title=lang.get("nickname_list_title", interaction.guild), description=ban_list) - banned_embed.set_footer(text=lang.get("list_page", interaction.guild) + f" {page}") + banned_embed = discord.Embed(title=lang_file.get("nickname_list_title", interaction.guild), description=ban_list) + banned_embed.set_footer(text=lang_file.get("list_page", interaction.guild) + f" {page}") await interaction.response.send_message(embed=banned_embed, allowed_mentions=discord.AllowedMentions().none()) diff --git a/vesta/commands/presentation.py b/vesta/commands/presentation.py index 1406a29..a9d8505 100644 --- a/vesta/commands/presentation.py +++ b/vesta/commands/presentation.py @@ -5,7 +5,7 @@ from typing import Optional -from .. import vesta_client, session_maker, lang +from .. import vesta_client, session_maker, lang_file from ..modals import PresentationForm from ..tables import Presentation, select, or_, Guild, Ban @@ -22,13 +22,13 @@ async def presentation(interaction: discord.Interaction): guild = session.scalar(r) if not guild or not guild.review_channel or not guild.projects_channel: return await interaction.response.send_message( - lang.get("presentations_not_available", interaction.guild), ephemeral=True) + lang_file.get("presentations_not_available", interaction.guild), ephemeral=True) r = select(Ban).where(Ban.user_id == interaction.user.id).where(Ban.guild_id == interaction.guild.id) response = session.scalar(r) if response and response.presentation_banned: return await interaction.response.send_message( - lang.get("presentations_banned", interaction.guild), + lang_file.get("presentations_banned", interaction.guild), ephemeral=True) await interaction.response.send_modal(PresentationForm(interaction)) @@ -41,14 +41,14 @@ async def on_error(self, interaction: discord.Interaction, error): logger.debug(f"Error {error} raised") if isinstance(error, app_commands.errors.MissingPermissions): await interaction.response.send_message( - lang.get("permissions_error", interaction.guild), ephemeral=True) + lang_file.get("permissions_error", interaction.guild), ephemeral=True) elif isinstance(error, app_commands.errors.BotMissingPermissions): await interaction.response.send_message( - lang.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", + lang_file.get("bot_permissions_error", interaction.guild) + f" {', '.join(error.missing_permissions)}", ephemeral=True) else: logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) presentation_manage = PresentationManage() @@ -60,19 +60,19 @@ async def show(interaction: discord.Interaction, research: Optional[str] = None, logger.debug(f"Command /presentationmanage show [research={research},user={user}] used") if not (research or user): return await interaction.response.send_message( - lang.get("minimum_one_parameter", interaction.guild), ephemeral=True) + lang_file.get("minimum_one_parameter", interaction.guild), ephemeral=True) if user: r = select(Presentation).where(Presentation.author_id == user.id) else: if not research.isdecimal() or int(research) > 2 ** 63 - 1: return await interaction.response.send_message( - content=lang.get("invalid_number", interaction.guild), + content=lang_file.get("invalid_number", interaction.guild), ephemeral=True) r = select(Presentation).where(or_(Presentation.id == research, Presentation.author_id == research)) presentations = session.scalars(r).all() if not len(presentations): return await interaction.response.send_message( - content=lang.get("no_result", interaction.guild), + content=lang_file.get("no_result", interaction.guild), ephemeral=True, ) if len(presentations) == 1: @@ -81,7 +81,7 @@ async def show(interaction: discord.Interaction, research: Optional[str] = None, return await interaction.response.send_message(embed=embed) embed = discord.Embed( colour=int('222222', 16), - title=lang.get("user_result_title", interaction.guild) + title=lang_file.get("user_result_title", interaction.guild) ) for presentation in presentations: emoji = '🕑' @@ -112,10 +112,10 @@ async def ban(interaction: discord.Interaction, user: discord.Member): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) await interaction.response.send_message( - content=f"{user} " + lang.get("user_result_title", interaction.guild)) + content=f"{user} " + lang_file.get("user_result_title", interaction.guild)) @presentation_manage.command(description="Unban a user from submitting a presentation") @@ -127,7 +127,7 @@ async def unban(interaction: discord.Interaction, user: discord.Member): response = session.scalar(r) if not (response and response.presentation_banned): return await interaction.response.send_message( - content=f"{user} " + lang.get("presentations_not_banned", interaction.guild)) + content=f"{user} " + lang_file.get("presentations_not_banned", interaction.guild)) response.presentation_banned = False try: @@ -136,10 +136,10 @@ async def unban(interaction: discord.Interaction, user: discord.Member): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) await interaction.response.send_message( - content=f"{user} " + lang.get("presentations_unban", interaction.guild)) + content=f"{user} " + lang_file.get("presentations_unban", interaction.guild)) @presentation_manage.command(name="list", description="Show the banlist") @@ -155,8 +155,8 @@ async def banlist(interaction: discord.Interaction, page: Optional[int] = 0): for result in results: ban_list += f"<@{result.user_id}>\n" - banned_embed = discord.Embed(title=lang.get("presentations_list_title", interaction.guild), description=ban_list) - banned_embed.set_footer(text=lang.get("list_page", interaction.guild) + f" {page}") + banned_embed = discord.Embed(title=lang_file.get("presentations_list_title", interaction.guild), description=ban_list) + banned_embed.set_footer(text=lang_file.get("list_page", interaction.guild) + f" {page}") await interaction.response.send_message(embed=banned_embed, allowed_mentions=discord.AllowedMentions().none()) diff --git a/vesta/data/lang.yml b/vesta/data/lang.yml index 4d10f44..3fb0bfd 100644 --- a/vesta/data/lang.yml +++ b/vesta/data/lang.yml @@ -1,35 +1,39 @@ fr: + general_yes: "🟩 Oui" + general_no: "🟥 Non" invalid_link: Le lien n'est pas valide invalid_image_link: Le lien de votre image n'est pas valide command_created: La commande a bien été créée ! - custom_form: Création d'une commande custom + custom_form: Création d'une commande personnalisée custom_form_keyword: Mot clé de la commande custom_form_title: Titre du message custom_form_content: Contenu du message custom_form_link: Lien custom_form_image: Image custom_form_color: Couleur de l'embed - command_already_exist: La commande existe déjà + command_already_exist: Cette commande existe déjà command_not_exist: Cette commande n'existe pas - command_deleted: La custom command à bien été supprimée + command_deleted: La commande personnalisée a bien été supprimée list_commands: Liste des commandes list_commands2: Partie 2 - too_much_commands: Il y a déjà trop de custom commands + too_much_commands: Il y a déjà trop de commandes personnalisées unexpected_error: Une erreur s'est produite - permissions_error: Vous n'avez pas les permissions pour faire cette commande - config_review: Le salon de review à bien été config - config_projects: Le salon de projets à bien été config - config_lang: La langue à bien été changée - nickname_banned: Vous avez été banni du système de rename - nickname_incorrect_title: ⚠️Pseudo incorrect ! - nickname_incorrect_description: "Ce nom n'est pas valide, merci d'entrer un nom validant la regex suivante : " - nickname_changed: Votre pseudo à été changé avec succès ! - bot_permissions_error: "Malheureusement, le bot n'a pas les permissions suffisantes pour faire cela, merci de demander à vos administrateurs de lui fournir les suivantes :" - nickname_ban: a bien été banni de la commande nickname - nickname_not_banned: n'est pas banni de la commande nickname - nickname_unban: a bien été débanni de la commande nickname - nickname_list_title: Membres bannis du nickname - presentations_not_available: Le systeme de présentation n'a pas été activé sur votre serveur + permissions_error: Vous n'avez pas les permissions pour exécuter cette commande + config_review: Le salon de revue à bien été modifié + config_projects: Le salon de projets à bien été modifié + config_coc_role: Le role à mentionner lors des parties de Clash of Code a bien été configuré + config_coc_channel: Le salon où envoyer les invitations pour les parties de Clash of Code a bien été configuré + config_lang: La langue a bien été modifiée + nickname_banned: Vous avez été banni du système de renommage + nickname_incorrect_title: ⚠️ Pseudo incorrect ! + nickname_incorrect_description: "Ce pseudo n'est pas valide, merci d'entrer un nom respectant la regex suivante : " + nickname_changed: Votre pseudo a bien été modifié ! + bot_permissions_error: "Malheureusement, le bot n'a pas les assez de permissions pour réaliser cette action, merci de demander à vos administrateurs de lui accorder les permissions suivantes :" + nickname_ban: a bien été banni de la commande `/nickname` + nickname_not_banned: n'est pas banni de la commande `/nickname` + nickname_unban: a bien été débanni de la commande `/nickname` + nickname_list_title: Membres bannis du `/nickname` + presentations_not_available: Le système de présentation n'a pas été activé sur votre serveur presentations_banned: Vous avez été banni du système de présentation minimum_one_parameter: Merci de mettre au moins un des paramètres invalid_number: Merci d'entrez un nombre valide @@ -45,7 +49,7 @@ fr: presentation_form_description: Description du projet presentation_form_link: Lien du projet presentation_form_image: Lien de l'image - review_channel_error: Il semblerais y avoir un problème avec le salon de review + review_channel_error: Il semble qu'il y a un problème avec le salon de review presentation_sent: Votre projet sera étudié dans les plus brefs délais denied_form: Raison de refus denied_form_reason: Raison du refus @@ -58,85 +62,164 @@ fr: reason_other: Autre deny: Refuser denied_by: Refusé par - projects_channel_error: Il semblerais y avoir un problème avec le salon de projets + projects_channel_error: Il semble qu'il y a un problème avec le salon de projets accept: Accepter accepted_by: Accepté par - text_not_enough_code: "Nous préférons mettre en avant des projets matures et aboutis, avec une quantité de code suffisante. Votre projet n'est ainsi pas assez gros à notre goût. Certes, cela est très bien de faire des projets, c'est une bonne façon d'apprendre la programmation, mais il faudrait peut-être l'épaissir un peu, ou attendre un plus gros projet, avant de le partager comme ça ^^." - text_not_open_source: "Le but du salon est de promouvoir l'open source, pas de faire de la publicité. Ainsi, nous préférons refuser les projets qui ne donnent pas un lien vers un github, gitlab, ou tout autre site destiné au partage de code." - text_illegal: "Votre projet est non conformes aux règles de ce discord ou aux TOS de discord. Par conséquent, nous ne pouvons pas vous laisser poster ce projet dans le salon." + text_not_enough_code: > + Nous préférons mettre en avant des projets matures et aboutis, avec une quantité de code suffisante. + Votre projet n'est ainsi pas assez gros à notre goût. Certes, cela est très bien de faire des projets, + c'est une bonne façon d'apprendre la programmation, mais il faudrait peut-être l'épaissir un peu, + ou attendre un plus gros projet, avant de le partager comme ça ^^. + text_not_open_source: > + Le but du salon est de promouvoir l'open source, pas de faire de la publicité. + Ainsi, nous préférons refuser les projets qui ne donnent pas un lien vers un github, gitlab, + ou tout autre site destiné au partage de code. + text_illegal: > + Votre projet est non conformes aux règles de ce discord ou aux TOS de discord. + Par conséquent, nous ne pouvons pas vous laisser poster ce projet dans le salon. too_long_keyword: Le mot-clé entré est trop long - invalid_keyword: Le mot clé est invalide + invalid_keyword: Le mot-clé est invalide custom_invalid_args: Merci de fournir des arguments valides thread_project: Discussion sur le projet nick_too_long: Ce nom est trop long + coc_invalid_link: Ce lien ne semble pas lié à une partie de Clash of Code valide ! + coc_game_already_started: La partie de Clash of Code que vous avez fourni est déjà en cours / terminée ! + coc_already_in_progress: Une partie de Clash of Code est déjà en cours sur ce serveur ! + coc_successfully_invited: Vous avez bien invité les autres membres du serveur à rejoindre la partie ! + coc_all_languages: Tous les langages + coc_role_not_set: > + Le rôle à mentionner lors des parties de Clash of Code n'a + pas été configuré sur ce serveur ! Prévenez un administrateur ! + coc_role_not_found: > + Le configuré rôle pour être mentionné lors des parties de Clash of Code + n'a pas été trouvé ! Prévenez un administrateur ! + coc_channel_not_set: > + Le salon où envoyer les invitations pour les parties de Clash of Code n'a + pas été configuré sur ce serveur ! Prévenez un administrateur ! + coc_channel_not_found: > + Le configuré salon où envoyer les invitations pour les parties de Clash of Code + n'a pas été trouvé ! Prévenez un administrateur ! + coc_game_state: "État de la partie :" + coc_game_state_pending: ⏳ En attente + coc_game_state_running: ⚡ En cours + coc_game_state_finished: 🏁 Terminée + coc_game_title: ⭐ Nouvelle partie de Clash of Code ! + coc_game_invite: Une nouvelle partie de Clash of Code a été lancée ! + coc_game_join: Rejoindre la partie + coc_game_modes: "Modes de jeu possibles :" + coc_game_mode: "Mode de jeu :" + coc_game_players: "Joueurs :" + coc_game_winner: "Gagnant :" + coc_game_languages: "Langages autorisés :" + coc_mode_fastest: ⚡ Le plus rapide + coc_mode_shortest: 📏 Le plus court + coc_mode_reverse: 🔄 Inversé en: + general_yes: "🟩 Yes" + general_no: "🟥 No" invalid_link: The link is not valid invalid_image_link: The link of the image is not valid - command_created: Command created successfully + command_created: Command successfully created custom_form: Creation of a custom command - custom_form_keyword: Keyword of the command - custom_form_title: Title of the message - custom_form_content: Content of the message + custom_form_keyword: Command keyword + custom_form_title: Message title + custom_form_content: Message content custom_form_link: Link custom_form_image: Image - custom_form_color: Color of the embed - command_already_exist: This command already exist + custom_form_color: Embed color + command_already_exist: This command already exists command_not_exist: This command does not exist command_deleted: The custom command have been deleted successfully list_commands: List of the custom commands list_commands2: Part 2 - too_much_commands: There is already too much commands - unexpected_error: An unexpected error have been raised - permissions_error: You don't have the permissions to do this command - config_review: Review channel have been configured - config_projects: Projects channel have been configured - config_lang: The lang have been updated + too_much_commands: There are already too much commands + unexpected_error: An unexpected error occurred + permissions_error: You don't have enough permissions to run this command + config_review: Review channel successfully configured + config_projects: Projects channel successfully configured + config_coc_role: The role to mention when a Clash of Code game is started was updated + config_coc_channel: The channel where to send the invitations for the Clash of Code games was updated + config_lang: The lang was updated nickname_banned: You have been banned from the nickname system - nickname_incorrect_title: ⚠️Incorrect Name ! - nickname_incorrect_description: "This name is not valid, please prompt something correct for the regex : " - nickname_changed: Your nickname have been updated - bot_permissions_error: "The bot don't have enough permissions to do this, please ask your administrator to add the following ones :" - nickname_ban: have been banned from the nickname command + nickname_incorrect_title: ⚠️ Incorrect nickname ! + nickname_incorrect_description: "This name is not valid, please input a nickname matching the regex: " + nickname_changed: Your nickname was updated + bot_permissions_error: "The bot isn't powerful enough to perform this action, please ask your administrator to grant the following permissions:" + nickname_ban: was banned from the nickname command nickname_not_banned: is not banned from the nickname command - nickname_unban: have been unbanned from the nickname command + nickname_unban: was unbanned from the nickname command nickname_list_title: Nickname Banned Members presentations_not_available: The presentations system is not active on this server - presentations_banned: You have been banned from the presentations system + presentations_banned: You were banned from the presentations system minimum_one_parameter: Please provide at least one argument - invalid_number: Please enter a valid number - no_result: No result found - user_result_title: Result from the research by user - presentations_ban: have been banned from the presentations system + invalid_number: Please input a valid number + no_result: No results found + user_result_title: Result from the search by user + presentations_ban: was banned from the presentations system presentations_not_banned: is not banned of the presentations system - presentations_unban: have been unbanned from the presentations system + presentations_unban: was unbanned from the presentations system presentations_list_title: Presentations System Banned Members list_page: Page presentation_form: Presentation - presentation_form_title: Name of the project - presentation_form_description: Description of the project - presentation_form_link: Link of the project - presentation_form_image: Link of the image - review_channel_error: It seems like there is a problem with the review channel + presentation_form_title: Project name + presentation_form_description: Project description + presentation_form_link: Project link + presentation_form_image: Image link + review_channel_error: It seems like there was a problem with the review channel presentation_sent: Your project will be reviewed soon - denied_form: Denied reason - denied_form_reason: Denied reason - denied_feedback_title: Your presentation has been refused - denied_feedback_content: "Reason :" + denied_form: Denial reason + denied_form_reason: Denial reason + denied_feedback_title: Your presentation was rejected + denied_feedback_content: "Reason:" denied_registered: Reason registered reason_not_enough_code: Not enough code - reason_not_open_source: Project not open-source + reason_not_open_source: Non open-source project reason_illegal: Illegal Project reason_other: Other deny: Deny denied_by: Denied by - projects_channel_error: It seems like there is a problem with the projects channel + projects_channel_error: It seems like there was a problem with the projects channel accept: Accept accepted_by: Accepted by - text_not_enough_code: "We prefer to share mature and complete projects, with a certain amount of code. Your project is not big enough to our liking. Yes, making projects is a very good way to learn about programming, but you could maybe elaborate a bit more, or wait util you do a bigger project before sharing it." - text_not_open_source: "The goal of the channel is to promote open source, not make advertisement. Therefore we must reject projects that do not contain link to any github, gitlab, or other code sharing website." - text_illegal: "Your project does not follow our rules or Discord TOS. Therefore, we cannot post your project in the channel" + text_not_enough_code: > + We prefer to share mature and complete projects, with a certain amount of code. + Your project was not considered as being big enough. Yes, making projects is a very good way to learn about programming, + but you could maybe elaborate a bit more, or wait util you make a bigger project before sharing it." + text_not_open_source: > + The goal of the channel is promoting open source, not making advertisement. + Therefore we reject projects that do not link to any github, gitlab, or other code sharing website. + text_illegal: > + Your project does not follow our policy or Discord TOS. Therefore, we cannot allow your project in the channel" too_long_keyword: This keyword is too long invalid_keyword: This keyword is not valid - custom_invalid_args: Please provide good arguments - thread_project: Talking about the project - nick_too_long: This nick is too long \ No newline at end of file + custom_invalid_args: Please provide valid arguments + thread_project: Chat about the project + nick_too_long: This nickname is too long + coc_invalid_link: This link does not seem to be a valid Clash of Code game ! + coc_game_already_started: The Clash of Code game you provided is already started / finished ! + coc_already_in_progress: A Clash of Code game is already in progress on this server ! + coc_successfully_invited: You successfully invited the other members of the server to join the game ! + coc_all_languages: All languages + coc_role_not_set: > + The role to mention when a Clash of Code game is started was not configured on this server ! Please warn an administrator ! + coc_role_not_found: > + The configured role to mention when a Clash of Code game is started was not found ! Please warn an administrator ! + coc_channel_not_set: > + The channel where to send the invitations for the Clash of Code games was not configured on this server ! Please warn an administrator ! + coc_channel_not_found: > + The configured channel where to send the invitations for the Clash of Code games was not found ! Please warn an administrator ! + coc_game_state: "Game state:" + coc_game_state_pending: ⏳ Pending + coc_game_state_running: ⚡ Running + coc_game_state_finished: 🏁 Finished + coc_game_title: ⭐ New Clash of Code game ! + coc_game_invite: A new Clash of Code game was started ! + coc_game_join: Join the game + coc_game_modes: "Possible game modes:" + coc_game_mode: "Game mode:" + coc_game_players: "Players:" + coc_game_winner: "Winner:" + coc_game_languages: "Allowed languages:" + coc_mode_fastest: ⚡ Fastest + coc_mode_shortest: 📏 Shortest + coc_mode_reverse: 🔄 Reverse diff --git a/vesta/exceptions/__init__.py b/vesta/exceptions/__init__.py new file mode 100644 index 0000000..0370bb6 --- /dev/null +++ b/vesta/exceptions/__init__.py @@ -0,0 +1 @@ +from .command_exceptions import CommandRuntimeError \ No newline at end of file diff --git a/vesta/exceptions/command_exceptions.py b/vesta/exceptions/command_exceptions.py new file mode 100644 index 0000000..08861b6 --- /dev/null +++ b/vesta/exceptions/command_exceptions.py @@ -0,0 +1,14 @@ +class CommandRuntimeError(Exception): + """ + Exception raised when a command fails to run. + """ + + message: str + def __init__(self, message: str): + """ + Initializes the exception with the given message. + + :param message: The reason why the command failed to run as a language key + """ + self.message = message + super().__init__(message) \ No newline at end of file diff --git a/vesta/lang.py b/vesta/lang.py index 582553a..4c9cc1c 100644 --- a/vesta/lang.py +++ b/vesta/lang.py @@ -1,5 +1,7 @@ import logging +import discord + from .tables import select, Guild logger = logging.getLogger(__name__) @@ -10,7 +12,7 @@ def __init__(self, data, session): self.data = data self.session = session - def get(self, item, guild): + def get(self, item: str, guild: discord.Guild): lang = "en" if guild.preferred_locale[:2] in self.data: @@ -28,4 +30,4 @@ def get(self, item, guild): return self.data[lang][item] logger.error(f"Element {item} not found : guild {guild}, lang {lang}") - return "Could not load" + return f"Could not load <{item}>" diff --git a/vesta/modals/custom_form.py b/vesta/modals/custom_form.py index 3dc1fcd..4fabaa5 100644 --- a/vesta/modals/custom_form.py +++ b/vesta/modals/custom_form.py @@ -6,7 +6,7 @@ import discord.ui from sqlalchemy import select -from .. import session_maker, vesta_client, lang +from .. import session_maker, vesta_client, lang_file from ..tables import User, CustomCommand logger = logging.getLogger(__name__) @@ -51,12 +51,12 @@ class CustomSlashForm(discord.ui.Modal, title=""): def __init__(self, keyword, interaction): logger.debug(f"CustomSlashForm created for {interaction.user}") - self.title = lang.get("custom_form", interaction.guild) - self.command_title.label = lang.get("custom_form_title", interaction.guild) - self.command_content.label = lang.get("custom_form_content", interaction.guild) - self.command_url.label = lang.get("custom_form_link", interaction.guild) - self.command_image.label = lang.get("custom_form_image", interaction.guild) - self.command_colour.label = lang.get("custom_form_color", interaction.guild) + self.title = lang_file.get("custom_form", interaction.guild) + self.command_title.label = lang_file.get("custom_form_title", interaction.guild) + self.command_content.label = lang_file.get("custom_form_content", interaction.guild) + self.command_url.label = lang_file.get("custom_form_link", interaction.guild) + self.command_image.label = lang_file.get("custom_form_image", interaction.guild) + self.command_colour.label = lang_file.get("custom_form_color", interaction.guild) super().__init__() self.keyword = keyword @@ -67,7 +67,7 @@ async def on_submit(self, interaction: discord.Interaction): command_title = self.command_title.value.strip() command_content = self.command_content.value.strip() if not command_title or not command_content: - return await interaction.response.send_message(lang.get("custom_invalid_args", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("custom_invalid_args", interaction.guild), ephemeral=True) command_url = self.command_url.value if command_url and not re.match(http_regex, command_url): @@ -75,7 +75,7 @@ async def on_submit(self, interaction: discord.Interaction): command_url = 'https://' + command_url else: return await interaction.response.send_message( - content=lang.get("invalid_link", interaction.guild), + content=lang_file.get("invalid_link", interaction.guild), ephemeral=True, ) image_url = self.command_image.value @@ -84,7 +84,7 @@ async def on_submit(self, interaction: discord.Interaction): image_url = 'https://' + image_url else: return await interaction.response.send_message( - content=lang.get("invalid_image_link", interaction.guild), + content=lang_file.get("invalid_image_link", interaction.guild), ephemeral=True, ) r = select(User).where(User.id == interaction.user.id) @@ -115,10 +115,10 @@ async def on_submit(self, interaction: discord.Interaction): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) - await interaction.response.send_message(lang.get("command_created", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("command_created", interaction.guild), ephemeral=True) @app_commands.guild_only() async def command(interaction: discord.Interaction): @@ -133,7 +133,7 @@ async def command(interaction: discord.Interaction): async def on_error(self, interaction, error): logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) class CustomMenuForm(discord.ui.Modal, title=""): @@ -170,12 +170,12 @@ class CustomMenuForm(discord.ui.Modal, title=""): def __init__(self, content, author, interaction): logger.debug(f"CustomMenuForm created for {interaction.user}") - self.title = lang.get("custom_form", interaction.guild) - self.command_keyword.label = lang.get("custom_form_keyword", interaction.guild) - self.command_title.label = lang.get("custom_form_title", interaction.guild) - self.command_url.label = lang.get("custom_form_link", interaction.guild) - self.command_image.label = lang.get("custom_form_image", interaction.guild) - self.command_colour.label = lang.get("custom_form_color", interaction.guild) + self.title = lang_file.get("custom_form", interaction.guild) + self.command_keyword.label = lang_file.get("custom_form_keyword", interaction.guild) + self.command_title.label = lang_file.get("custom_form_title", interaction.guild) + self.command_url.label = lang_file.get("custom_form_link", interaction.guild) + self.command_image.label = lang_file.get("custom_form_image", interaction.guild) + self.command_colour.label = lang_file.get("custom_form_color", interaction.guild) super().__init__() self.content = content @@ -186,11 +186,11 @@ async def on_submit(self, interaction: discord.Interaction): keyword = self.command_keyword.value.lower() if not re.match(custom_regex, keyword): - return await interaction.response.send_message(lang.get("invalid_keyword", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("invalid_keyword", interaction.guild), ephemeral=True) command_title = self.command_title.value.strip() if not command_title: - return await interaction.response.send_message(lang.get("custom_invalid_args", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("custom_invalid_args", interaction.guild), ephemeral=True) command_url = self.command_url.value if command_url and not re.match(http_regex, command_url): @@ -198,7 +198,7 @@ async def on_submit(self, interaction: discord.Interaction): command_url = 'https://' + command_url else: return await interaction.response.send_message( - content=lang.get("invalid_link", interaction.guild), + content=lang_file.get("invalid_link", interaction.guild), ephemeral=True, ) image_url = self.command_image.value @@ -207,7 +207,7 @@ async def on_submit(self, interaction: discord.Interaction): image_url = 'https://' + image_url else: return await interaction.response.send_message( - content=lang.get("invalid_image_link", interaction.guild), + content=lang_file.get("invalid_image_link", interaction.guild), ephemeral=True, ) r = select(User).where(User.id == self.author.id) @@ -222,7 +222,7 @@ async def on_submit(self, interaction: discord.Interaction): r = select(CustomCommand).where(CustomCommand.guild_id == interaction.guild_id) r = r.where(CustomCommand.keyword == keyword) if session.scalar(r): - return await interaction.response.send_message(lang.get("command_already_exist", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("command_already_exist", interaction.guild), ephemeral=True) custom_command = CustomCommand( guild_id=interaction.guild_id, keyword=keyword, @@ -241,10 +241,10 @@ async def on_submit(self, interaction: discord.Interaction): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) - await interaction.response.send_message(lang.get("command_created", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("command_created", interaction.guild), ephemeral=True) @app_commands.guild_only() async def command(interaction: discord.Interaction): @@ -257,4 +257,4 @@ async def command(interaction: discord.Interaction): async def on_error(self, interaction, error): logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) diff --git a/vesta/modals/presentation_form.py b/vesta/modals/presentation_form.py index cfe2046..9a6912a 100644 --- a/vesta/modals/presentation_form.py +++ b/vesta/modals/presentation_form.py @@ -4,7 +4,7 @@ from sqlalchemy import select import logging -from .. import vesta_client, session_maker, lang +from .. import vesta_client, session_maker, lang_file from ..views import Review from ..tables import Presentation, User, Guild @@ -41,11 +41,11 @@ class PresentationForm(discord.ui.Modal, title=""): def __init__(self, interaction): logger.debug(f"PresentationForm created for {interaction.user}") - self.title = lang.get("presentation_form", interaction.guild) - self.presentation_title.label = lang.get("presentation_form_title", interaction.guild) - self.description.label = lang.get("presentation_form_description", interaction.guild) - self.link.label = lang.get("presentation_form_link", interaction.guild) - self.image_url.label = lang.get("presentation_form_image", interaction.guild) + self.title = lang_file.get("presentation_form", interaction.guild) + self.presentation_title.label = lang_file.get("presentation_form_title", interaction.guild) + self.description.label = lang_file.get("presentation_form_description", interaction.guild) + self.link.label = lang_file.get("presentation_form_link", interaction.guild) + self.image_url.label = lang_file.get("presentation_form_image", interaction.guild) super().__init__() @@ -54,7 +54,7 @@ async def on_submit(self, interaction: discord.Interaction): title = self.presentation_title.value.strip() if not title: - return await interaction.response.send_message(lang.get("custom_invalid_args", interaction.guild), ephemeral=True) + return await interaction.response.send_message(lang_file.get("custom_invalid_args", interaction.guild), ephemeral=True) link_value = self.link.value if not re.match(http_regex, link_value): @@ -62,7 +62,7 @@ async def on_submit(self, interaction: discord.Interaction): link_value = 'https://' + link_value else: return await interaction.response.send_message( - content=lang.get("invalid_link", interaction.guild), + content=lang_file.get("invalid_link", interaction.guild), ephemeral=True, ) image_url = self.image_url.value @@ -71,7 +71,7 @@ async def on_submit(self, interaction: discord.Interaction): image_url = 'https://' + image_url else: return await interaction.response.send_message( - content=lang.get("invalid_image_link", interaction.guild), + content=lang_file.get("invalid_image_link", interaction.guild), ephemeral=True, ) @@ -88,11 +88,11 @@ async def on_submit(self, interaction: discord.Interaction): r = select(Guild).where(Guild.id == interaction.guild_id) guild = session.scalar(r) if not guild or not guild.review_channel: - return await interaction.response.send_message(lang.get("review_channel_error", interaction.guild)) + return await interaction.response.send_message(lang_file.get("review_channel_error", interaction.guild)) channel = vesta_client.get_channel(guild.review_channel) if not channel: - return await interaction.response.send_message(lang.get("review_channel_error", interaction.guild)) + return await interaction.response.send_message(lang_file.get("review_channel_error", interaction.guild)) presentation = Presentation( title=self.presentation_title.value, @@ -116,19 +116,19 @@ async def on_submit(self, interaction: discord.Interaction): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) - await message.create_thread(name=lang.get("thread_project", interaction.guild) + " [" + title + "]") + await message.create_thread(name=lang_file.get("thread_project", interaction.guild) + " [" + title + "]") await interaction.response.send_message( - content=lang.get("presentation_sent", interaction.guild), + content=lang_file.get("presentation_sent", interaction.guild), ephemeral=True) await view.wait() async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: await interaction.response.send_message( - content=lang.get("unexpected_error", interaction.guild), + content=lang_file.get("unexpected_error", interaction.guild), ephemeral=True, ) traceback.print_exc() diff --git a/vesta/modals/refused_form.py b/vesta/modals/refused_form.py index f1bcb36..770c2ce 100644 --- a/vesta/modals/refused_form.py +++ b/vesta/modals/refused_form.py @@ -2,7 +2,7 @@ import logging import traceback -from .. import vesta_client, lang +from .. import vesta_client, lang_file logger = logging.getLogger(__name__) @@ -16,8 +16,8 @@ class RefusedReasonForm(discord.ui.Modal, title=""): def __init__(self, presentation, interaction): logger.debug(f"RefusedReasonForm created for {interaction.user}") - self.title = lang.get("denied_form", interaction.guild) - self.reason.label = lang.get("denied_form_reason", interaction.guild) + self.title = lang_file.get("denied_form", interaction.guild) + self.reason.label = lang_file.get("denied_form_reason", interaction.guild) super().__init__() self.presentation = presentation @@ -27,14 +27,14 @@ async def on_submit(self, interaction: discord.Interaction): presentation_embed = self.presentation.embed('222222') reason_embed = discord.Embed( colour=int('ff2222', 16), - title=lang.get("denied_feedback_title", interaction.guild), - description=lang.get("denied_feedback_content", interaction.guild) + f" {self.reason.value}", + title=lang_file.get("denied_feedback_title", interaction.guild), + description=lang_file.get("denied_feedback_content", interaction.guild) + f" {self.reason.value}", ) user = await vesta_client.fetch_user(self.presentation.author_id) await user.send(embeds=[presentation_embed, reason_embed]) - await interaction.response.send_message(lang.get("denied_registered", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("denied_registered", interaction.guild), ephemeral=True) async def on_error(self, interaction, error): logger.error(traceback.format_exc()) - await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), ephemeral=True) + await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) diff --git a/vesta/services/__init__.py b/vesta/services/__init__.py new file mode 100644 index 0000000..6e3bffd --- /dev/null +++ b/vesta/services/__init__.py @@ -0,0 +1,2 @@ +from .clash_of_code_entities import ClashOfCodeGame, ClashOfCodePlayer, GameMode, Role, State +from . import clash_of_code_helper \ No newline at end of file diff --git a/vesta/services/clash_of_code_entities.py b/vesta/services/clash_of_code_entities.py new file mode 100644 index 0000000..d0f46b6 --- /dev/null +++ b/vesta/services/clash_of_code_entities.py @@ -0,0 +1,213 @@ +import datetime +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional + +import discord +from discord import Embed + + +class GameMode(Enum): + """ + Represents a Clash of Code game mode + """ + + FASTEST = 0 + REVERSE = 1 + SHORTEST = 2 + + def __repr__(self): + return f"coc_mode_{self.name.lower()}" + +class Role(Enum): + """ + Represents a Clash of Code player role + """ + + OWNER = 0 + STANDARD = 1 + +class State(Enum): + """ + Represents a Clash of Code game state + """ + + PENDING = 0 + RUNNING = 1 + FINISHED = 2 + + def __repr__(self): + """ + Gives the translation key for the state + """ + return f"coc_game_state_{self.name.lower()}" + + def __str__(self): + """ + Gives the emoji for the state + """ + return { + State.PENDING: "🕒", + State.RUNNING: "🔥", + State.FINISHED: "🏁" + }[self] + +@dataclass +class ClashOfCodePlayer: + """ + Represents a player in a Clash of Code game + """ + + name: str + role: Role + rank: int + state: State + + def __init__(self, **kwargs): + """ + Initializes the object with the given data + + :param kwargs: The data to initialize the object with + :see hydrate + """ + self.hydrate(**kwargs) + + def hydrate(self, *, + codingamerNickname: str, + status: str, + rank: Optional[int], + testSessionStatus: Optional[str] = None, + **ignored) -> "ClashOfCodePlayer": + """ + Hydrates the object with the given data + + :param codingamerNickname: The player's nickname + :param status: The player's role + :return: The hydrated object for chaining convenience + """ + self.name = codingamerNickname + self.role = Role[status.upper()] or Role.STANDARD + self.rank = rank + self.state = State.PENDING if testSessionStatus is None\ + else State.FINISHED if testSessionStatus == "COMPLETED" \ + else State.RUNNING + + return self + + +@dataclass +class ClashOfCodeGame: + """ + Represents a Clash of Code game + """ + + link: str + state: State + players: List[ClashOfCodePlayer] + programming_language: List[str] + modes: List[GameMode] + mode: Optional[GameMode] + + start_time: datetime.datetime + end_time: Optional[datetime.datetime] + + def __init__(self, **kwargs): + """ + Initializes the object with the given data + :param kwargs: The data to initialize the object with + :see hydrate + """ + + self.hydrate(**kwargs) + + @property + def id(self): return self.link.split("/")[-1] + + def hydrate(self, *, + publicHandle: str, + started: bool, + finished: bool, + players: List[dict], + programmingLanguages: List[str], + modes: List[str], + mode: Optional[str] = None, + startTime: str, + endTime: Optional[str] = None, + **ignored) -> "ClashOfCodeGame": + """ + Hydrates the object with the given data + + :param publicHandle: The game id + :param started: Whether the game has started + :param finished: Whether the game has finished + :param players: The players in the game + :param programmingLanguages: The programming languages used in the game + :param modes: The possible game modes + :param mode: The current game mode. Only defined if started is True + :return: The hydrated object for chaining convenience + """ + + self.link = f"https://www.codingame.com/clashofcode/clash/{publicHandle}" + self.state = State.FINISHED if finished else State.RUNNING if started else State.PENDING + self.players = [ + ClashOfCodePlayer(**player) + for player in players + ] + self.programming_language = programmingLanguages + self.modes = [ + GameMode[mode.upper()] + for mode in modes + ] + self.mode = GameMode[mode.upper()] if mode else None + + self.start_time = datetime.datetime.strptime(startTime, "%B %d, %Y, %I:%M:%S %p") + self.end_time = datetime.datetime.strptime(endTime, "%B %d, %Y, %I:%M:%S %p") if endTime else None + + return self + + def embed(self, guild: discord.Guild): + """ + Builds a legible embed to display on discord + + :param guild: + :return: + """ + from .. import lang_file + + embed = Embed( + title=lang_file.get("coc_game_title", guild), + color=discord.Color.blurple(), + ) + + embed.add_field(name=lang_file.get("coc_game_state", guild), + value=lang_file.get(repr(self.state), guild), + inline=True) + + if not self.mode: + embed.add_field(name=lang_file.get("coc_game_modes", guild), + value=' - ' + "\n - ".join( + [lang_file.get(repr(mode), guild) for mode in self.modes]), + inline=False) + else: + embed.add_field(name=lang_file.get("coc_game_mode", guild), + value=lang_file.get(repr(self.mode), guild), + inline=True) + + if self.state == State.FINISHED: + winner = sorted(self.players, key=lambda player: player.rank)[0] + embed.add_field(name=lang_file.get("coc_game_winner", guild), + value=f"🏆 {winner.name}", + inline=True) + else: + embed.add_field(name=lang_file.get("coc_game_players", guild), + value=f"`{'`, `'.join([f'{str(player.state)} {player.name}' for player in self.players])}`", + inline=False) + + languages = f"`{'`, `'.join(self.programming_language)}`" \ + if len(self.programming_language) >= 1 \ + else lang_file.get("coc_all_languages", guild) + + embed.add_field(name=lang_file.get("coc_game_languages", guild), + value=languages) + + return embed diff --git a/vesta/services/clash_of_code_helper.py b/vesta/services/clash_of_code_helper.py new file mode 100644 index 0000000..3697926 --- /dev/null +++ b/vesta/services/clash_of_code_helper.py @@ -0,0 +1,132 @@ +import asyncio +import logging +import uuid +from typing import Optional + +import discord +import requests +from discord.utils import MISSING +from sqlalchemy import select + +from . import ClashOfCodeGame, State + +BASE_ENDPOINT = "https://www.codingame.com/services/ClashOfCode" +logger = logging.getLogger(__name__) + +def game_id_from_link(link: str) -> str: + return link.split("/")[-1] + +def fetch(game_id) -> Optional[ClashOfCodeGame]: + """ + Retrieves a game object from the API + + :param game_id: The game id to retrieve + :return: The game object if it exists, None otherwise + """ + r = requests.post(f"{BASE_ENDPOINT}/findClashByHandle", json=[ + game_id + ]) + + if r.status_code != 200: + return None + + return ClashOfCodeGame(**r.json()) + +def update(game: ClashOfCodeGame) -> ClashOfCodeGame: + """ + Updates the game object with the latest information from the API + + :param game: The game object to update + :return: The updated game object for chaining convenience + :raises NameError: If there was an error updating the game object + """ + r = requests.post(f"{BASE_ENDPOINT}/findClashByHandle", json=[ + game.id + ]) + + if r.status_code != 200: + raise NameError("There was an error updating the game object") + + game.hydrate(**r.json()) + return game + +def resume_update_loops() -> None: + """ + Resumes all update loops for all guilds + + :return: Nothing + """ + from .. import session_maker, vesta_client + from ..tables import ClashOfCodeGuildGame, Guild + session = session_maker() + + logger.debug(f"Resuming update loops for all guilds") + + r = select(ClashOfCodeGuildGame) + guild_games: list[ClashOfCodeGuildGame] = session.execute(r).scalars().all() + + for guild_game in guild_games: + guild = vesta_client.get_guild(guild_game.guild_id) + + if not guild: + continue + + r = select(Guild).where(Guild.id == guild.id) + guild_table: Guild = session.scalar(r) + + if not guild_table: + continue + + announcement_channel = guild.get_channel(guild_table.coc_channel) + if not announcement_channel: + continue + + message = announcement_channel.get_partial_message(guild_game.announcement_message_id) + if not message: + continue + + start_update_loop(message, guild) + +def start_update_loop(message: discord.Message, guild: discord.Guild) -> None: + """ + Starts an update loop for the given message and guild to + always match the latest information from the API and the + message content + + :param message: The message to update + :param guild: The guild to update the data for + :return: Nothing + """ + from .. import session_maker + from ..tables import ClashOfCodeGuildGame + session = session_maker() + + uid = str(uuid.uuid4())[0:5] + + logger.debug(f"[{uid}] Starting update loop for {message.id} in {guild.id}") + + r = select(ClashOfCodeGuildGame).where(ClashOfCodeGuildGame.guild_id == guild.id) + guild_game: ClashOfCodeGuildGame = session.scalar(r) + fetched_game: ClashOfCodeGame = guild_game.fetch() + + async def update_loop(game: ClashOfCodeGame) -> None: + """ + The update loop that runs every 10 seconds to update the message + + :param game: The game object to update + :return: Nothing + """ + while True: + game = update(game) + await message.edit(embed=game.embed(guild), view=None if game.state == State.FINISHED else MISSING) + + if game.state == State.FINISHED: + logger.debug(f"[{uid}] Game {game.id} has finished, deleting from database") + + session.delete(guild_game) + session.commit() + break + + await asyncio.sleep(10) + + asyncio.create_task(update_loop(fetched_game)) \ No newline at end of file diff --git a/vesta/tables/__init__.py b/vesta/tables/__init__.py index 32f221b..4387aa9 100644 --- a/vesta/tables/__init__.py +++ b/vesta/tables/__init__.py @@ -7,4 +7,5 @@ from .users import User from .custom_commands import CustomCommand from .guilds import Guild -from .bans import Ban \ No newline at end of file +from .bans import Ban +from .clash_of_code import ClashOfCodeGuildGame \ No newline at end of file diff --git a/vesta/tables/clash_of_code.py b/vesta/tables/clash_of_code.py new file mode 100644 index 0000000..9ccdace --- /dev/null +++ b/vesta/tables/clash_of_code.py @@ -0,0 +1,28 @@ +from typing import Optional + +import sqlalchemy as db + +from . import Base +from .. import session_maker +from ..services import clash_of_code_helper, ClashOfCodeGame + +session = session_maker() + +class ClashOfCodeGuildGame(Base): + __tablename__ = "clash_of_code_guild_game" + + guild_id = db.Column(db.BigInteger, nullable=False, primary_key=True) + last_clash_id = db.Column(db.String(511), nullable=True) + announcement_message_id = db.Column(db.BigInteger, nullable=True) + + def fetch(self) -> Optional[ClashOfCodeGame]: + """ + Fetches the latest clash of code game + + :return: The latest clash of code game + """ + + return clash_of_code_helper.fetch(self.last_clash_id) + + def __repr__(self): + return f"Clash of Code Guild Games (guild_id={self.guild_id}, last_clash_id={self.last_clash_id})" diff --git a/vesta/tables/guilds.py b/vesta/tables/guilds.py index 5ef3973..bed4475 100644 --- a/vesta/tables/guilds.py +++ b/vesta/tables/guilds.py @@ -2,7 +2,6 @@ from . import Base - class Guild(Base): __tablename__ = "guild" @@ -10,6 +9,8 @@ class Guild(Base): name = db.Column(db.String(32), nullable=False) review_channel = db.Column(db.BigInteger) projects_channel = db.Column(db.BigInteger) + coc_channel = db.Column(db.BigInteger) + coc_role = db.Column(db.BigInteger) lang = db.Column(db.String(2)) def __repr__(self): diff --git a/vesta/views/review.py b/vesta/views/review.py index c01972e..b55bae7 100644 --- a/vesta/views/review.py +++ b/vesta/views/review.py @@ -3,7 +3,7 @@ import logging import traceback -from .. import session_maker, vesta_client, lang +from .. import session_maker, vesta_client, lang_file from ..modals import RefusedReasonForm from ..tables import select, Guild, Presentation @@ -14,12 +14,12 @@ class DropdownReview(discord.ui.Select): def __init__(self, interaction): options = [ - discord.SelectOption(label=lang.get("reason_not_enough_code", interaction.guild), value="text_not_enough_code", description='', emoji='📝'), - discord.SelectOption(label=lang.get("reason_not_open_source", interaction.guild), value="text_not_open_source", description='', emoji='🔒'), - discord.SelectOption(label=lang.get("reason_illegal", interaction.guild), value="text_illegal", description='', emoji='👮'), - discord.SelectOption(label=lang.get("reason_other", interaction.guild), value="Other", description='', emoji='✒') + discord.SelectOption(label=lang_file.get("reason_not_enough_code", interaction.guild), value="text_not_enough_code", description='', emoji='📝'), + discord.SelectOption(label=lang_file.get("reason_not_open_source", interaction.guild), value="text_not_open_source", description='', emoji='🔒'), + discord.SelectOption(label=lang_file.get("reason_illegal", interaction.guild), value="text_illegal", description='', emoji='👮'), + discord.SelectOption(label=lang_file.get("reason_other", interaction.guild), value="Other", description='', emoji='✒') ] - super().__init__(placeholder=lang.get("deny", interaction.guild), options=options, custom_id="deny_select") + super().__init__(placeholder=lang_file.get("deny", interaction.guild), options=options, custom_id="deny_select") async def callback(self, interaction: discord.Interaction): @@ -34,8 +34,8 @@ async def callback(self, interaction: discord.Interaction): presentation_embed = presentation.embed('222222') reason_embed = discord.Embed( colour=int('ff2222', 16), - title=lang.get("denied_feedback_title", interaction.guild), - description=f"{lang.get('denied_feedback_content', interaction.guild)} {lang.get(self.values[0], interaction.guild)}") + title=lang_file.get("denied_feedback_title", interaction.guild), + description=f"{lang_file.get('denied_feedback_content', interaction.guild)} {lang_file.get(self.values[0], interaction.guild)}") user = await vesta_client.fetch_user(presentation.author_id) await user.send(embeds=[presentation_embed, reason_embed]) @@ -50,12 +50,12 @@ async def callback(self, interaction: discord.Interaction): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) embed = interaction.message.embeds[0] embed.colour = int("ff2222", 16) - embed.set_footer(text=lang.get("denied_by", interaction.guild) + f" {interaction.user.display_name}") + embed.set_footer(text=lang_file.get("denied_by", interaction.guild) + f" {interaction.user.display_name}") embed.timestamp = presentation.review_date await interaction.message.edit(embed=embed, view=None) self.view.stop() @@ -64,7 +64,7 @@ async def callback(self, interaction: discord.Interaction): class AcceptReview(discord.ui.Button): def __init__(self, interaction): - super().__init__(style=discord.ButtonStyle.green, label=lang.get("accept", interaction.guild), custom_id="accept_button") + super().__init__(style=discord.ButtonStyle.green, label=lang_file.get("accept", interaction.guild), custom_id="accept_button") async def callback(self, interaction: discord.Interaction): @@ -77,11 +77,11 @@ async def callback(self, interaction: discord.Interaction): guild = session.scalar(r) if not guild or not guild.projects_channel: - return interaction.response.send_message(lang.get("projects_channel_error", interaction.guild)) + return interaction.response.send_message(lang_file.get("projects_channel_error", interaction.guild)) channel = vesta_client.get_channel(guild.projects_channel) if not channel: - return interaction.response.send_message(lang.get("projects_channel_error", interaction.guild)) + return interaction.response.send_message(lang_file.get("projects_channel_error", interaction.guild)) presentation.reviewed = True presentation.review_date = datetime.now() @@ -94,18 +94,18 @@ async def callback(self, interaction: discord.Interaction): session.rollback() logger.error(traceback.format_exc()) - return await interaction.response.send_message(lang.get("unexpected_error", interaction.guild), + return await interaction.response.send_message(lang_file.get("unexpected_error", interaction.guild), ephemeral=True) embed = interaction.message.embeds[0] embed.colour = int("22ff22", 16) - embed.set_footer(text=lang.get("accepted_by", interaction.guild) + f" {interaction.user.display_name}") + embed.set_footer(text=lang_file.get("accepted_by", interaction.guild) + f" {interaction.user.display_name}") embed.timestamp = presentation.review_date await interaction.response.edit_message(embed=embed, view=None) embed.title = " ".join(embed.title.split()[1:]) message = await channel.send(embed=embed) - await message.create_thread(name=lang.get("thread_project", interaction.guild) + " [" + embed.title + "]") + await message.create_thread(name=lang_file.get("thread_project", interaction.guild) + " [" + embed.title + "]") self.view.stop()