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, ButtonStyle from discord_slash.client import SlashCommand from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component, create_button, create_actionrow import re 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): self.client = client lookup = yaml_load(lookupFile) 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='', name='delete', description='Deletes a game role and accompanying category, text, voice channels, and data.', # 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='game_role', description='The role representing the game you want to interact with.', option_type=8, required=True ) ] ) async def _game_delete(self, ctx:SlashContext, game_role:discord.Role): await ctx.channel.trigger_typing() conf = yaml_load(configFile) 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 'bot' not in conf[guildStr]['roles']: await ctx.send(f'```Error: `Bot` role for guild `{ctx.guild.name}` has not been defined. Cannot configure game.```',hidden=True) return 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 ctx.channel.category.id != lookup[guildStr][rStr]['category']: await ctx.send(f'```Error: You must issue this command in a text channel associated with the game you are trying to delete.```', hidden=True) return game_title = lookup[guildStr][rStr]['game_title'] time = lookup[guildStr][rStr]['time'] c = ctx.guild.get_channel(lookup[guildStr][rStr]['category']) gm = lookup[guildStr][rStr]['gm'] channelsFound = False del data[guildStr][time][rStr] 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}`') del categories[guildStr][str(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) 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.']) 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) await c.send( content = 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)]: del gms[guildStr][str(gm)] if not data[guildStr][time]: del data[guildStr][time] 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') if self.client.get_cog('T-Card Command') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/tcard.py') if self.client.get_cog('Pitch Command') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py') await self.client.slash.sync_all_commands() @cog_ext.cog_subcommand( base='game', # subcommand_group='', name='modify', description='Edit the information of an existing game channel.', # 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='game_role', description='The role of the game you are trying to edit.', option_type=8, required=True, ), create_option( name='timeslot', description='The new timeslot, if you are changing timeslots.', option_type=3, required=False ), create_option( name='gm', description='The new GM, if the GM is changing.', option_type=6, required=False ), create_option( name='max_players', description='The maximum number of players the game can take.', option_type=4, required=False ), create_option( name='game_title', description='The new title if the title is changing.', option_type=3, required=False ), create_option( name='min_players', description='The minimum number of players the gane can take.', option_type=4, required=False ), create_option( name='current_players', description='The number players currently 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_modify( self, ctx:SlashContext, game_role:discord.Role, timeslot:str=None, gm:discord.User=None, max_players:int=None, game_title:str=None, min_players:int=None, current_players:int=None, system:str=None, platform:str=None ): await ctx.channel.trigger_typing() if all(x is None for x in [timeslot, gm, max_players, game_title, min_players, current_players, system, platform]): await ctx.send(f'```Error: No parameters have been entered to modify the game.```',hidden=True) return conf = yaml_load(configFile) 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) old_time = lookup[guildStr][rStr]['time'] time = re.sub(r"\W+",'', timeslot[:9].lower()) if timeslot else old_time if guildStr not in lookup: lookup[guildStr] = {} if guildStr not in data: data[guildStr] = {} if time not in data[guildStr]: data[guildStr][time] = {} # Command Validation Checks 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 timeslot is not None: if time not in conf[guildStr]['timeslots']: await ctx.send(f'```Error: 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.```',hidden=True) return if any(x is not None and x < 0 for x in [min_players, max_players, current_players]): await ctx.send(f'```Error: You cannot enter negative integers for the number of players.```',hidden=True) return if min_players and max_players and min_players > max_players: await ctx.send(f'```Error: The minimum number of players cannot exceed the maximum number of players.```',hidden=True) return if current_players and max_players and current_players > max_players: await ctx.send(f'```Error: The number of reserved spaces cannot exceed the maximum number of players.```',hidden=True) return # Infer Old Data old_data = data[guildStr][old_time][rStr].copy() result = f'The game {old_data["game_title"]} has been updated.\n' ## Change to the Title and/or Time Slot: if time != old_time: result = ''.join([result,f"The game's time slot has changed from {conf[guildStr]['timeslots'][time]} to {conf[guildStr]['timeslots'][time]}.\n"]) del data[guildStr][old_time][rStr] if not data[guildStr][old_time]: del data[guildStr][old_time] if game_title and game_title != old_data['game_title']: if game_title in [x['game_title'] for x in lookup[str(ctx.guild.id)].values()] and time in [x['time'] for x in lookup[str(ctx.guild.id)].values()]: await ctx.send(f'```Error: The target 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.```',hidden=True) return result = ''.join([result,f"The game's title has been updated to {game_title}\n"]) game_title = old_data['game_title'] if not game_title else game_title if time != old_time or game_title != old_data['game_title']: result = ''.join([result,f"The names of the game role and channel categories have been changed to match the updated {'timeslot' if time != old_time and game_title == old_data['game_title'] else 'game title' if game_title != old_data['game_title'] and time == old_time else 'time slot and game title'}.\n"]) # Update Role await r.edit( mentionable=True, name=f'{time.upper()}: {game_title}', permissions=discord.Permissions.none(), reason=f'/game modify command issued by `{ctx.author.display_name}`', colour=discord.Colour.green() ) # Update GM if gm and gm.id != old_data['gm']: result = ''.join([result,f"The GM has been updated to {gm.display_name}.\n"]) gms[guildStr][str(old_data['gm'])].remove(r.id) if not gms[guildStr][str(old_data['gm'])]: del gms[guildStr][str(old_data['gm'])] gm = await ctx.guild.fetch_member(old_data['gm']) if not gm else gm if r not in gm.roles: await gm.add_roles(r) # Update Category cExists = False 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_id = lookup[guildStr][rStr]['category'] c = discord.utils.get(ctx.guild.categories, id=c_id) t_id = lookup[guildStr][rStr]['text_channel'] t = discord.utils.get(ctx.guild.text_channels, id=t_id) if not c: c = await ctx.guild.create_category( name=f'{time.upper()}: {game_title}', overwrites=permissions, reason=f'/game modify 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 modify 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 modify command issued by `{ctx.author.display_name}`' ) else: cExists= True await c.edit( name=f'{time.upper()}: {game_title}', overwrites=permissions, reason=f'/game modify 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 modify command issued by `{ctx.author.display_name}`' ) for vc in c.voice_channels: await vc.edit( sync_permissions=True, reason=f'/game modify command issued by `{ctx.author.display_name}`' ) v = True if not t: 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 modify command issued by `{ctx.author.display_name}`' ) else: pins = await t.pins() if pins: hm = discord.utils.find(lambda x: x.content.startswith('```Hello ') and 'Your game channels for ' in x.content and x.author.id == self.client.user.id, pins) if hm: 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 modify command issued by `{ctx.author.display_name}`' ) # Determine remaining variables max_players = old_data['max_players'] if not max_players else max_players min_players = old_data['min_players'] if not min_players else min_players current_players = old_data['current_players'] if not current_players else current_players platform = old_data['platform'] if not platform else platform system = old_data['system'] if not system else system result = ''.join(['```',result,f"The game has been configured with space for {max_players} players (with {current_players if current_players else '0'} currently occupied).",'```']) output = f'```Hello {gm.display_name}! Your game channels for `{game_title}` have been updated.\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: {current_players if current_players else "0"}\n']) output = ''.join([output,f'Platform: {platform}```' if platform is not None else '```']) output = ''.join([output,f'\n\n{gm.mention} | {r.mention}']) await ctx.send(result,hidden=True) o = await t.send(output) await o.pin(reason=f'/game modify command issued by `{ctx.author.display_name}`') data[guildStr][time][rStr] = { 'game_title': game_title, 'gm': gm.id, 'max_players': max_players, 'min_players': min_players, 'current_players': current_players, 'system': system, 'platform': platform, 'role': r.id, 'category': c.id, 'text_channel': t.id, 'header_message': o.id } lookup[guildStr][rStr] = { 'category': c.id, 'gm': gm.id, 'time': time, '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)] = [] 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', # subcommand_group='', name='purge', description='Delete all games in a given timeslot.', # 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, ) async def _game_purge( self, ctx:SlashContext ): await ctx.channel.trigger_typing() conf = yaml_load(configFile) data = yaml_load(dataFile) lookup = yaml_load(lookupFile) gms = yaml_load(gmFile) categories = yaml_load(categoriesFile) guildStr = str(ctx.guild.id) async def purgeGames(ctx:SlashContext, timeslot:str): for g in list(data[guildStr][timeslot].values()): c = discord.utils.find(lambda x: x.id == g['category'], ctx.guild.categories) 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) if not gms[guildStr][str(g['gm'])]: del gms[guildStr][str(g['gm'])] del lookup[guildStr][str(r.id)] del data[guildStr][timeslot] if 'timeslots' not in conf[guildStr]: conf[guildStr]['timeslots'] = {} tsDict = {k: conf[guildStr]['timeslots'][k] for k in data[guildStr] if data[guildStr][k]} optionsList = [create_select_option(label=tsDict[x], value=x, description=x) for x in tsDict] optionsList.insert(0, create_select_option(label='All Timeslots', value='--all', description='--all')) try: m = await ctx.send( content='```Select which time slot for which you would like to purge all games.```', delete_after=5, components=[ create_actionrow( create_select( placeholder='Time Slot', options= optionsList, min_values=1, max_values=1 ) ) ] ) while True: select_ctx = await wait_for_component(self.client,messages=m, timeout=5) if select_ctx.author != ctx.author: await select_ctx.send(f'```Invalid response: you are not the person who issued the command.```', hidden=True) else: break await m.delete() [timeslot] = select_ctx.selected_options except asyncio.TimeoutError: await ctx.send(f'```Error: Command timed out.```', hidden=True) return if timeslot == '--all': m = await ctx.send( content=f'```You are attempting to purge games for all time slots. This will delete every game currently running for guild {ctx.guild.name}. Are you sure?```', delete_after=5, components=[ create_actionrow( create_button( style=ButtonStyle.green, label='Yes', emoji='👍', custom_id='purge_yes', ), create_button( style=ButtonStyle.red, label='No', emoji='👎', custom_id='purge_no', ) ) ] ) while True: button_ctx = await wait_for_component(self.client, messages=m, timeout=5) if button_ctx.author != ctx.author: await button_ctx.send(f'```Invalid response: you are not the person who issued the command.```', hidden=True) else: break await m.delete() if button_ctx.custom_id == 'purge_no': await ctx.send(f'```The action `/game purge --all` has been aborted.```',hidden=True) return await ctx.channel.trigger_typing() ctx_id = ctx.channel.id for t in list(data[guildStr]): await purgeGames(ctx=ctx, timeslot=t) if discord.utils.find(lambda x: x.id == ctx_id, ctx.guild.text_channels) is not None: await ctx.send( content = '```All games for all time slots have been purged.```', hidden=True ) else: 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) await c.send( content = '```All games for all time slots have been purged.```' ) else: m = await ctx.send( content=f'```You are attempting to purge games for `{conf[guildStr]["timeslots"][timeslot]}` for guild {ctx.guild.name}. Are you sure?```', delete_after=5, components=[ create_actionrow( create_button( style=ButtonStyle.green, label='Yes', emoji='👍', custom_id='purge_yes', ), create_button( style=ButtonStyle.red, label='No', emoji='👎', custom_id='purge_no', ) ) ] ) while True: button_ctx = await wait_for_component(self.client, messages=m, timeout=5) if button_ctx.author != ctx.author: await button_ctx.send(f'```Invalid response: you are not the person who issued the command.```', hidden=True) else: break await m.delete() if button_ctx.custom_id == 'purge_no': await ctx.send(f'```The action `/game purge {timeslot}` has been aborted.```',hidden=True) return await ctx.channel.trigger_typing() ctx_id = ctx.channel.id await purgeGames(ctx=ctx, timeslot=timeslot) if discord.utils.find(lambda x: x.id == ctx_id, ctx.guild.text_channels) is not None: await ctx.send( content = f'```All games for time slot `{conf[guildStr]["timeslots"][timeslot]}` have been purged.```', hidden=True ) else: 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) await c.send( content = f'```All games for time slot `{conf[guildStr]["timeslots"][timeslot]}` have been purged.```' ) yaml_dump(gms,gmFile) 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') if self.client.get_cog('T-Card Command') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/tcard.py') if self.client.get_cog('Pitch Command') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py') await self.client.slash.sync_all_commands() def setup(client): client.add_cog(GameManagement(client))