diff --git a/TODO.md b/TODO.md index 04e1e20..f607b41 100644 --- a/TODO.md +++ b/TODO.md @@ -57,7 +57,9 @@ > - [x] help notifications (notification group) > - [x] signup notifications (notification group) - [x] Set up timeslots -- [x] Delete timeslots +- [ ] Delete timeslots +> - [x] Base command +> - [ ] Delete all games with the timeslot - [x] List timeslots - [ ] Set up command permissions > - [ ] Slash Commands @@ -65,9 +67,9 @@ >> - [ ] Game Management Commands > - [x] Native Bot Commands - [ ] Migrate existing bot commands -> - [ ] setupgame +> - [x] setupgame > - [x] ~~definebotrole~~ config -> - [ ] deletegame +> - [x] deletegame > - [ ] reset > - [ ] migrate > - [ ] kickplayer diff --git a/app/bot.py b/app/bot.py index f0d326c..fa8b5ad 100644 --- a/app/bot.py +++ b/app/bot.py @@ -36,6 +36,12 @@ dataFile = os.getenv('DATA') if ((os.getenv('DATA').endswith('.yml') or os.geten if not os.path.exists(dataFile): yaml_dump({},dataFile) +# Locate or create lookup file. Same as above. +lookupFile = os.getenv('LOOKUP') if os.getenv('LOOKUP').endswith('.yml') or os.getenv('LOOKUP').endswith('.yaml') else './data/lookup.yml' + +if not os.path.exists(lookupFile): + yaml_dump({},lookupFile) + # Locate Cogs Directory cogsDir = 'cogs' diff --git a/app/cogs/slashcommands/secondary/edit_membership.py b/app/cogs/slashcommands/secondary/edit_membership.py index 15b0d92..bc753a7 100644 --- a/app/cogs/slashcommands/secondary/edit_membership.py +++ b/app/cogs/slashcommands/secondary/edit_membership.py @@ -79,7 +79,7 @@ class EditMembership(commands.Cog, name='Edit Membership'): # subcommand_group_description='Adds a time slot available to the channel for games.', guild_ids=guild_ids, ) - async def _config_timeslots_list(self, ctx:SlashContext): + async def _config_membership_list(self, ctx:SlashContext): conf = yaml_load(configFile) if 'membership' not in conf[str(ctx.guild.id)]: conf[str(ctx.guild.id)]['membership'] = {} diff --git a/app/cogs/slashcommands/secondary/game_management.py b/app/cogs/slashcommands/secondary/game_management.py new file mode 100644 index 0000000..70c659f --- /dev/null +++ b/app/cogs/slashcommands/secondary/game_management.py @@ -0,0 +1,223 @@ +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 re + +from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile, lookupFile + +#### Game Role and Channel Management Commands + +class GameSetup(commands.Cog, name='Game Setup'): + 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? + @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) + 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..```') + return + time = lookup[guildStr][str(game_role.id)]['time'] + c = ctx.guild.get_channel(lookup[guildStr][str(game_role.id)]['category']) + 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}`') + lookup[guildStr].pop(str(game_role.id), None) + await game_role.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`') + yaml_dump(lookup, lookupFile) + yaml_dump(data, dataFile) + +def setup(client): + client.add_cog(GameSetup(client)) \ No newline at end of file