From 304dd9f9de96d8786f779a0041be979ca03be06c Mon Sep 17 00:00:00 2001 From: Vivek Santayana Date: Mon, 19 Jul 2021 15:30:04 +0100 Subject: [PATCH] Finessed game create and delete commands --- app/bot.py | 22 +- app/cogs/events/on_guild_role_delete.py | 2 +- app/cogs/slashcommands/config.py | 127 +++++++- .../secondary/edit_membership.py | 2 +- .../secondary/game_management.py | 209 +++---------- .../slashcommands/secondary/game_setup.py | 282 ++++++++++++++++++ .../secondary/manipulate_timeslots.py | 6 +- app/data/config.yml | 24 +- app/data/data.yml | 3 +- app/data/gm.yml | 1 + app/data/lookup.yml | 1 + app/debug/debug.py | 2 +- 12 files changed, 489 insertions(+), 192 deletions(-) create mode 100644 app/cogs/slashcommands/secondary/game_setup.py create mode 100644 app/data/gm.yml create mode 100644 app/data/lookup.yml diff --git a/app/bot.py b/app/bot.py index fa8b5ad..fa8721a 100644 --- a/app/bot.py +++ b/app/bot.py @@ -42,6 +42,12 @@ lookupFile = os.getenv('LOOKUP') if os.getenv('LOOKUP').endswith('.yml') or os.g if not os.path.exists(lookupFile): yaml_dump({},lookupFile) +# Locate or create GM lookup file. Same as above. +gmFile = os.getenv('GM') if os.getenv('GM').endswith('.yml') or os.getenv('GM').endswith('.yaml') else './data/gm.yml' + +if not os.path.exists(gmFile): + yaml_dump({},gmFile) + # Locate Cogs Directory cogsDir = 'cogs' @@ -114,7 +120,7 @@ def setConfig(guild:discord.Guild): 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 type(gDict['timeslots']) is not dict or None in gDict['timeslots']: - gDict['timeslots'] = [] + gDict['timeslots'] = {} yaml_dump(conf, configFile) def clearConfig(guildKey:str): @@ -254,9 +260,15 @@ loadCogs('controlcommands') loadCogs('events') loadCogs('botcommands') loadCogs('slashcommands') -if all([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): - loadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py') -if all([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]): - loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py') +if yaml_load(configFile): + if all([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)]): + loadCog(f'./{cogsDir}/slashcommands/secondary/game_setup.py') + if yaml_load(lookupFile): + if all([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/edit_membership.py') client.run(os.getenv('TEST_3_TOKEN')) \ No newline at end of file diff --git a/app/cogs/events/on_guild_role_delete.py b/app/cogs/events/on_guild_role_delete.py index 189da66..57abc5a 100644 --- a/app/cogs/events/on_guild_role_delete.py +++ b/app/cogs/events/on_guild_role_delete.py @@ -19,7 +19,7 @@ class on_guild_role_delete(commands.Cog, name='On Guild Role Delete Events'): conf = yaml_load(configFile) #### Bot will only respond if the role is not a bot-managed role, and the role is an admin role if role.id in conf[str(role.guild.id)]['roles']['admin']: - conf[str(role.guild.id)]['roles']['adminroles'].remove(role.id) + conf[str(role.guild.id)]['roles']['admin'].remove(role.id) yaml_dump(conf, configFile) #### If the role is one of the Admin roles and is deleted, updates the bot's config to delete that role, preventing unnecessary roles from accumulating. diff --git a/app/cogs/slashcommands/config.py b/app/cogs/slashcommands/config.py index 8942a77..1d87280 100644 --- a/app/cogs/slashcommands/config.py +++ b/app/cogs/slashcommands/config.py @@ -8,7 +8,7 @@ from discord_slash.utils.manage_commands import create_choice, create_option, cr from discord_slash.model import SlashCommandPermissionType import re -from bot import configFile, yaml_load, yaml_dump, loadCog, unloadCog, reloadCog, cogsDir, slash +from bot import configFile, yaml_load, yaml_dump, loadCog, unloadCog, reloadCog, cogsDir, slash, lookupFile ##### Configuration Cog class Configuration(commands.Cog, name='Configuration Commands'): @@ -63,21 +63,57 @@ class Configuration(commands.Cog, name='Configuration Commands'): ) ] ), + create_option( + name='role_exists', + description='Whether or not a role for this parameter already exists', + option_type=5, + required=True + ), create_option( name='role', description='The role assigned to the parameter.', option_type=8, - required=True + required=False ) ] ) - async def _config_roles(self, ctx:SlashContext, key:str, role:discord.Role): + async def _config_roles(self, ctx:SlashContext, key:str, role_exists:bool, role:discord.Role=None): + if role_exists and role is None: + await ctx.send(f'```If the role you want to assign to `{key}` already exists, you must assign it. If it does not exist, the Bot will create one.```') + return + if not role_exists and role is not None: + await ctx.send(f'```You have specified a role to assign to `{key}` but have also specified it does not exist. If a role already exists, indicate it. If it does not exist, do not assign a role.```') + return + r = role + if not role_exists: + r = await ctx.guild.create_role( + name=key, + permissions=discord.Permissions(administrator=True) if key == 'committee' else discord.Permissions().none(), + reason=f'`/config roles` command issued by {ctx.author.display_name}', + colour = discord.Colour.orange() if key == 'bot' else discord.Colour.blue() if key == 'committee' else discord.Colour.default(), + hoist=True if key == 'committee' else None, + ) + else: + await r.edit( + permissions=discord.Permissions(administrator=True) if key == 'committee' else discord.Permissions().none(), + reason=f'`/config roles` command issued by {ctx.author.display_name}', + colour = discord.Colour.orange() if key == 'bot' else discord.Colour.blue() if key == 'committee' else discord.Colour.default(), + 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(role.id) + conf[str(ctx.guild.id)]['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 `{role.name}`.```') + await ctx.send(f'```The `{key}` role for the guild `{ctx.guild.name}` has been set to `{r.name}`.```\n{r.mention}') + 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 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()]): + if self.client.get_cog('Game Management') is None: + loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') + await self.client.slash.sync_all_commands() @cog_ext.cog_subcommand( base='config', @@ -105,26 +141,51 @@ class Configuration(commands.Cog, name='Configuration Commands'): value='mod' ), create_choice( - name='SIgnup Channel', + name='Signup Channel', value='signup' ) ] ), + create_option( + name='channel_exists', + description='Does the channel for this parameter already exist?', + option_type=5, + required=True + ), create_option( name='channel', description='The channel assigned to the parameter.', option_type=7, - required=True + required=False ) ] ) - async def _config_channels(self, ctx:SlashContext, key:str, channel:discord.TextChannel): + async def _config_channels(self, ctx:SlashContext, key:str, channel_exists:bool, channel:discord.TextChannel=None): + if channel_exists and channel is None: + await ctx.send(f'```If the channel you want to assign to `{key}` already exists, you must assign it. If it does not exist, the Bot will create one.```') + return + if not channel_exists and channel is not None: + await ctx.send(f'```You have specified a channel to assign to `{key}` but have also specified it does not exist. If a channel already exists, indicate it. If it does not exist, do not assign a channel.```') + return + c = channel + if not channel_exists: + c = await ctx.guild.create_text_channel( + name=key, + overwrites={}, + reason=f'`/config channels` command issued by {ctx.author.display_name}' + ) + else: + await c.edit( + name=key, + overwrites={}, + 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(channel.id) + conf[str(ctx.guild.id)]['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 `{channel.name}`.```') + 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}') @cog_ext.cog_subcommand( base='config', @@ -210,6 +271,13 @@ class Configuration(commands.Cog, name='Configuration Commands'): if all([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): 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)]): + if self.client.get_cog('Game Setup') is None: + loadCog(f'./{cogsDir}/slashcommands/secondary/game_setup.py') + if yaml_load(lookupFile): + if all([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() @cog_ext.cog_subcommand( @@ -245,7 +313,7 @@ class Configuration(commands.Cog, name='Configuration Commands'): ) async def _config_membership_add(self, ctx:SlashContext, name:str, role_exists:bool, role:discord.Role=None): if role_exists and role is None: - await ctx.send(f'```If the role for membership type `{name}` already exists, you must assign it. If it has not been assigned, the Bot will create one.```') + await ctx.send(f'```If the role for membership type `{name}` already exists, you must assign it. If it does not exist, the Bot will create one.```') return if not role_exists and role is not None: 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.```') @@ -264,7 +332,8 @@ class Configuration(commands.Cog, name='Configuration Commands'): r = await ctx.guild.create_role( name=name, permissions=discord.Permissions(read_messages=True,use_slash_commands=True), - mentionable=False + mentionable=False, + reason=f'`/config membership add` command issued by {ctx.author.display_name}' ) if role is not None: await role.edit( @@ -281,5 +350,39 @@ class Configuration(commands.Cog, name='Configuration Commands'): loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py') await self.client.slash.sync_all_commands() + @cog_ext.cog_slash( + name='test', + description='testing options', + guild_ids=guild_ids, + options=[ + create_option( + name='required', + description='This option is mandatory', + option_type=3, + required=True + ), + create_option( + name='not_required', + description='This option is not mandatory', + option_type=3, + required=False + ), + create_option( + name='optional', + description='This option is optional', + option_type=3, + required=False + ) + ] + ) + async def _test( + self, + ctx:SlashContext, + required:str, + not_required:str=None, + optional:str=None + ): + await ctx.send(f'```You entered: name = {required}, not_required = {not_required}, and optional = {optional}```') + def setup(client): client.add_cog(Configuration(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 bc753a7..8a8c4c9 100644 --- a/app/cogs/slashcommands/secondary/edit_membership.py +++ b/app/cogs/slashcommands/secondary/edit_membership.py @@ -57,7 +57,7 @@ class EditMembership(commands.Cog, name='Edit Membership'): yaml_dump(conf, configFile) await ctx.send(f'```Membership type {role.name} has been deleted for the guild `{ctx.guild.name}`.```') await role.delete(reason=f'`/config membership remove` command issued by `{ctx.author.display_name}`.') - if not all([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]): + 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: diff --git a/app/cogs/slashcommands/secondary/game_management.py b/app/cogs/slashcommands/secondary/game_management.py index 70c659f..e47be39 100644 --- a/app/cogs/slashcommands/secondary/game_management.py +++ b/app/cogs/slashcommands/secondary/game_management.py @@ -9,170 +9,19 @@ from discord_slash.model import SlashCommandPermissionType from discord_slash.client import SlashCommand import re -from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile, lookupFile +from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile, lookupFile, gmFile -#### Game Role and Channel Management Commands - -class GameSetup(commands.Cog, name='Game Setup'): +class GameManagement(commands.Cog, name='Game Management'): def __init__(self, client): self.client = client - - 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]))) - permissions[guildID] = [] - permissions[guildID].append(create_permission(id=conf[str(guildID)]['owner'],id_type=SlashCommandPermissionType.USER,permission=True)) - for admin in conf[str(guildID)]['roles']['admin']: - permissions[guildID].append(create_permission(id=admin,id_type=SlashCommandPermissionType.ROLE,permission=True)) - @cog_ext.cog_subcommand( - base='game', - # subcommand_group='', - name='create', - description='Create a new game role and accompanying category, text, and voice channels.', - base_description='Commands for setting up and removing games on the Guild.', - base_default_permission=False, - base_permissions=permissions, - # subcommand_group_description='Adds a time slot available to the channel for games.', - guild_ids=guild_ids, - options=[ - create_option( - name='timeslot', - description='The timeslot code for when the game will run.', - option_type=3, - required=True - ), - create_option( - name='gm', - description='The person who will be running the game.', - option_type=6, - required=True - ), - create_option( - name='max_players', - description='The maximum number of players the GM can take.', - option_type=3, - required=True - ), - create_option( - name='min_players', - description='The minimum number of players the GM can take.', - option_type=3, - required=False - ), - create_option( - name='reserved_spaces', - description='The number of spaces the GM is reserving in the game.', - option_type=3, - required=False - ), - create_option( - name='game_title', - description='What the game is called.', - option_type=3, - required=True - ), - create_option( - name='system', - description='What system the game is using.', - option_type=3, - required=False - ), - create_option( - name='platform', - description='What platform the game will be running on.', - option_type=3, - required=False - ) - ] - ) - async def _game_create( - self, - ctx:SlashContext, - timeslot:str, - gm:discord.Member, - max_players:int, - game_title:str, - min_players:int=None, - reserved_spaces:int=None, - system:str=None, - platform:str=None - ): - await ctx.channel.trigger_typing() - conf = yaml_load(configFile) - data = yaml_load(dataFile) - lookup = yaml_load(lookupFile) - guildStr = str(ctx.guild.id) - time = re.sub(r"\W+",'', timeslot[:9].lower()) - if 'roles' not in conf[guildStr]: - conf[guildStr]['roles'] = {} - if 'bot' not in conf[guildStr]['roles']: - await ctx.send(f'```\`Bot` role for guild `{ctx.guild.name}` has not been defined. Cannot configure game.```') - return - if 'timeslots' not in conf[guildStr]: - conf[guildStr]['timeslots'] = {} - if time not in conf[guildStr]['timeslots']: - await ctx.send(f'```Time code `{timeslot}` is not recognised. Please enter a valid time code to register the game.```') - return - if guildStr not in data: - data[guildStr] = {} - if time not in data[guildStr]: - data[guildStr][time] = [] - r = await ctx.guild.create_role( - name=f'{time.upper}: {game_title}', - reason=f'/game create command issued by `{ctx.author.display_name}`', - mentionable=True, - permissions=discord.Permissions.none(), - ) - permissions = { - ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), - r: discord.PermissionOverwrite(read_messages=True), - ctx.guild.get_role(conf[guildStr]['roles']['bot']): discord.PermissionOverwrite(read_messages=True), - gm: discord.PermissionOverwrite(read_messages=True, manage_messages=True, manage_channels=True, manage_permissions=True,priority_speaker=True,move_members=True,mute_members=True,deafen_members=True) - } - c = await ctx.guild.create_category( - name=f'{time.upper}: {game_title}', - overwrites=permissions, - reason=f'/game create command issued by `{ctx.author.display_name}`' - ) - await c.create_voice_channel(name=f'voice: {game_title}', topic=f'Default voice channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.',reason=f'/game create command issued by `{ctx.author.display_name}`') - t = await c.create_text_channel( - name=f'text: {game_title}', - topic=f'Default text channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.', - reason=f'/game create command issued by `{ctx.author.display_name}`' - ) - await ctx.send(f'```Roles and channels for the game `{game_title}` have been created.```\n{r.mention}') - output = f'```Hello {gm.display_name}! Yout game channels for `{game_title}` have been created.\nYou can ping your players or edit the game settings by interacting with the game role through the Bot commands. Have fun!```\n' - output = ''.join([output,f'```Game Title: {game_title}\n']) - output = ''.join([output,f'GM: {gm.display_name}\n']) - output = ''.join([output,f'Time: {conf[guildStr]["timeslots"][time]}\n']) - output = ''.join([output,f'System: {system}\n'if system is not None else '']) - output = ''.join([output,f'Max Players: {max_players}\n']) - output = ''.join([output,f'Min Players: {min_players}\n'if min_players is not None else '']) - output = ''.join([output,f'Reserved Spaces: {reserved_spaces}\n' if reserved_spaces is not None else '']) - output = ''.join([output,f'Platform: {platform}```' if platform is not None else '```']) - o = await t.send(output) - await o.pin(reason=f'/game create command issued by `{ctx.author.display_name}`') - tDict = {} - tDict['game_title'] = game_title - tDict['gm'] = gm.id - tDict['max_players'] = max_players - tDict['min_players'] = min_players - tDict['reserved_spaces'] = reserved_spaces - tDict['system'] = system - tDict['platform'] = platform - tDict['role'] = r.id - tDict['category'] = c.id - data[guildStr][time].append(tDict) - if guildStr not in lookup: - lookup[guildStr] = {} - lookup[guildStr][str(r.id)] = {} - lookup[guildStr][str(r.id)]['category'] = c.id - lookup[guildStr][str(r.id)]['gm'] = gm.id - lookup[guildStr][str(r.id)]['time'] = time - yaml_dump(data,dataFile) - yaml_dump(lookup,lookupFile) - - ### Move delete, Modify, and Reset commands to a separate secondary cog to enable when games exist? + conf = yaml_load(configFile) + lookup = yaml_load(lookupFile) + data = yaml_load(dataFile) + + guild_ids= [ int(x) for x in list(lookup)] + +### Move delete, Modify, and Reset commands to a separate secondary cog to enable when games exist? @cog_ext.cog_subcommand( base='game', # subcommand_group='', @@ -196,28 +45,50 @@ class GameSetup(commands.Cog, name='Game Setup'): await ctx.channel.trigger_typing() conf = yaml_load(configFile) data = yaml_load(dataFile) + gms = yaml_load(gmFile) lookup = yaml_load(lookupFile) guildStr = str(ctx.guild.id) if str(game_role.id) 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.```') return + game_title = lookup[guildStr][str(game_role.id)]['game_title'] time = lookup[guildStr][str(game_role.id)]['time'] c = ctx.guild.get_channel(lookup[guildStr][str(game_role.id)]['category']) + gm = lookup[guildStr][str(game_role.id)]['gm'] + channelsFound = False for g in list(data[guildStr][time]): if game_role.id in g.values(): data[guildStr][time].remove(g) break - for t in ctx.guild.text_channels: - if t.category == c: - await t.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') - 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}`') - await c.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') + if c is not None: + channelsFound = True + for t in ctx.guild.text_channels: + if t.category == c: + await t.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') + 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}`') + await c.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') lookup[guildStr].pop(str(game_role.id), None) + gm_m = await ctx.guild.fetch_member(gm) + output = f'The game `{game_title}` for timeslot `{conf[guildStr]["timeslots"][time]}` and with GM `{gm_m.display_name}` has been deleted.' + if channelsFound: + output = ''.join([output,' All associated text, voice, and category channels have been deleted.']) + else: + output = ''.join([output,' No associated text, voice, or category channels were found. Please delete them manually if they still persist.']) + await ctx.send(f'```{output}```') await game_role.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') + gms[guildStr][str(gm)].remove(game_role.id) + if not gms[guildStr][str(gm)]: + gms[guildStr].pop(str(gm)) yaml_dump(lookup, lookupFile) yaml_dump(data, dataFile) + yaml_dump(gms, gmFile) + if not any([x for x in yaml_load(lookupFile).values()]): + unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py') + await self.client.slash.sync_all_commands() + + def setup(client): - client.add_cog(GameSetup(client)) \ No newline at end of file + client.add_cog(GameManagement(client)) \ No newline at end of file diff --git a/app/cogs/slashcommands/secondary/game_setup.py b/app/cogs/slashcommands/secondary/game_setup.py new file mode 100644 index 0000000..25efb35 --- /dev/null +++ b/app/cogs/slashcommands/secondary/game_setup.py @@ -0,0 +1,282 @@ +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 discord_slash.client import SlashCommand +import typing +import re + +from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile, lookupFile, gmFile, loadCog + +#### Game Role and Channel Setup Command + +class GameSetup(commands.Cog, name='Game Setup'): + def __init__(self, client): + self.client = client + + 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]))) + for guildID in guild_ids: + permissions[guildID] = [] + permissions[guildID].append(create_permission(id=conf[str(guildID)]['owner'],id_type=SlashCommandPermissionType.USER,permission=True)) + for admin in conf[str(guildID)]['roles']['admin']: + permissions[guildID].append(create_permission(id=admin,id_type=SlashCommandPermissionType.ROLE,permission=True)) + permissions[guildID] = [] + permissions[guildID].append(create_permission(id=conf[str(guildID)]['owner'],id_type=SlashCommandPermissionType.USER,permission=True)) + for admin in conf[str(guildID)]['roles']['admin']: + permissions[guildID].append(create_permission(id=admin,id_type=SlashCommandPermissionType.ROLE,permission=True)) + + @cog_ext.cog_subcommand( + base='game', + # subcommand_group='', + name='create', + description='Create a new game role and accompanying category, text, and voice channels.', + base_description='Commands for setting up and removing games on the Guild.', + base_default_permission=False, + base_permissions=permissions, + # subcommand_group_description='Adds a time slot available to the channel for games.', + guild_ids=guild_ids, + options=[ + create_option( + name='timeslot', + description='The timeslot code for when the game will run.', + option_type=3, + required=True + ), + create_option( + name='gm', + description='The person who will be running the game.', + option_type=6, + required=True + ), + create_option( + name='max_players', + description='The maximum number of players the GM can take.', + option_type=4, + required=True + ), + create_option( + name='game_title', + description='What the game is called.', + option_type=3, + required=True + ), + create_option( + name='min_players', + description='The minimum number of players the GM can take.', + option_type=4, + required=False + ), + create_option( + name='reserved_spaces', + description='The number of spaces the GM is reserving in the game.', + option_type=4, + required=False + ), + create_option( + name='system', + description='What system the game is using.', + option_type=3, + required=False + ), + create_option( + name='platform', + description='What platform the game will be running on.', + option_type=3, + required=False + ) + ] + ) + async def _game_create( + self, + ctx:SlashContext, + timeslot:str, + gm:discord.Member, + max_players:int, + game_title:str, + min_players: typing.Optional[int]=None, + reserved_spaces: typing.Optional[int]=None, + system:typing.Optional[str]= None, + platform:typing.Optional[str]=None + ): + await ctx.channel.trigger_typing() + conf = yaml_load(configFile) + data = yaml_load(dataFile) + gms = yaml_load(gmFile) + lookup = yaml_load(lookupFile) + guildStr = str(ctx.guild.id) + time = re.sub(r"\W+",'', timeslot[:9].lower()) + if 'roles' not in conf[guildStr]: + conf[guildStr]['roles'] = {} + if 'bot' not in conf[guildStr]['roles']: + await ctx.send(f'```\`Bot` role for guild `{ctx.guild.name}` has not been defined. Cannot configure game.```') + return + if 'timeslots' not in conf[guildStr]: + conf[guildStr]['timeslots'] = {} + if time not in conf[guildStr]['timeslots']: + await ctx.send(f'```Time code `{timeslot}` is not recognised. Please enter a valid time code to register the game. use `/config timeslots list` to get a list of valid time codes.```') + return + if min_players > max_players: + await ctx.send(f'```The minimum number of players cannot exceed the maximum number of players.```') + return + if reserved_spaces > max_players: + await ctx.send(f'```The number of reserved spaces cannot exceed the maximum number of players.```') + return + if guildStr not in lookup: + lookup[guildStr] = {} + if game_title in [x['game_title'] for x in lookup[guildStr].values()] and time in [x['time'] for x in lookup[guildStr].values()]: + await ctx.send(f'```Game `{game_title}` has already been created for the time slot `{conf[guildStr]["timeslots"][time]}`. Please avoud duplicates, or use the `modify` sub-command to edit the existing game.```') + return + if guildStr not in data: + data[guildStr] = {} + if time not in data[guildStr]: + data[guildStr][time] = [] + rExists, cExists = False, False + r = discord.utils.get(ctx.guild.roles, name=f'{time.upper()}: {game_title}') + if r is None: + r = await ctx.guild.create_role( + name=f'{time.upper()}: {game_title}', + reason=f'/game create command issued by `{ctx.author.display_name}`', + mentionable=True, + permissions=discord.Permissions.none(), + ) + else: + rExists = True + await r.edit( + mentionable=True, + permissions=discord.Permissions.none(), + reason=f'/game create command issued by `{ctx.author.display_name}`', + ) + await gm.add_roles(r) + permissions = { + ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + r: discord.PermissionOverwrite(read_messages=True), + ctx.guild.get_role(conf[guildStr]['roles']['bot']): discord.PermissionOverwrite(read_messages=True), + gm: discord.PermissionOverwrite( + read_messages=True, + manage_messages=True, + manage_channels=True, + manage_permissions=True, + priority_speaker=True, + move_members=True, + mute_members=True, + deafen_members=True + ) + } + c = discord.utils.get(ctx.guild.categories, name=f'{time.upper()}: {game_title}') + if c is None: + c = await ctx.guild.create_category( + name=f'{time.upper()}: {game_title}', + overwrites=permissions, + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + await c.create_voice_channel( + name=f'voice: {game_title}', + topic=f'Default voice channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.', + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + t = await c.create_text_channel( + name=f'text: {game_title}', + topic=f'Default text channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.', + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + else: + cExists= True + await c.edit( + overwrites=permissions, + reason=f'/game create command issued by `{ctx.author.display_name}`', + ) + tPos = len(ctx.guild.channels) + t = None + v = False + for tc in c.text_channels: + if tc.position <= tPos: + t, tPos = tc, tc.position + await t.edit( + sync_permissions=True, + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + for vc in c.voice_channels: + await vc.edit( + sync_permissions=True, + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + v = True + if t is None: + t = await c.create_text_channel( + name=f'text: {game_title}', + topic=f'Default text channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.', + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + else: + pins = await t.pins + if pins: + hm = discord.utils.find(lambda x: x.text.startswith('```Hello ' and 'Your game channels for ' in x.text and x.author.id == self.client.user.id), pins) + await hm.delete() + if not v: + await c.create_voice_channel( + name=f'voice: {game_title}', + topic=f'Default voice channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.', + reason=f'/game create command issued by `{ctx.author.display_name}`' + ) + result = f'Game `{game_title}` has been created for timeslot `{conf[guildStr]["timeslots"][time]}`` with GM `{gm.display_name}` and space for {max_players} players (with spaces {reserved_spaces} currently occupied).\n' + if not rExists: + result = ''.join([result,f'There was already a role that matched the game, so that role has been reconfigured.\n\nNote: Editing this role will synchronise changes with the game channels, and deleting the role will delete the game and all its data.\n\n']) + else: + result = ''.join([result,f'A role for the game has been created.\n']) + if not cExists: + result = ''.join([result,f'There was already a channel category that matched the game, so it has been reconfigured with the appropriate permissions and text and voice channels.\n\nNote: Editing this role will synchronise changes with the game channels, and deleting the role will delete the game and all its data.\n\n']) + else: + result = ''.join([result,f'A channel category with the appropriate text and voice channels has been created.\n']) + result = ''.join(['```',result,f'```\n{gm.mention} {r.mention} {t.mention}']) + output = f'```Hello {gm.display_name}! Your game channels for `{game_title}` have been created.\nYou can ping your players or edit the game settings by interacting with the game role through the Bot commands. Have fun!```\n' + output = ''.join([output,f'```Game Title: {game_title}\n']) + output = ''.join([output,f'GM: {gm.display_name}\n']) + output = ''.join([output,f'Time: {conf[guildStr]["timeslots"][time]}\n']) + output = ''.join([output,f'System: {system}\n'if system is not None else '']) + output = ''.join([output,f'Max Players: {max_players}\n']) + output = ''.join([output,f'Min Players: {min_players}\n'if min_players is not None else '']) + output = ''.join([output,f'Current Players: {reserved_spaces}\n' if reserved_spaces is not None else '0\n']) + output = ''.join([output,f'Platform: {platform}```' if platform is not None else '```']) + await ctx.send(result) + o = await t.send(output) + await o.pin(reason=f'/game create command issued by `{ctx.author.display_name}`') + gDict = {} + gDict['game_title'] = game_title + gDict['gm'] = gm.id + gDict['max_players'] = max_players + gDict['min_players'] = min_players + gDict['current_players'] = reserved_spaces + gDict['system'] = system + gDict['platform'] = platform + gDict['role'] = r.id + gDict['category'] = c.id + gDict['text_channel'] = t.id + gDict['header_message'] = o.id + data[guildStr][time].append(gDict) + lookup[guildStr][str(r.id)] = {} + lookup[guildStr][str(r.id)]['category'] = c.id + lookup[guildStr][str(r.id)]['gm'] = gm.id + lookup[guildStr][str(r.id)]['time'] = time + lookup[guildStr][str(r.id)]['game_title'] = game_title + 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) + yaml_dump(data,dataFile) + yaml_dump(lookup,lookupFile) + yaml_dump(gms,gmFile) + if all([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() + # Enable the Game Management 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 0cd03a3..03c3348 100644 --- a/app/cogs/slashcommands/secondary/manipulate_timeslots.py +++ b/app/cogs/slashcommands/secondary/manipulate_timeslots.py @@ -56,8 +56,12 @@ class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'): await ctx.send(f'```Timeslot {conf[str(ctx.guild.id)]["timeslots"][timeslot]} with the key `{timeslot}` has been deleted for the guild `{ctx.guild.name}`.```') conf[str(ctx.guild.id)]['timeslots'].pop(timeslot, None) yaml_dump(conf, configFile) - if not all([len(yaml_load(configFile)[x]['timeslots']) > 0 for x in yaml_load(configFile)]): + if not all([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') + if self.client.get_cog('Game Setup') is not None: + unloadCog(f'./{cogsDir}/slashcommands/secondary/game_setup.py') await self.client.slash.sync_all_commands() elif len(conf[str(ctx.guild.id)]['timeslots']) > 0: output = f'```Timeslot `{timeslot}` was not found in the guild `{ctx.guild.name}`. Please enter a valid key.\n\n Available timeslots are:\n(key): (timeslot name)' diff --git a/app/data/config.yml b/app/data/config.yml index 9e26dfe..76111cc 100644 --- a/app/data/config.yml +++ b/app/data/config.yml @@ -1 +1,23 @@ -{} \ No newline at end of file +'864651943820525609': + channels: + help: 866645822472454206 + mod: 865348933022515220 + signup: 866110421592965171 + configured: false + membership: [] + name: Test + notifications: + help: true + signup: true + owner: 493694762210033664 + prefix: '-' + roles: + admin: + - 866642278529368095 + bot: 866639184121954305 + committee: 866642278529368095 + newcomer: 866645308091138060 + returning_player: 866645365524660224 + student: 866645394699714570 + timeslots: + sunaft: Sunday Afternoon 1 pm diff --git a/app/data/data.yml b/app/data/data.yml index 0967ef4..315aaa4 100644 --- a/app/data/data.yml +++ b/app/data/data.yml @@ -1 +1,2 @@ -{} +'864651943820525609': + sunaft: [] diff --git a/app/data/gm.yml b/app/data/gm.yml new file mode 100644 index 0000000..104d380 --- /dev/null +++ b/app/data/gm.yml @@ -0,0 +1 @@ +'864651943820525609': {} diff --git a/app/data/lookup.yml b/app/data/lookup.yml new file mode 100644 index 0000000..104d380 --- /dev/null +++ b/app/data/lookup.yml @@ -0,0 +1 @@ +'864651943820525609': {} diff --git a/app/debug/debug.py b/app/debug/debug.py index 7a331c4..d86349c 100644 --- a/app/debug/debug.py +++ b/app/debug/debug.py @@ -76,7 +76,7 @@ class Debug(commands.Cog, name='Debug Commands'): bot_token=os.getenv('TEST_3_TOKEN'), guild_ids=[ ctx.guild.id ] ) - await ctx.reply(f'```All slash commands have been deleted for the guold {ctx.guild.name}.```') + await ctx.reply(f'```All slash commands have been deleted for the guild `{ctx.guild.name}``.```') elif command == '--global' or command == '-g': await utils.manage_commands.remove_all_commands( bot_id=self.client.user.id,