From 78bc73c023b6e532628b7b7eeadd0523c61d1fdf Mon Sep 17 00:00:00 2001 From: Vivek Santayana Date: Sat, 17 Jul 2021 13:56:04 +0100 Subject: [PATCH] Started writing /commands. Completed channel group of config subcommands Fixed bugs in config initialisation function --- TODO.md | 26 +++- app/bot.py | 18 ++- app/cogs/botcommands/prefix.py | 11 +- app/cogs/{dev => devcommands}/dev.py | 7 + app/cogs/events/on_command_error.py | 5 +- app/cogs/events/on_ready.py | 11 +- app/cogs/events/on_slash_command_error.py | 36 +++++ app/cogs/slashcommands/config.py | 167 +++++++++++++++++++--- app/data/config.yml | 3 +- app/data/config_blueprint.yml | 1 + app/debug/debug.py | 15 +- 11 files changed, 259 insertions(+), 41 deletions(-) rename app/cogs/{dev => devcommands}/dev.py (91%) create mode 100644 app/cogs/events/on_slash_command_error.py diff --git a/TODO.md b/TODO.md index 6e35016..1d38c6c 100644 --- a/TODO.md +++ b/TODO.md @@ -5,17 +5,21 @@ - [x] Split event listeners into individual cogs. - [x] Update with re-organised data and config structure > - [x] Correct references to data in existing cogs. +- [x] Setup minimally functioning configs of guild on startup +- [x] Synchronise core configuration `/commands` on startup +- [ ] Synchronise secondary `/commands` on complete configuration **(see below)** -## Bot Functionality +## Bot Functionality and Processes - [ ] 'Delete Commands' Function - [ ] 'Register Commands' Function -- [ ] Infer Permissions from Config +- [x] Infer Permissions from Config - [x] Dynamic Command Prefixes - [ ] Infer Games from Server Structure - [ ] Re-enable logging - [x] Delete Dev/Test Functions - [x] Error handlers - [ ] Debug Features +> - [ ] Command Installer/Uninstaller - [x] Help Channel Event Listener > - [x] Add Config key for Help Channel - [ ] Slash Command Buttons or @@ -28,7 +32,7 @@ - [ ] Membership Restriction > - [ ] Message Receive Listener > - [ ] Membership Validation Listener -- [ ] Re-register commands after any relevant config changes +- [ ] Re-synchronise commands after any relevant config changes **(see above)** - [x] Flag for checking completeness of configuration for a guild. > - [x] Function for checking configs for completeness ## Event Listeners @@ -40,10 +44,24 @@ ## Commands - [ ] Configure Bot function and sub commands +> - [x] botrole (role group) +> - [ ] committeerole (role group) +> - [x] modchannel (channel group) +> - [x] help channel (channel group) +> - [x] signup channel (channel group) +> - [ ] newcomer role (role group) +> - [ ] returning player role (role group) +> - [ ] student role (role group) +> - [ ] help notifications (notification group) +> - [ ] signup notifications (notification group) - [ ] Set up command permissions +> - [ ] Slash Commands +>> - [x] Admin Commands +>> - [ ] Game Management Commands +> - [x] Native Bot Commands - [ ] Migrate existing bot commands > - [ ] setupgame -> - [ ] ~~definebotrole~~ config +> - [x] ~~definebotrole~~ config > - [ ] deletegame > - [ ] reset > - [ ] migrate diff --git a/app/bot.py b/app/bot.py index 0da227c..02eea06 100644 --- a/app/bot.py +++ b/app/bot.py @@ -76,7 +76,7 @@ def setConfig(guild:discord.Guild): if guildStr not in conf: conf[guildStr] = {} gDict = conf[guildStr] - if 'channels' not in gDict or gDict['channels'] is not dict or None in gDict['channels']: + if 'channels' not in gDict or type(gDict['channels']) is not dict or None in gDict['channels']: gDict['channels'] = {} cDict = gDict['channels'] if 'mod' not in cDict: @@ -89,9 +89,9 @@ def setConfig(guild:discord.Guild): cDict['mod'] = t.id else: cDict['mod'] = guild.system_channel.id - if 'configured' not in gDict or gDict['configured'] is not bool: + if 'configured' not in gDict or type(gDict['configured']) is not bool: gDict['configured'] = False - if 'membership' not in gDict or gDict['membership'] is not dict or None in gDict['membership']: + if 'membership' not in gDict or type(gDict['membership']) is not dict or None in gDict['membership']: gDict['membership'] = {} if 'name' not in gDict or gDict['name'] != guild.name: gDict['name'] = guild.name @@ -99,7 +99,7 @@ def setConfig(guild:discord.Guild): gDict['owner'] = guild.owner_id if 'prefix' not in gDict: gDict['prefix'] = '-' - if 'roles' not in gDict or (type(gDict)['roles'] is not dict or None in gDict['roles']): + if 'roles' not in gDict or (type(gDict['roles']) is not dict or None in gDict['roles']): gDict['roles'] = {} rDict = gDict['roles'] if 'admin' not in rDict or (type(rDict['admin']) is not list or len(rDict['admin']) == 0 or None in rDict['admin']): @@ -107,7 +107,7 @@ def setConfig(guild:discord.Guild): for role in guild.roles: if not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator: rDict['admin'].append(role.id) - if 'timeslots' not in gDict or gDict['timeslots'] is not dict or None in gDict['timeslots']: + if 'timeslots' not in gDict or type(gDict['timeslots']) is not dict or None in gDict['timeslots']: gDict['timeslots'] = [] yaml_dump(conf, configFile) @@ -133,7 +133,8 @@ def checkConfig(guild:discord.Guild): for item in list(i.split("'")): if '[' in item or ']' in item: s.remove(item) - unconfigured.append('.'.join(s)) + if 'notifications' not in '.'.join(s): + unconfigured.append('.'.join(s)) if 'type_changes' in diff: for i in diff['type_changes']: s = i.split("'") @@ -155,6 +156,8 @@ def checkConfig(guild:discord.Guild): unconfigured.append(i) if 'meta' in unconfigured: unconfigured.remove('meta') + if 'initialised' in unconfigured: + unconfigured.remove('initialised') if 'configured' in unconfigured: unconfigured.remove('configured') output = list(set(unconfigured)) @@ -228,8 +231,9 @@ def unloadCogs(cogClass:str = 'all'): if cogfile.endswith('.py'): unloadCog(f'./{cogsDir}/{category}/{cogfile}') -loadCogs('dev') +loadCogs('devcommands') loadCogs('events') loadCogs('botcommands') +loadCogs('slashcommands') client.run(os.getenv('TEST_3_TOKEN')) \ No newline at end of file diff --git a/app/cogs/botcommands/prefix.py b/app/cogs/botcommands/prefix.py index 73157a0..05c3bea 100644 --- a/app/cogs/botcommands/prefix.py +++ b/app/cogs/botcommands/prefix.py @@ -9,7 +9,14 @@ from bot import configFile, yaml_load, yaml_dump class Prefix(commands.Cog, name='Server Command Prefix'): def __init__(self, client): self.client = client - + + #### Check if user is an administrator + async def cog_check(self, ctx): + for role in ctx.author.roles: + if role.permissions.administrator: + return True + return ctx.author.guild_permissions.administrator + @commands.command( name = 'changeprefix', aliases = ['prefix'], @@ -20,7 +27,7 @@ class Prefix(commands.Cog, name='Server Command Prefix'): conf = yaml_load(configFile) conf[str(ctx.guild.id)]['prefix'] = prefix.lower() yaml_dump(conf, configFile) - await ctx.send(f"`{self.client.user.name}`'s prefix for native bot commands has been changed to `{prefix}` for the guild `{ctx.guild.name}`.\n`Note: This will not affect /commands.`") + await ctx.send(f"```{self.client.user.name}'s prefix for native bot commands has been changed to `{prefix}` for the guild `{ctx.guild.name}`.\n\nNote: This will not affect /commands.```") def setup(client): client.add_cog(Prefix(client)) \ No newline at end of file diff --git a/app/cogs/dev/dev.py b/app/cogs/devcommands/dev.py similarity index 91% rename from app/cogs/dev/dev.py rename to app/cogs/devcommands/dev.py index a28773e..6db72e8 100644 --- a/app/cogs/dev/dev.py +++ b/app/cogs/devcommands/dev.py @@ -16,6 +16,13 @@ class Dev(commands.Cog, name='Developer Commands'): def __init__(self, client): self.client = client + #### Check if user is an administrator + async def cog_check(self, ctx): + for role in ctx.author.roles: + if role.permissions.administrator: + return True + return ctx.author.guild_permissions.administrator + @commands.command( name='debug', description='Toggles debug feature for the guild. Enter either `on` or `off`.', diff --git a/app/cogs/events/on_command_error.py b/app/cogs/events/on_command_error.py index a7df095..88c7526 100644 --- a/app/cogs/events/on_command_error.py +++ b/app/cogs/events/on_command_error.py @@ -16,7 +16,10 @@ class on_command_error(commands.Cog, name='On Command Error'): @commands.Cog.listener() async def on_command_error(self, ctx, error): if isinstance(error, discord.DiscordException): - if isinstance(error, commands.CheckFailure): + if isinstance(error, commands.CommandNotFound): + print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} entered an invalid command <{ctx.message.clean_content}> in the guild {ctx.guild.name}.') + await ctx.reply(f'```Error: This is not a valid command.```') + elif isinstance(error, commands.CheckFailure): print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} is not authorised to issue the command <{ctx.command.name}> in the guild {ctx.guild.name}.') await ctx.reply(f'```Error: You are not authorised to issue this command.```') else: diff --git a/app/cogs/events/on_ready.py b/app/cogs/events/on_ready.py index 1d132fa..552d8d8 100644 --- a/app/cogs/events/on_ready.py +++ b/app/cogs/events/on_ready.py @@ -7,7 +7,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features import logging # logger and handler -from bot import checkConfig, clearConfig, configFile, parseConfigCheck, setConfig, yaml_dump, yaml_load +from bot import checkConfig, clearConfig, configFile, parseConfigCheck, setConfig, yaml_dump, yaml_load, loadCogs, unloadCogs #### Actions for the Bot to take once it is ready to interact with commands. class on_ready(commands.Cog, name='On Ready Events'): @@ -17,6 +17,9 @@ class on_ready(commands.Cog, name='On Ready Events'): @commands.Cog.listener() async def on_ready(self): + async def test(self): + await self.client.get_channel(865348933022515220).send('Foo') + #### Create any missing config entries for guilds for guild in self.client.guilds: setConfig(guild) @@ -32,6 +35,12 @@ class on_ready(commands.Cog, name='On Ready Events'): conf = yaml_load(configFile) if not status: await guild.get_channel(conf[str(guild.id)]['channels']['mod']).send(f"```The Bot's configurations are incomplete for the guild `{guild.name}`. Some limited functions will still be available, but most features cannot be used until the configurations are complete.\n{parseConfigCheck(output)}\nYou can set these configuration values using the `/config` command.```") + + # #### Reload the /commands after the configs have finished loading. + # unloadCogs('slashcommands') + # loadCogs('slashcommands') + + await test(self) def setup(client): client.add_cog(on_ready(client)) \ No newline at end of file diff --git a/app/cogs/events/on_slash_command_error.py b/app/cogs/events/on_slash_command_error.py new file mode 100644 index 0000000..5153b16 --- /dev/null +++ b/app/cogs/events/on_slash_command_error.py @@ -0,0 +1,36 @@ +import yaml # YAML parser for Bot config files +import asyncio # Discord Py Dependency +import discord # Main Lib +from discord.ext import commands # Commands module +from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library +from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features +import logging +# logger and handler +from bot import configFile, yaml_load, yaml_dump + +#### Error Handler Event Listener for Slash Command Errors +class on_slash_command_error(commands.Cog, name='On Command Error'): + def __init__(self, client): + self.client = client + + @commands.Cog.listener() + async def on_slash_command_error(self, ctx:SlashContext, error): + if isinstance(error, Exception): + await ctx.send( + content='```Invalid Command: {error}```', + tts=True, + hidden=True, + delete_after=10, + ) + # if isinstance(error, commands.CommandNotFound): + # print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} entered an invalid command <{ctx.message.clean_content}> in the guild {ctx.guild.name}.') + # await ctx.reply(f'```Error: This is not a valid command.```') + # elif isinstance(error, commands.CheckFailure): + # print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} is not authorised to issue the command <{ctx.command.name}> in the guild {ctx.guild.name}.') + # await ctx.reply(f'```Error: You are not authorised to issue this command.```') + # else: + # print(f'User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} received error: "{error}" when attempting to issue command <{ctx.command.name}> in the guild {ctx.guild.name}.') + # await ctx.reply(f'```Error: {error}```') + +def setup(client): + client.add_cog(on_slash_command_error(client)) \ No newline at end of file diff --git a/app/cogs/slashcommands/config.py b/app/cogs/slashcommands/config.py index 4c30c0c..f351299 100644 --- a/app/cogs/slashcommands/config.py +++ b/app/cogs/slashcommands/config.py @@ -4,32 +4,163 @@ import asyncio # Discord Py Dependency import discord # Main Lib from discord.ext import commands # Commands module from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library -from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features +from discord_slash.utils.manage_commands import create_choice, create_option, create_permission # Slash Command features +from discord_slash.model import SlashCommandPermissionType + +from bot import configFile, yaml_load, yaml_dump ##### Configuration Cog class Configuration(commands.Cog): def __init__(self, client): self.client = client + guild_ids=[int(guildKey) for guildKey in yaml_load(configFile)] + permissions = {} + conf = yaml_load(configFile) + for guildStr in conf: + permissions[int(guildStr)] = [] + permissions[int(guildStr)].append(create_permission(id=conf[guildStr]['owner'],id_type=SlashCommandPermissionType.USER,permission=True)) + for admin in conf[guildStr]['roles']['admin']: + permissions[int(guildStr)].append(create_permission(id=admin,id_type=SlashCommandPermissionType.ROLE,permission=True)) + @cog_ext.cog_slash( - # base='botrole', - # subcommand_group='configure', - name='configure', - description='Parameter to define the role assigned to the dice bots.', - # base_description='Command to configure the various guild parameters.', - # subcommand_group_description='These are configuration commands to set up the various guild parameters.', - guild_ids=guild_ids - # options=[ - # create_option( - # name='botrole', - # description='The role that the dice bots are assigned in order to access the text channels.' - # type=8, - # required=True - # ) - # ] + name='hello', + description='Test command to see if registration works.', + guild_ids=guild_ids, + options=[ + create_option( + name='input', + description='Choose a phrase that best goes with the opening Hello', + option_type=3, + required=True, + choices=[ + create_choice( + name='...there', + value='there' + ), + create_choice( + name='...world!', + value='world' + ), + create_choice( + name='...beautiful!', + value='beautiful' + ) + ], + ) + ] ) - async def _configure(self, ctx:SlashContext, option): - await ctx.send(f'The `botrole` for the guild `{ctx.guild.name}` has been set to `{option}`.') + async def _hello(self, ctx:SlashContext, input): + await ctx.send(f'{input}') + + @cog_ext.cog_subcommand( + base='config', + subcommand_group='role', + name='bot', + description='Designate the role on the guild which the dice bots use to access game text channels.', + base_description='Commands for configuring the various parameters of the Guild', + base_default_permission=False, + base_permissions=permissions, + subcommand_group_description='Designates the various key command roles for the guild.', + guild_ids=guild_ids, + options=[ + create_option( + name='role', + description='The role assigned to dice bots to access game channels.', + option_type=8, + required=True + ) + ] + ) + async def _config_role_bot(self, ctx:SlashContext, role:discord.Role): + conf = yaml_load(configFile) + if 'roles' not in conf[str(ctx.guild.id)]: + conf[str(ctx.guild.id)]['roles'] = {} + conf[str(ctx.guild.id)]['roles']['bots'] = int(role.id) + yaml_dump(conf, configFile) + await ctx.send(f'```The `botrole` for the guild `{ctx.guild.name}` has been set to `{role.name}`.```') + + @cog_ext.cog_subcommand( + base='config', + subcommand_group='channel', + name='signup', + description='Designate the channel where members can post their sign-up confirmation.', + # base_description='Commands for configuring the various parameters of the Guild', + # base_default_permission=False, + # base_permissions=permissions, + subcommand_group_description='Designates the various key Bot channels for the guild.', + guild_ids=guild_ids, + options=[ + create_option( + name='channel', + description='The channel assigned for members to verify their membership.', + option_type=7, + required=True + ) + ] + ) + async def _config_channel_signup(self, ctx:SlashContext, channel:discord.TextChannel): + conf = yaml_load(configFile) + if 'channels' not in conf[str(ctx.guild.id)]: + conf[str(ctx.guild.id)]['channels'] = {} + conf[str(ctx.guild.id)]['channels']['signup'] = int(channel.id) + yaml_dump(conf, configFile) + await ctx.send(f'```The `signup` channel for the guild `{ctx.guild.name}` has been set to `{channel.name}`.```') + + @cog_ext.cog_subcommand( + base='config', + subcommand_group='channel', + name='modlog', + description='Designate the channel for bot to post notifications for the admins.', + # base_description='Commands for configuring the various parameters of the Guild', + # base_default_permission=False, + # base_permissions=permissions, + # subcommand_group_description='Designates the various key Bot channels for the guild.', + guild_ids=guild_ids, + options=[ + create_option( + name='channel', + description='The channel assigned for moderation notifications.', + option_type=7, + required=True + ) + ] + ) + async def _config_channel_mod(self, ctx:SlashContext, channel:discord.TextChannel): + conf = yaml_load(configFile) + if 'channels' not in conf[str(ctx.guild.id)]: + conf[str(ctx.guild.id)]['channels'] = {} + conf[str(ctx.guild.id)]['channels']['mod'] = int(channel.id) + yaml_dump(conf, configFile) + await ctx.send(f'```The `moderation log` channel for the guild `{ctx.guild.name}` has been set to `{channel.name}`.```') + + @cog_ext.cog_subcommand( + base='config', + subcommand_group='channel', + name='help', + description='Designate the hel channel which the bot will monitor for member queries.', + # base_description='Commands for configuring the various parameters of the Guild', + # base_default_permission=False, + # base_permissions=permissions, + # subcommand_group_description='Designates the various key Bot channels for the guild.', + guild_ids=guild_ids, + options=[ + create_option( + name='channel', + description='The channel monitored by the Bot for help queries.', + option_type=7, + required=True + ) + ] + ) + async def _config_channel_help(self, ctx:SlashContext, channel:discord.TextChannel): + conf = yaml_load(configFile) + if 'channels' not in conf[str(ctx.guild.id)]: + conf[str(ctx.guild.id)]['channels'] = {} + conf[str(ctx.guild.id)]['channels']['help'] = int(channel.id) + yaml_dump(conf, configFile) + await ctx.send(f'```The `help` channel for the guild `{ctx.guild.name}` has been set to `{channel.name}`.```') + def setup(client): client.add_cog(Configuration(client)) \ No newline at end of file diff --git a/app/data/config.yml b/app/data/config.yml index 2797aec..4fb638a 100644 --- a/app/data/config.yml +++ b/app/data/config.yml @@ -1,6 +1,6 @@ '864651943820525609': channels: - mod: 865348933022515220 + mod: 865662560225067018 configured: false membership: {} name: Test @@ -12,4 +12,5 @@ roles: admin: - 864661232005939280 + bots: 864661167297527830 timeslots: [] diff --git a/app/data/config_blueprint.yml b/app/data/config_blueprint.yml index 2284a9f..1ddff44 100644 --- a/app/data/config_blueprint.yml +++ b/app/data/config_blueprint.yml @@ -28,3 +28,4 @@ guild_id_string: notifications: help: true signup: true + initialised: true diff --git a/app/debug/debug.py b/app/debug/debug.py index ca62ff1..44ccfe8 100644 --- a/app/debug/debug.py +++ b/app/debug/debug.py @@ -26,7 +26,7 @@ class Debug(commands.Cog, name='Debug Commands'): async def _reload(self, ctx, cog_category: str='all'): unloadCogs(cog_category) loadCogs(cog_category) - await ctx.reply(f'`{cog_category}` cogs have been reloaded.') + await ctx.reply(f'````{cog_category}` cogs have been reloaded.```') @commands.command( name='unloadcogs', @@ -36,7 +36,7 @@ class Debug(commands.Cog, name='Debug Commands'): async def _unloadcogs(self, ctx, cog_category: str='all'): unloadCogs(cog_category) loadCogs(cog_category) - await ctx.reply(f'`{cog_category}` cogs have been unloaded.') + await ctx.reply(f'````{cog_category}` cogs have been unloaded.```') @commands.command( name='loadcogs', @@ -46,11 +46,11 @@ class Debug(commands.Cog, name='Debug Commands'): async def _loadcogs(self, ctx, cog_category: str='all'): unloadCogs(cog_category) loadCogs(cog_category) - await ctx.reply(f'`{cog_category}` cogs have been loaded.') + await ctx.reply(f'````{cog_category}` cogs have been loaded.```') @commands.command( name='deletecommands', - aliases=['delallcommands','deleteslashcommands'], + aliases=['delallcommands','deleteslashcommands','clearcommands','clearslashcommands'], description='Deletes all the public and guild slash commands registered by the bot.', brief='Delets all slash commands' ) @@ -65,7 +65,7 @@ class Debug(commands.Cog, name='Debug Commands'): bot_token=os.getenv('TEST_3_TOKEN'), guild_ids=[ int(g) for g in yaml_load(configFile)] ) - await ctx.reply('All slash commands have been deleted.') + await ctx.reply('```All slash commands have been deleted.```') @commands.command( name='retrievecommands', @@ -76,6 +76,7 @@ class Debug(commands.Cog, name='Debug Commands'): async def _retrievecommands(self, ctx:commands.Context): c = await utils.manage_commands.get_all_commands(self.client.user.id,os.getenv('TEST_3_TOKEN'),guild_id=ctx.guild.id) print(c) + await ctx.reply(f'```All registered `/commands` have been fetched and sent to the Python console.```') @commands.command( name='clearconfig', @@ -87,7 +88,7 @@ class Debug(commands.Cog, name='Debug Commands'): conf = yaml_load(configFile) for key in list(conf): clearConfig(key) - await ctx.reply(f'Config entries have been cleared.') + await ctx.reply(f'```Config entries for unknown guilds have been cleared.```') @commands.command( name='setconfig', @@ -97,7 +98,7 @@ class Debug(commands.Cog, name='Debug Commands'): ) async def _setconfig(self, ctx:commands.Context): setConfig(ctx.guild) - await ctx.reply(f'Config entry has been added for guild `{ctx.guild.name}`.') + await ctx.reply(f'```Config entry has been added for guild `{ctx.guild.name}`.```') def setup(client): client.add_cog(Debug(client)) \ No newline at end of file