From ede87e799c1a82ffe4ab78ddd10f7690aa9c5cbc Mon Sep 17 00:00:00 2001 From: Vivek Santayana Date: Wed, 21 Jul 2021 11:49:29 +0100 Subject: [PATCH] Wrote player commands, still not tested. --- TODO.md | 32 +-- app/bot.py | 9 +- app/cogs/slashcommands/config.py | 80 +++--- app/cogs/slashcommands/game.py | 18 -- .../secondary/edit_membership.py | 8 +- .../secondary/game_management.py | 26 +- .../slashcommands/secondary/game_setup.py | 26 +- .../secondary/manipulate_timeslots.py | 8 +- .../secondary/player_commands.py | 241 ++++++++++++++++++ app/data/categories.yml | 3 +- 10 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 app/cogs/slashcommands/game.py create mode 100644 app/cogs/slashcommands/secondary/player_commands.py diff --git a/TODO.md b/TODO.md index 8d10a63..10e02ed 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,8 @@ - [ ] ~~'Register Commands' Function~~ - [x] Infer Permissions from Config - [x] Dynamic Command Prefixes -- [ ] Infer Games from Server Structure +- [ ] Infer current games from Server Structure +**Create a separate cog to do this instead of having a migrate command, install the cog temporarily and remove the cog once migration is done.** - [ ] Re-enable logging - [x] Delete Dev/Test Functions - [x] Error handlers @@ -33,12 +34,13 @@ > - [ ] Message Receive Listener > - [ ] Membership Validation Listener - [ ] Re-synchronise commands after any relevant config changes **(see above)** -> - [ ] Role Delete (member, admin) -> - [ ] Channel delete (notifications, logs) +> - [ ] Role Delete (member, admin, game) +> - [ ] Channel delete (notifications, logs, game text channel) +> - [ ] Category delete (games) - [x] Flag for checking completeness of configuration for a guild. > - [x] Function for checking configs for completeness -- [ ] Synchronise roles on game channel updates -> - [ ] Exception to event listener to prevent circularity +- [ ] Synchronise game channel on role updates +> - [ ] Exception to event listener to prevent circularity `unsure what this means` ## Event Listeners @@ -60,25 +62,25 @@ > - [x] help notifications (notification group) > - [x] signup notifications (notification group) - [x] Set up timeslots -- [ ] Delete timeslots +- [x] Delete timeslots > - [x] Base command -> - [ ] ~~Delete all games with the timeslot~~ +> - [x] ~~Delete all games with the timeslot~~ Do the opposite: block deleting timeslots with existing games. - [x] List timeslots -- [ ] Set up command permissions -> - [ ] Slash Commands +- [x] Set up command permissions +> - [x] Slash Commands >> - [x] Admin Commands ->> - [ ] Game Management Commands +>> - [x] Game Management Commands > - [x] Native Bot Commands - [ ] Migrate existing bot commands > - [x] setupgame > - [x] ~~definebotrole~~ config > - [x] deletegame -> - [ ] reset -> - [ ] migrate -> - [ ] kickplayer -> - [ ] addplayer -> - [ ] leavegame +> - [x] ~~reset~~ purge +> - [ ] ~~migrate~~ **See above** +> - [x] ~~kickplayer~~ `/player remove` +> - [x] ~~addplayer~~ `/player add` +> - [x] ~~leavegame~~ `/player leave` > - [ ] Pitch command and sub-commands > > - [ ] run > > - [ ] clear diff --git a/app/bot.py b/app/bot.py index 096a353..95f0203 100644 --- a/app/bot.py +++ b/app/bot.py @@ -267,14 +267,15 @@ loadCogs('events') loadCogs('botcommands') loadCogs('slashcommands') if yaml_load(configFile): - if all([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): + if any([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): loadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py') - if all(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]): + if any(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]): loadCog(f'./{cogsDir}/slashcommands/secondary/game_setup.py') if yaml_load(lookupFile): - if all([len(x) > 0 for x in yaml_load(lookupFile).values()]): + if any([len(x) > 0 for x in yaml_load(lookupFile).values()]): loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') - if all([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]): + loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py') + if any([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]): loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py') client.run(os.getenv('TEST_3_TOKEN')) \ No newline at end of file diff --git a/app/cogs/slashcommands/config.py b/app/cogs/slashcommands/config.py index d64dff8..4463ee1 100644 --- a/app/cogs/slashcommands/config.py +++ b/app/cogs/slashcommands/config.py @@ -18,11 +18,11 @@ class Configuration(commands.Cog, name='Configuration Commands'): 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)) + for gStr in conf: + permissions[int(gStr)] = [] + permissions[int(gStr)].append(create_permission(id=conf[gStr]['owner'],id_type=SlashCommandPermissionType.USER,permission=True)) + for admin in conf[gStr]['roles']['admin']: + permissions[int(gStr)].append(create_permission(id=admin,id_type=SlashCommandPermissionType.ROLE,permission=True)) @cog_ext.cog_subcommand( base='config', @@ -101,19 +101,23 @@ class Configuration(commands.Cog, name='Configuration Commands'): hoist=True if key == 'committee' else None, ) 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'][key] = int(r.id) + guildStr = str(ctx.guild.id) + if 'roles' not in conf[guildStr]: + conf[guildStr]['roles'] = {} + conf[guildStr]['roles'][key] = int(r.id) yaml_dump(conf, configFile) await ctx.send(f'```The `{key}` role for the guild `{ctx.guild.name}` has been set to `{r.name}`.```\n{r.mention}',hidden=True) - if all(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]): - if all([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): + if any(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]): + if any([yaml_load(configFile)[x]['timeslots'] for x in yaml_load(configFile)]): + flag = False if self.client.get_cog('Game Setup') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/game_setup.py') - if all([x for x in yaml_load(lookupFile).values()]): + flag = True + if any([x for x in yaml_load(lookupFile).values()]): if self.client.get_cog('Game Management') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') - await self.client.slash.sync_all_commands() + flag = True + if flag: await self.client.slash.sync_all_commands() @cog_ext.cog_subcommand( base='config', @@ -181,9 +185,10 @@ class Configuration(commands.Cog, name='Configuration Commands'): reason=f'`/config channels` command issued by {ctx.author.display_name}' ) 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'][key] = int(c.id) + guildStr = str(ctx.guild.id) + if 'channels' not in conf[guildStr]: + conf[guildStr]['channels'] = {} + conf[guildStr]['channels'][key] = int(c.id) yaml_dump(conf, configFile) await ctx.send(f'```The `{key}` channel for the guild `{ctx.guild.name}` has been set to `{c.name}`.\n\nAll permission overrides for the channel have been reset. Remember to set the appropriate permissions for your guild.```\n{c.mention}',hidden=True) @@ -224,9 +229,10 @@ class Configuration(commands.Cog, name='Configuration Commands'): ) async def _config_notifications(self, ctx:SlashContext, channel:str, notifications:bool): conf = yaml_load(configFile) - if 'notifications' not in conf[str(ctx.guild.id)]: - conf[str(ctx.guild.id)]['notifications'] = {} - conf[str(ctx.guild.id)]['notifications'][channel] = notifications + guildStr = str(ctx.guild.id) + if 'notifications' not in conf[guildStr]: + conf[guildStr]['notifications'] = {} + conf[guildStr]['notifications'][channel] = notifications yaml_dump(conf, configFile) await ctx.send(f'```Notifications for posts in the `{channel}` channel for the guild `{ctx.guild.name}` have been set to `{notifications}`.```',hidden=True) @@ -260,25 +266,30 @@ class Configuration(commands.Cog, name='Configuration Commands'): if not key.isalnum(): await ctx.send(f'```Key value {key} is not a valid alphanumeric time code. Sanitising to `{sanitisedKey}`.```',hidden=True) conf = yaml_load(configFile) - if 'timeslots' not in conf[str(ctx.guild.id)]: - conf[str(ctx.guild.id)]['timeslots'] = {} - if sanitisedKey in conf[str(ctx.guild.id)]['timeslots']: - await ctx.send(f'```Key value {sanitisedKey} has already been defined for guild `{ctx.guild.name}` for `{conf[str(ctx.guild.id)]["timeslots"][sanitisedKey]}`. Please use the `remove` or `modify` sub-commands to amend it.```',hidden=True) + guildStr = str(ctx.guild.id) + if 'timeslots' not in conf[guildStr]: + conf[guildStr]['timeslots'] = {} + if sanitisedKey in conf[guildStr]['timeslots']: + await ctx.send(f'```Key value {sanitisedKey} has already been defined for guild `{ctx.guild.name}` for `{conf[guildStr]["timeslots"][sanitisedKey]}`. Please use the `remove` or `modify` sub-commands to amend it.```',hidden=True) return - conf[str(ctx.guild.id)]['timeslots'][sanitisedKey] = name + conf[guildStr]['timeslots'][sanitisedKey] = name yaml_dump(conf, configFile) await ctx.send(f'```Timeslot `{name}` with the key `{sanitisedKey}` has been added for the guild `{ctx.guild.name}`.```',hidden=True) - if all([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): + if any([yaml_load(configFile)[x]['timeslots'] for x in yaml_load(configFile)]): + flag = False if self.client.get_cog('Manipulate Timeslots') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py') - if all(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]): + flag = True + if any(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]): if self.client.get_cog('Game Setup') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/game_setup.py') + Flag = True if yaml_load(lookupFile): - if all([x for x in yaml_load(lookupFile).values()]): + if any([x for x in yaml_load(lookupFile).values()]): if self.client.get_cog('Game Management') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') - await self.client.slash.sync_all_commands() + Flag = True + if flag: await self.client.slash.sync_all_commands() @cog_ext.cog_subcommand( base='config', @@ -319,13 +330,14 @@ class Configuration(commands.Cog, name='Configuration Commands'): await ctx.send(f'```You have specified a role for `{name}` does not already exist but have also specified a role to assign. Please either assign a role if it exists, or leave it blank if does not.```',hidden=True) return conf = yaml_load(configFile) - if 'membership' not in conf[str(ctx.guild.id)]: - conf[str(ctx.guild.id)]['membership'] = [] + guildStr = str(ctx.guild.id) + if 'membership' not in conf[guildStr]: + conf[guildStr]['membership'] = [] if role is not None: - if role.id in conf[str(ctx.guild.id)]['membership']: + if role.id in conf[guildStr]['membership']: await ctx.send(f'```The role {name} has already been assigned to a membership type for guild `{ctx.guild.name}`. Please use the `remove` sub-command to delete it or assign a different role.```',hidden=True) return - if any([ctx.guild.get_role(m).name == name for m in conf[str(ctx.guild.id)]['membership']]): + if any([ctx.guild.get_role(m).name == name for m in conf[guildStr]['membership']]): await ctx.send(f'```The membership type {name} has already been assigned a role for guild `{ctx.guild.name}`. Please use the `remove` sub-command to delete the role or assign a different membership type.```',hidden=True) return if not role_exists: @@ -342,13 +354,13 @@ class Configuration(commands.Cog, name='Configuration Commands'): mentionable=False, reason=f'`/config membership add` command issued by {ctx.author.display_name}' ) - conf[str(ctx.guild.id)]['membership'].append(role.id) if role is not None else conf[str(ctx.guild.id)]['membership'].append(r.id) + conf[guildStr]['membership'].append(role.id) if role is not None else conf[guildStr]['membership'].append(r.id) yaml_dump(conf, configFile) await ctx.send(f'```Membership type `{role.name if role is not None else r.name}` has been registered for the guild `{ctx.guild.name}`.```',hidden=True) - if all([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]): + if any([yaml_load(configFile)[x]['membership'] for x in yaml_load(configFile)]): if self.client.get_cog('Edit Membership') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py') - await self.client.slash.sync_all_commands() + await self.client.slash.sync_all_commands() @cog_ext.cog_slash( name='test', diff --git a/app/cogs/slashcommands/game.py b/app/cogs/slashcommands/game.py deleted file mode 100644 index bfceebf..0000000 --- a/app/cogs/slashcommands/game.py +++ /dev/null @@ -1,18 +0,0 @@ -import os # OS Locations -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, create_permission # Slash Command features -from discord_slash.model import SlashCommandPermissionType - -from bot import configFile, yaml_load, yaml_dump - -##### Game Management Commands -class Game_Management(commands.Cog, name='Game Management Commands'): - def __init__(self, client): - self.client = client - -def setup(client): - client.add_cog(Game_Management(client)) \ No newline at end of file diff --git a/app/cogs/slashcommands/secondary/edit_membership.py b/app/cogs/slashcommands/secondary/edit_membership.py index 7e053ad..647c511 100644 --- a/app/cogs/slashcommands/secondary/edit_membership.py +++ b/app/cogs/slashcommands/secondary/edit_membership.py @@ -8,7 +8,7 @@ from discord_slash.utils.manage_commands import create_choice, create_option, cr from discord_slash.model import SlashCommandPermissionType from discord_slash.client import SlashCommand -from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog +from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog #### Separate cog to remove and modify membership registrations that is reloaded if timeslots are added or removed @@ -20,7 +20,7 @@ class EditMembership(commands.Cog, name='Edit Membership'): #### N.B.: if there are no guilds with any membership types, then this will throw an exception. #### The solution I have implemented is that this will be classed as a 'secondary' cog: it will not be loaded by default, and will only be loaded if at least one guild has a membership role registered. #### If the deletion of membership roles removes memberships from all guilds, it will unload the cog and delete the commands until a new membership role is defined. - guild_ids=[int(guildKey) for guildKey in yaml_load(configFile) if len(yaml_load(configFile)[guildKey]['membership']) > 0] + guild_ids=[int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['membership']] conf = yaml_load(configFile) permissions = {} for guildID in guild_ids: @@ -60,7 +60,7 @@ class EditMembership(commands.Cog, name='Edit Membership'): if not any([x['membership'] for x in yaml_load(configFile).values()]): unloadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py') await self.client.slash.sync_all_commands() - elif len(conf[str(ctx.guild.id)]['membership']) > 0: + elif conf[str(ctx.guild.id)]['membership']: output = f'Role `{role.name}` is not a registered membership role in the guild `{ctx.guild.name}`. Please select a valid membership role.\n\n Eligible roles are:\n' for m in conf[str(ctx.guild.id)]['membership']: output = ''.join([output, f'\n{ctx.guild.get_role(m).name}']) @@ -83,7 +83,7 @@ class EditMembership(commands.Cog, name='Edit Membership'): conf = yaml_load(configFile) if 'membership' not in conf[str(ctx.guild.id)]: conf[str(ctx.guild.id)]['membership'] = {} - if len(conf[str(ctx.guild.id)]['membership']) > 0: + if conf[str(ctx.guild.id)]['membership']: output = f'The following membership types have been registered for the guild {ctx.guild.name}:\n' for m in conf[str(ctx.guild.id)]['membership']: output = ''.join([output, f'\n{ctx.guild.get_role(m).name}']) diff --git a/app/cogs/slashcommands/secondary/game_management.py b/app/cogs/slashcommands/secondary/game_management.py index e98a893..812ed85 100644 --- a/app/cogs/slashcommands/secondary/game_management.py +++ b/app/cogs/slashcommands/secondary/game_management.py @@ -11,7 +11,7 @@ from discord_slash.utils.manage_components import create_select, create_select_o import re -from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile +from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile class GameManagement(commands.Cog, name='Game Management'): def __init__(self, client): @@ -49,6 +49,7 @@ class GameManagement(commands.Cog, name='Game Management'): data = yaml_load(dataFile) gms = yaml_load(gmFile) lookup = yaml_load(lookupFile) + categories = yaml_load(categoriesFile) guildStr = str(ctx.guild.id) rStr = str(game_role.id) if rStr not in lookup[guildStr]: @@ -68,6 +69,7 @@ class GameManagement(commands.Cog, name='Game Management'): for v in ctx.guild.voice_channels: if v.category == c: await v.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') + del categories[guildStr][c.id] await c.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') lookup[guildStr].pop(rStr, None) gm_m = await ctx.guild.fetch_member(gm) @@ -84,8 +86,10 @@ class GameManagement(commands.Cog, name='Game Management'): yaml_dump(lookup, lookupFile) yaml_dump(data, dataFile) yaml_dump(gms, gmFile) + yaml_dump(categories, categoriesFile) if not any([x for x in yaml_load(lookupFile).values()]): unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') + if self.client.get_cog('Player Commands') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py') await self.client.slash.sync_all_commands() @cog_ext.cog_subcommand( @@ -176,6 +180,7 @@ class GameManagement(commands.Cog, name='Game Management'): data = yaml_load(dataFile) gms = yaml_load(gmFile) lookup = yaml_load(lookupFile) + categories = yaml_load(categoriesFile) guildStr = str(ctx.guild.id) r = game_role rStr = str(r.id) @@ -189,7 +194,7 @@ class GameManagement(commands.Cog, name='Game Management'): data[guildStr][time] = {} # Command Validation Checks if rStr not in lookup[guildStr]: - await ctx.send(f'```This is not a valid game role. Please mention a role that is associated with a game.```') + await ctx.send(f'```This is not a valid game role. Please mention a role that is associated with a game.```',hidden=True) return if timeslot is not None: if time not in conf[guildStr]['timeslots']: @@ -363,14 +368,15 @@ class GameManagement(commands.Cog, name='Game Management'): 'game_title': game_title, 'text_channel': t.id } - if guildStr not in gms: - gms[guildStr] = {} - if str(gm.id) not in gms[guildStr]: - gms[guildStr][str(gm.id)] = [] + if guildStr not in gms: gms[guildStr] = {} + if str(gm.id) not in gms[guildStr]: gms[guildStr][str(gm.id)] = [] gms[guildStr][str(gm.id)].append(r.id) + if guildStr not in categories: categories[guildStr] = {} + categories[guildStr][str(c.id)] = r.id yaml_dump(data,dataFile) yaml_dump(lookup,lookupFile) yaml_dump(gms,gmFile) + yaml_dump(categories, categoriesFile) @cog_ext.cog_subcommand( base='game', @@ -383,7 +389,7 @@ class GameManagement(commands.Cog, name='Game Management'): # subcommand_group_description='Adds a time slot available to the channel for games.', guild_ids=guild_ids, ) - async def _purge( + async def _game_purge( self, ctx:SlashContext ): @@ -401,6 +407,7 @@ class GameManagement(commands.Cog, name='Game Management'): r = discord.utils.find(lambda x: x.id == g['role'], ctx.guild.roles) for x in c.channels: await x.delete(reason=f'/game purge command issued by `{ctx.author.display_name}`') + del categories[guildStr][str(c.id)] await c.delete(reason=f'/game purge command issued by `{ctx.author.display_name}`') await r.delete(reason=f'/game purge command issued by `{ctx.author.display_name}`') gms[guildStr][str(g['gm'])].remove(r.id) @@ -478,6 +485,7 @@ class GameManagement(commands.Cog, name='Game Management'): hidden=True ) else: + await ctx.channel.trigger_typing() await purgeGames(ctx=ctx, timeslot=timeslot) await ctx.send( content = f'```All games for time slot `{conf[guildStr]["timeslots"][timeslot]}` have been purged.```', @@ -487,6 +495,10 @@ class GameManagement(commands.Cog, name='Game Management'): yaml_dump(lookup,lookupFile) yaml_dump(data,dataFile) yaml_dump(categories,categoriesFile) + if not any([x for x in yaml_load(lookupFile).values()]): + unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') + if self.client.get_cog('Player Commands') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py') + await self.client.slash.sync_all_commands() def setup(client): diff --git a/app/cogs/slashcommands/secondary/game_setup.py b/app/cogs/slashcommands/secondary/game_setup.py index 0ea371b..4077516 100644 --- a/app/cogs/slashcommands/secondary/game_setup.py +++ b/app/cogs/slashcommands/secondary/game_setup.py @@ -10,7 +10,7 @@ from discord_slash.client import SlashCommand import typing import re -from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile, lookupFile, gmFile, loadCog +from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, loadCog, categoriesFile #### Game Role and Channel Setup Command @@ -20,7 +20,7 @@ class GameSetup(commands.Cog, name='Game Setup'): conf=yaml_load(configFile) permissions={} - guild_ids = list(set.union(set([int(guildKey) for guildKey in yaml_load(configFile) if len(yaml_load(configFile)[guildKey]['timeslots']) > 0]),set([int(guildKey) for guildKey in yaml_load(configFile) if type(yaml_load(configFile)[guildKey]['roles']['bot']) is int]))) + guild_ids = list(set.intersection(set([int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['timeslots']]),set([int(guildKey) for guildKey in yaml_load(configFile) if 'bot' in yaml_load(configFile)[guildKey]['roles'] and type(yaml_load(configFile)[guildKey]['roles']['bot']) is int]))) for guildID in guild_ids: permissions[guildID] = [] permissions[guildID].append(create_permission(id=conf[str(guildID)]['owner'],id_type=SlashCommandPermissionType.USER,permission=True)) @@ -109,6 +109,7 @@ class GameSetup(commands.Cog, name='Game Setup'): data = yaml_load(dataFile) gms = yaml_load(gmFile) lookup = yaml_load(lookupFile) + categories = yaml_load(categoriesFile) guildStr = str(ctx.guild.id) time = re.sub(r"\W+",'', timeslot[:9].lower()) if 'roles' not in conf[guildStr]: @@ -274,19 +275,26 @@ class GameSetup(commands.Cog, name='Game Setup'): 'game_title': game_title, 'text_channel': t.id } - if guildStr not in gms: - gms[guildStr] = {} - if str(gm.id) not in gms[guildStr]: - gms[guildStr][str(gm.id)] = [] + if guildStr not in gms: gms[guildStr] = {} + if str(gm.id) not in gms[guildStr]: gms[guildStr][str(gm.id)] = [] + if str(guildStr) not in categories: categories[guildStr] = {} gms[guildStr][str(gm.id)].append(r.id) + if guildStr not in categories: categories[guildStr] = {} + categores[guildStr][str(c.id)] = r.id yaml_dump(data,dataFile) yaml_dump(lookup,lookupFile) yaml_dump(gms,gmFile) - if all([x for x in yaml_load(lookupFile).values()]): + yaml_dump(categories,categoriesFile) + # Enable the Game Management and Player commands + if any([x for x in yaml_load(lookupFile).values()]): + flag = False if self.client.get_cog('Game Management') is None: loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') - await self.client.slash.sync_all_commands() - # Enable the Game Management commands + flag = True + if self.client.get_cog('Player Commands') is None: + loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py') + flag = True + if flag: await self.client.slash.sync_all_commands() def setup(client): client.add_cog(GameSetup(client)) \ No newline at end of file diff --git a/app/cogs/slashcommands/secondary/manipulate_timeslots.py b/app/cogs/slashcommands/secondary/manipulate_timeslots.py index 8869239..547b880 100644 --- a/app/cogs/slashcommands/secondary/manipulate_timeslots.py +++ b/app/cogs/slashcommands/secondary/manipulate_timeslots.py @@ -22,7 +22,7 @@ class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'): #### N.B.: if there are no guilds with any timeslots, then this will throw an exception. #### The solution I have implemented is that this will be classed as a 'secondary' cog: it will not be loaded by default, and will only be loaded if at least one guild has a timeslot configured. #### If the deletion of timeslots removes timeslots from all guilds, it will unload the cog and delete the commands until a new timeslot is defined. - guild_ids=[int(guildKey) for guildKey in yaml_load(configFile) if len(yaml_load(configFile)[guildKey]['timeslots']) > 0] + guild_ids=[int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['timeslots']] conf = yaml_load(configFile) permissions = {} for guildID in guild_ids: @@ -89,7 +89,7 @@ class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'): await ctx.send(f'```Timeslot {conf[guildStr]["timeslots"][timeslot]} with the key `{timeslot}` has been deleted for the guild `{ctx.guild.name}`.```',hidden=True) conf[guildStr]['timeslots'].pop(timeslot, None) yaml_dump(conf, configFile) - if not all([x['timeslots'] for x in yaml_load(configFile).values()]): + if not any([x['timeslots'] for x in yaml_load(configFile).values()]): unloadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py') if self.client.get_cog('Game Management') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') @@ -133,7 +133,7 @@ class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'): await ctx.send(f'```Timeslot {conf[guildStr]["timeslots"][key]} with the key `{key}` has been renamed to {name} for the guild `{ctx.guild.name}`.```',hidden=True) conf[guildStr]['timeslots'][key] = name yaml_dump(conf, configFile) - elif len(conf[guildStr]['timeslots']) > 0: + elif conf[guildStr]['timeslots']: output = f'```Timeslot `{key}` was not found in the guild `{ctx.guild.name}`. Please enter a valid key.\n\n Available timeslots are:\n(key): (timeslot name)' for c in conf[guildStr]['timeslots']: output = ''.join([output, f'\n {c}: {conf[guildStr]["timeslots"][c]}']) @@ -157,7 +157,7 @@ class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'): guildStr = str(ctx.guild.id) if 'timeslots' not in conf[guildStr]: conf[guildStr]['timeslots'] = {} - if len(conf[guildStr]['timeslots']) > 0: + if conf[guildStr]['timeslots']: output = f'```The following timeslots have been configured for the guild {ctx.guild.name}:\n(key): (timeslot name)' for c in conf[guildStr]['timeslots']: output = ''.join([output, f'\n {c}: {conf[guildStr]["timeslots"][c]}']) diff --git a/app/cogs/slashcommands/secondary/player_commands.py b/app/cogs/slashcommands/secondary/player_commands.py new file mode 100644 index 0000000..59734d4 --- /dev/null +++ b/app/cogs/slashcommands/secondary/player_commands.py @@ -0,0 +1,241 @@ +import os # OS Locations +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, create_permission # Slash Command features +from discord_slash.model import SlashCommandPermissionType + +from bot import configFile, yaml_load, yaml_dump, lookupFile, dataFile, gmFile, categoriesFile + +##### Game Management Commands +class PlayerCommands(commands.Cog, name='Player Commands'): + def __init__(self, client): + self.client = client + + conf = yaml_load(configFile) + lookup = yaml_load(lookupFile) + data = yaml_load(dataFile) + gms = yaml_load(gmFile) + categories = yaml_load(categoriesFile) + guild_ids= [ int(x) for x in list(lookup)] + + @cog_ext.cog_subcommand( + base='player', + name='add', + description='Add a player to a game.', + base_description='Commands to manage players in a game.', + base_default_permission=True, + guild_ids = guild_ids, + options = [ + create_option( + name='player', + description='The player you want to add to the game.', + option_type=6, + required=True + ), + create_option( + name='game', + description='The role of the game you want to add the player to.', + option_type=8, + required=True + ) + ] + ) + async def _player_add( + self, + ctx:SlashContext, + player:discord.User, + game:discord.Role + ): + await ctx.channel.trigger_typing() + conf = yaml_load(configFile) + lookup = yaml_load(lookupFile) + data = yaml_load(dataFile) + gms = yaml_load(gmFile) + guildStr = str(ctx.guild.id) + rStr = str(game.id) + if rStr not in lookup[guildStr]: + await ctx.send(f'```Error: This is not a valid game role. Please mention a role that is associated with a game.```',hidden=True) + return + if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[guildStr]['roles']['admin']]) or ctx.author == ctx.guild.owner): + if not set(ctx.author.roles) & set([ctx.guild.get_role(int(x)) for x in gms[guildStr] if gms[guildStr][x]]): + await ctx.send(f'```Error: You are not authorised to issue this command. The command may only be issued by an administrator or by a GM.```',hidden=True) + return + if ctx.author.id != lookup[guildStr][rStr]['gm']: + await ctx.send(f'```Error: You are not authorised to issue this command. A player may only be added to a game by the GM or by an administrator.```',hidden=True) + return + if game in player.roles: + await ctx.send(f'```Error: Player `{player.display_name}` is already in the game {lookup[guildStr][rStr]["game_title"]}.```',hidden=True) + return + await player.add_roles(game, reason=f'`/player add` command issued by {ctx.author.display_name}`.') + t = lookup[rStr]['time'] + if type(data[guildStr][t][rStr]['current_players']) is not int: data[guildStr][t][rStr]['current_players'] = 1 + else: data[guildStr][t][rStr]['current_players'] += 1 + hm = lookup[rStr]['header_message'] + tc = discord.utils.find(lambda x: x.id == lookup[rStr]['text_message'],ctx.guild.channels) + if tc is not None: + p = await tc.pins() + if p: + header = discord.utils.find(lambda x: x.id == hm, p) + if header is not None: + text = header.content.split('\n') + for line in text: + if 'Current Players: ' in line: + line = f'Current Players: {str(data[guildStr][t][rStr]["current_players"])}' + break + await header.edit(content='\n'.join(text)) + else: + tPos = len(ctx.guild.channels) + cat = discord.utils.find(lambda x: x.id==lookup[guildStr][rStr]['category'], ctx.guild.categories) + if cat is not None: + for t in cat.text_channels: + if t.position <= tPos: + tc = t + tPos = t.position + await tc.send(f'```Player {player.display_name} has been added to the game by `{ctx.author.display_name}`.```\n{player.mention} | {game.mention}') + if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']: + c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels) + r = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['committee'], ctx.guild.roles) + await c.send(f'```Player `{player.display_name}` has been added to the game `{lookup[guildStr][rStr]["game_title"]}` by `{ctx.author.display_name}` via the `/player add` command.```') + yaml_dump(data,dataFile) + await ctx.send(content=f'```Player `{player.display_name}` has been added to the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True) + + @cog_ext.cog_subcommand( + base='player', + name='remove', + description='Remove a player from a game.', + # base_description='Commands to manage players in a game.', + base_default_permission=True, + guild_ids = guild_ids, + options = [ + create_option( + name='player', + description='The player you want to remove from the game.', + option_type=6, + required=True + ) + ] + ) + async def _player_remove( + self, + ctx:SlashContext, + player:discord.User + ): + await ctx.channel.trigger_typing() + conf = yaml_load(configFile) + lookup = yaml_load(lookupFile) + data = yaml_load(dataFile) + gms = yaml_load(gmFile) + categories = yaml_load(categoriesFile) + guildStr = str(ctx.guild.id) + if str(ctx.channel.category.id) not in categories[guildStr]: + await ctx.send(f'```Error: This command can only be issued in a text channel associated with a game.```', hidden=True) + return + game = categories[guildStr][str(ctx.channel.category.id)] + rStr = str(game.id) + if game not in player.roles: + await ctx.send(f'```Error: Player `{player.display_name}` is not in the game {lookup[guildStr][rStr]["game_title"]}.```',hidden=True) + return + if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[guildStr]['roles']['admin']]) or ctx.author == ctx.guild.owner): + if not set(ctx.author.roles) & set([ctx.guild.get_role(int(x)) for x in gms[guildStr] if gms[guildStr][x]]): + await ctx.send(f'```Error: You are not authorised to issue this command. The command may only be issued by an administrator or by a GM.```',hidden=True) + return + if ctx.author.id != lookup[guildStr][rStr]['gm']: + await ctx.send(f'```Error: You are not authorised to issue this command. A player may only be added to a game by the GM or by an administrator.```',hidden=True) + return + await player.remove_roles(game, reason=f'`/player remove` command issued by {ctx.author.display_name}`.') + t = lookup[rStr]['time'] + if type(data[guildStr][t][rStr]['current_players']) <= 1: data[guildStr][t][rStr]['current_players'] = None + else: data[guildStr][t][rStr]['current_players'] -= 1 + hm = lookup[rStr]['header_message'] + tc = discord.utils.find(lambda x: x.id == lookup[rStr]['text_message'],ctx.guild.channels) + if tc is not None: + p = await tc.pins() + if p: + header = discord.utils.find(lambda x: x.id == hm, p) + if header is not None: + text = header.content.split('\n') + for line in text: + if 'Current Players: ' in line: + line = f'Current Players: {str(data[guildStr][t][rStr]["current_players"]) if data[guildStr][t][rStr]["current_players"] is not None else str(0)}' + break + await header.edit(content='\n'.join(text)) + else: + tPos = len(ctx.guild.channels) + cat = ctx.channel.category + for t in cat.text_channels: + if t.position <= tPos: + tc = t + tPos = t.position + await tc.send(f'```Player {player.display_name} has been removed from the game by `{ctx.author.display_name}`.```\n{game.mention}') + if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']: + c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels) + r = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['committee'], ctx.guild.roles) + await c.send(f'```Player `{player.display_name}` has been removed from the game `{lookup[guildStr][rStr]["game_title"]}` by `{ctx.author.display_name}` via the `/player remove` command.```') + yaml_dump(data,dataFile) + await ctx.send(content=f'```Player `{player.display_name}` has been removed from the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True) + + @cog_ext.cog_subcommand( + base='player', + name='leave', + description='Leave a game.', + # base_description='Commands to manage players in a game.', + base_default_permission=True, + guild_ids = guild_ids + ) + async def _player_leave( + self, + ctx:SlashContext + ): + await ctx.channel.trigger_typing() + conf = yaml_load(configFile) + lookup = yaml_load(lookupFile) + data = yaml_load(dataFile) + gms = yaml_load(gmFile) + categories = yaml_load(categoriesFile) + guildStr = str(ctx.guild.id) + player = ctx.author + if str(ctx.channel.category.id) not in categories[guildStr]: + await ctx.send(f'```Error: This command can only be issued in a text channel associated with a game.```', hidden=True) + return + game = categories[guildStr][str(ctx.channel.category.id)] + rStr = str(game.id) + if game not in player.roles: + await ctx.send(f'```Error: Player `{player.display_name}` is not in the game {lookup[guildStr][rStr]["game_title"]}.```',hidden=True) + return + await player.remove_roles(game, reason=f'`/player leave` command issued by {ctx.author.display_name}`.') + t = lookup[rStr]['time'] + if type(data[guildStr][t][rStr]['current_players']) <= 1: data[guildStr][t][rStr]['current_players'] = None + else: data[guildStr][t][rStr]['current_players'] -= 1 + hm = lookup[rStr]['header_message'] + tc = discord.utils.find(lambda x: x.id == lookup[rStr]['text_message'],ctx.guild.channels) + if tc is not None: + p = await tc.pins() + if p: + header = discord.utils.find(lambda x: x.id == hm, p) + if header is not None: + text = header.content.split('\n') + for line in text: + if 'Current Players: ' in line: + line = f'Current Players: {str(data[guildStr][t][rStr]["current_players"]) if data[guildStr][t][rStr]["current_players"] is not None else str(0)}' + break + await header.edit(content='\n'.join(text)) + else: + tPos = len(ctx.guild.channels) + cat = ctx.channel.category + for t in cat.text_channels: + if t.position <= tPos: + tc = t + tPos = t.position + await tc.send(f'```Player {player.display_name} has left the game.```\n{game.mention}') + if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']: + c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels) + r = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['committee'], ctx.guild.roles) + await c.send(f'```Player `{player.display_name}` has left the game `{lookup[guildStr][rStr]["game_title"]}` via the `/player leave` command.```') + yaml_dump(data,dataFile) + await ctx.send(content=f'```You have left the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True) + +def setup(client): + client.add_cog(PlayerCommands(client)) \ No newline at end of file diff --git a/app/data/categories.yml b/app/data/categories.yml index 0967ef4..4cf0d48 100644 --- a/app/data/categories.yml +++ b/app/data/categories.yml @@ -1 +1,2 @@ -{} +'864651943820525609': + '866788661026488352': 866788659839238164