From efdc9666118dc62b90904188e5e957461a941a06 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 22 Jun 2022 00:22:26 +0200 Subject: [PATCH 1/5] Invoke custom commands --- database/crud/custom_commands.py | 5 ++--- didier/didier.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/database/crud/custom_commands.py b/database/crud/custom_commands.py index afd41ce..e06a4ff 100644 --- a/database/crud/custom_commands.py +++ b/database/crud/custom_commands.py @@ -34,8 +34,7 @@ async def create_alias(session: AsyncSession, command: str, alias: str) -> Custo raise NoResultFoundException # Check if the alias exists (either as an alias or as a name) - alias_instance = await get_command(session, alias) - if alias_instance is not None: + if await get_command(session, alias) is not None: raise DuplicateInsertException alias_instance = CustomCommandAlias(alias=alias, indexed_alias=clean_name(alias), command=command_instance) @@ -47,7 +46,7 @@ async def create_alias(session: AsyncSession, command: str, alias: str) -> Custo async def get_command(session: AsyncSession, message: str) -> Optional[CustomCommand]: """Try to get a command out of a message""" - # Search lowercase & without spaces, and strip the prefix + # Search lowercase & without spaces message = clean_name(message) return (await get_command_by_name(session, message)) or (await get_command_by_alias(session, message)) diff --git a/didier/didier.py b/didier/didier.py index b42dfcc..06786e0 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -8,6 +8,7 @@ from discord.ext import commands from sqlalchemy.ext.asyncio import AsyncSession import settings +from database.crud import custom_commands from database.engine import DBSession from didier.utils.discord.prefix import get_prefix @@ -109,9 +110,23 @@ class Didier(commands.Bot): """Check if the message tries to invoke a custom command If it does, send the reply associated with it """ + # Doesn't start with the custom command prefix if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX): return False + async with self.db_session as session: + # Remove the prefix + content = message.content[len(settings.DISCORD_CUSTOM_COMMAND_PREFIX) :] + command = await custom_commands.get_command(session, content) + + # Command found + if command is not None: + await message.reply(command.response, mention_author=False) + return True + + # Nothing found + return False + async def on_command_error(self, context: commands.Context, exception: commands.CommandError, /) -> None: """Event triggered when a regular command errors""" # If developing, print everything to stdout so you don't have to From d8192cfa0af8f6d145fbb9c779ace302d070a4ef Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 22 Jun 2022 00:49:00 +0200 Subject: [PATCH 2/5] Adding custom commands & aliases --- didier/cogs/owner.py | 43 ++++++++++++++++++++++++++++++++++++++++++- didier/didier.py | 25 ++++++++++++++++--------- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index af50633..f87dc5e 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -3,6 +3,9 @@ from typing import Optional import discord from discord.ext import commands +from database.crud import custom_commands +from database.exceptions.constraints import DuplicateInsertException +from database.exceptions.not_found import NoResultFoundException from didier import Didier @@ -14,13 +17,51 @@ class Owner(commands.Cog): def __init__(self, client: Didier): self.client = client + async def cog_check(self, ctx: commands.Context) -> bool: + """Global check for every command in this cog, so we don't have to add + is_owner() to every single command separately + """ + return await self.client.is_owner(ctx.author) + @commands.command(name="Sync") - @commands.is_owner() async def sync(self, ctx: commands.Context, guild: Optional[discord.Guild] = None): """Sync all application-commands in Discord""" await self.client.tree.sync(guild=guild) await ctx.message.add_reaction("🔄") + @commands.group(name="Add", case_insensitive=True, invoke_without_command=False) + async def add(self, ctx: commands.Context): + """Command group for [add X] commands""" + + @add.command(name="Custom") + async def add_custom(self, ctx: commands.Context, name: str, *, response: str): + """Add a new custom command""" + async with self.client.db_session as session: + try: + await custom_commands.create_command(session, name, response) + await self.client.confirm_message(ctx.message) + except DuplicateInsertException: + await ctx.reply("Er bestaat al een commando met deze naam.") + await self.client.reject_message(ctx.message) + + @add.command(name="Alias") + async def add_alias(self, ctx: commands.Context, command: str, alias: str): + """Add a new alias for a custom command""" + async with self.client.db_session as session: + try: + await custom_commands.create_alias(session, command, alias) + await self.client.confirm_message(ctx.message) + except NoResultFoundException: + await ctx.reply(f'Geen commando gevonden voor "{command}".') + await self.client.reject_message(ctx.message) + except DuplicateInsertException: + await ctx.reply("Er bestaat al een commando met deze naam.") + await self.client.reject_message(ctx.message) + + @commands.group(name="Edit") + async def edit(self, ctx: commands.Context): + """Command group for [edit X] commands""" + async def setup(client: Didier): """Load the cog""" diff --git a/didier/didier.py b/didier/didier.py index 06786e0..3cd2648 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -3,7 +3,6 @@ import sys import traceback import discord -from discord import Message from discord.ext import commands from sqlalchemy.ext.asyncio import AsyncSession @@ -35,6 +34,11 @@ class Didier(commands.Bot): command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status ) + @property + def db_session(self) -> AsyncSession: + """Obtain a database session""" + return DBSession() + async def setup_hook(self) -> None: """Hook called once the bot is initialised""" # Load extensions @@ -48,11 +52,6 @@ class Didier(commands.Bot): self.tree.copy_global_to(guild=guild_object) await self.tree.sync(guild=guild_object) - @property - def db_session(self) -> AsyncSession: - """Obtain a database session""" - return DBSession() - async def _load_initial_extensions(self): """Load all extensions that should be loaded before the others""" for extension in self.initial_extensions: @@ -86,11 +85,19 @@ class Didier(commands.Bot): channel = self.get_channel(reference.channel_id) return await channel.fetch_message(reference.message_id) + async def confirm_message(self, message: discord.Message): + """Add a checkmark to a message""" + await message.add_reaction("✅") + + async def reject_message(self, message: discord.Message): + """Add an X to a message""" + await message.add_reaction("❌") + async def on_ready(self): """Event triggered when the bot is ready""" print(settings.DISCORD_READY_MESSAGE) - async def on_message(self, message: Message, /) -> None: + async def on_message(self, message: discord.Message, /) -> None: """Event triggered when a message is sent""" # Ignore messages by bots if message.author.bot: @@ -101,12 +108,12 @@ class Didier(commands.Bot): await message.add_reaction(settings.DISCORD_BOOS_REACT) # Potential custom command - if self._try_invoke_custom_command(message): + if await self._try_invoke_custom_command(message): return await self.process_commands(message) - async def _try_invoke_custom_command(self, message: Message) -> bool: + async def _try_invoke_custom_command(self, message: discord.Message) -> bool: """Check if the message tries to invoke a custom command If it does, send the reply associated with it """ From fc195e40b387f4f9b56cba90cb3903a86e6acc12 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 22 Jun 2022 01:56:13 +0200 Subject: [PATCH 3/5] Fix syncing --- didier/cogs/owner.py | 33 +++++++++++++++++++++++---- didier/data/modals/__init__.py | 0 didier/data/modals/custom_commands.py | 20 ++++++++++++++++ didier/didier.py | 7 ------ 4 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 didier/data/modals/__init__.py create mode 100644 didier/data/modals/custom_commands.py diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index f87dc5e..531d0b8 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -1,12 +1,14 @@ from typing import Optional import discord +from discord import app_commands from discord.ext import commands from database.crud import custom_commands from database.exceptions.constraints import DuplicateInsertException from database.exceptions.not_found import NoResultFoundException from didier import Didier +from didier.data.modals.custom_commands import CreateCustomCommand class Owner(commands.Cog): @@ -14,6 +16,9 @@ class Owner(commands.Cog): client: Didier + # Slash groups + add_slash = app_commands.Group(name="add", description="Add something new to the database") + def __init__(self, client: Didier): self.client = client @@ -26,14 +31,20 @@ class Owner(commands.Cog): @commands.command(name="Sync") async def sync(self, ctx: commands.Context, guild: Optional[discord.Guild] = None): """Sync all application-commands in Discord""" - await self.client.tree.sync(guild=guild) + if guild is not None: + self.client.tree.copy_global_to(guild=guild) + await self.client.tree.sync(guild=guild) + else: + self.client.tree.clear_commands(guild=None) + await self.client.tree.sync() + await ctx.message.add_reaction("🔄") @commands.group(name="Add", case_insensitive=True, invoke_without_command=False) - async def add(self, ctx: commands.Context): - """Command group for [add X] commands""" + async def add_msg(self, ctx: commands.Context): + """Command group for [add X] message commands""" - @add.command(name="Custom") + @add_msg.command(name="Custom") async def add_custom(self, ctx: commands.Context, name: str, *, response: str): """Add a new custom command""" async with self.client.db_session as session: @@ -44,7 +55,7 @@ class Owner(commands.Cog): await ctx.reply("Er bestaat al een commando met deze naam.") await self.client.reject_message(ctx.message) - @add.command(name="Alias") + @add_msg.command(name="Alias") async def add_alias(self, ctx: commands.Context, command: str, alias: str): """Add a new alias for a custom command""" async with self.client.db_session as session: @@ -58,6 +69,18 @@ class Owner(commands.Cog): await ctx.reply("Er bestaat al een commando met deze naam.") await self.client.reject_message(ctx.message) + @add_slash.command(name="custom", description="Add a custom command") + async def add_custom_slash(self, interaction: discord.Interaction): + """Slash command to add a custom command""" + if not self.client.is_owner(interaction.user): + return interaction.response.send_message( + "Je hebt geen toestemming om dit commando uit te voeren.", ephemeral=True + ) + + await interaction.response.defer(ephemeral=True) + modal = CreateCustomCommand() + await interaction.response.send_message(modal) + @commands.group(name="Edit") async def edit(self, ctx: commands.Context): """Command group for [edit X] commands""" diff --git a/didier/data/modals/__init__.py b/didier/data/modals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/didier/data/modals/custom_commands.py b/didier/data/modals/custom_commands.py new file mode 100644 index 0000000..8e6aa97 --- /dev/null +++ b/didier/data/modals/custom_commands.py @@ -0,0 +1,20 @@ +import traceback + +import discord + + +class CreateCustomCommand(discord.ui.Modal, title="Custom Command"): + """Modal shown to visually create custom commands""" + + name = discord.ui.TextInput(label="Name", placeholder="Name of the command...") + + response = discord.ui.TextInput( + label="Response", style=discord.TextStyle.long, placeholder="Response of the command...", max_length=2000 + ) + + async def on_submit(self, interaction: discord.Interaction) -> None: + await interaction.response.send_message("Submitted", ephemeral=True) + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + await interaction.response.send_message("Errored", ephemeral=True) + traceback.print_tb(error.__traceback__) diff --git a/didier/didier.py b/didier/didier.py index 3cd2648..a36e57c 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -45,13 +45,6 @@ class Didier(commands.Bot): await self._load_initial_extensions() await self._load_directory_extensions("didier/cogs") - # Sync application commands to the test guild - for guild in settings.DISCORD_TEST_GUILDS: - guild_object = discord.Object(id=guild) - - self.tree.copy_global_to(guild=guild_object) - await self.tree.sync(guild=guild_object) - async def _load_initial_extensions(self): """Load all extensions that should be loaded before the others""" for extension in self.initial_extensions: From 57e805e31cac15d1e42ec8a93f3cb11393599145 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 22 Jun 2022 02:05:04 +0200 Subject: [PATCH 4/5] Try to fix async tests --- tests/conftest.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a74ba4c..c2c5e0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ -from typing import AsyncGenerator +import asyncio +from typing import AsyncGenerator, Generator from unittest.mock import MagicMock import pytest @@ -11,7 +12,14 @@ from didier import Didier @pytest.fixture(scope="session") -def tables(): +def event_loop() -> Generator: + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +@pytest.fixture(scope="session") +def tables(event_loop): """Initialize a database before the tests, and then tear it down again Starts from an empty database and runs through all the migrations to check those as well while we're at it @@ -23,7 +31,7 @@ def tables(): @pytest.fixture -async def database_session(tables) -> AsyncGenerator[AsyncSession, None]: +async def database_session(tables, event_loop) -> AsyncGenerator[AsyncSession, None]: """Fixture to create a session for every test Rollbacks the transaction afterwards so that the future tests start with a clean database """ From add939994481cdac714efd443b11bb876f6284ae Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 22 Jun 2022 02:09:16 +0200 Subject: [PATCH 5/5] Fix linting & typing --- didier/cogs/owner.py | 1 + didier/data/modals/custom_commands.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index 531d0b8..1e2d6b3 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -26,6 +26,7 @@ class Owner(commands.Cog): """Global check for every command in this cog, so we don't have to add is_owner() to every single command separately """ + # pylint: disable=W0236 # Pylint thinks this can't be async, but it can return await self.client.is_owner(ctx.author) @commands.command(name="Sync") diff --git a/didier/data/modals/custom_commands.py b/didier/data/modals/custom_commands.py index 8e6aa97..18d9809 100644 --- a/didier/data/modals/custom_commands.py +++ b/didier/data/modals/custom_commands.py @@ -6,15 +6,15 @@ import discord class CreateCustomCommand(discord.ui.Modal, title="Custom Command"): """Modal shown to visually create custom commands""" - name = discord.ui.TextInput(label="Name", placeholder="Name of the command...") + name: discord.ui.TextInput = discord.ui.TextInput(label="Name", placeholder="Name of the command...") - response = discord.ui.TextInput( + response: discord.ui.TextInput = discord.ui.TextInput( label="Response", style=discord.TextStyle.long, placeholder="Response of the command...", max_length=2000 ) async def on_submit(self, interaction: discord.Interaction) -> None: await interaction.response.send_message("Submitted", ephemeral=True) - async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: # type: ignore await interaction.response.send_message("Errored", ephemeral=True) traceback.print_tb(error.__traceback__)