diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ca179..e09964f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Major Changes in Version 3 + - Discards the database engine in favour of data storage in `.yml` files `The database engine was overkill. It only slowed down the Bot's performance, made it more fragile, and caused unnecessary complexity while not really providing any necessary utility. This also makes server migration much easier. @@ -21,4 +22,4 @@ This makes the code a lot more complex to debug and maintain, especially when ha - Refactors the old code for more clarity in the bot's code `Adds some descriptions and explanatory text to the commands themselves so they can be accessed and displayed via the bot help command, making the bot a lot more usable. Also presents the code in a more readable format, with clearer indentation and parameter naming for core functions. -The code has a lot more in-line comments explaining how it works so that it can be maintained by other people.` \ No newline at end of file +The code has a lot more in-line comments explaining how it works so that it can be maintained by other people.` diff --git a/COMMANDS.md b/COMMANDS.md index fc755ef..48d14f4 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -1,4 +1,5 @@ # Bot Commands + A full list of bot commands can be retrieved using the `-help` command in the bot. The commands have full descriptions of their function as well as syntax that can be accessed via the help command. This is a reference file of all of the commands available, as well as the various cogs that control them. \ No newline at end of file diff --git a/README.md b/README.md index 35b8695..079e1ab 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The first version I committed to the repository is version 2.1, and I previously Version 3 was the second major upgrade, taking advantage of some of the recent changes to the Discord API. ## Setup + The Bot is dockerised and uses docker-compose for deployment, so it is fairly straightforward to deploy an instance of. Clone the repository, install Docker and Docker Compose, navigate to the root directory (that contains the `docker-compose.yml` file), and use `docker-compose up -d` to set up and run the bot. The bot runs on one Docker container with the instance of the app as well as storage for its data and configuration. @@ -47,6 +48,7 @@ client.run(os.getenv('BOT_TOKEN')) in order for to authenticate as the correct bot. ## File Structure + ``` |-- app | |-- bot.py @@ -99,6 +101,7 @@ I was considering merging them into one file, but given how different the two co I had initially condsiders a `.ini` file for the configuration settings and `.json` for the data, but I decided to use `.yml` for both just to avoid unnecessary complexity. ### `config.yml` Structure + This tree gives the list of various keys for the `.yml` dictionary as well as the types of different data expected. The entire configuration file is essentially a dictionary with other dictionaries, strings, integers, and lists as values. All values in the dictionary are referenced first by a string of the guild id integer. @@ -128,6 +131,7 @@ Remember to convert the guild ID to strings during several operations, and be ca ``` ### `data.yml` Structure + Just like above, the `data.yml` file is also a dictionary of dictionaries that is indexed by a string of the guild id. It stores only the relevant data necessary for the code to function. It only holds, for instance, ID numbers rather than user handles, Discord discriminators, or names. diff --git a/TODO.md b/TODO.md index 10e02ed..738ccd3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,15 +1,19 @@ # To Do ## Bot Architecture + - [x] Simplify directory tree - [x] Split event listeners into individual cogs. - [x] Update with re-organised data and config structure + > - [x] Correct references to data in existing cogs. + - [x] Setup minimally functioning configs of guild on startup - [x] Synchronise core configuration `/commands` on startup - [ ] ~~Synchronise secondary `/commands` on complete configuration **(see below)**~~ ## Bot Functionality and Processes + - [ ] ~~'Delete Commands' Function~~ - [ ] ~~'Register Commands' Function~~ - [x] Infer Permissions from Config @@ -20,37 +24,53 @@ - [x] Delete Dev/Test Functions - [x] Error handlers - [x] Debug Features + > - [ ] ~~Command Installer/Uninstaller~~ + - [x] Help Channel Event Listener + > - [x] Add Config key for Help Channel + - [ ] Slash Command Buttons or - [ ] Reaction listener selectors - [ ] Member Verification + > - [x] Add Config key membership signup channels > - [x] Add config keys: Membership Category Roles > - [ ] Message Receive listener > - [ ] Message React listener or buttons + - [ ] Membership Restriction + > - [ ] Message Receive Listener > - [ ] Membership Validation Listener + - [ ] Re-synchronise commands after any relevant config changes **(see above)** + > - [ ] 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 game channel on role updates + > - [ ] Exception to event listener to prevent circularity `unsure what this means` ## Event Listeners ## Review Configs When + - [x] Guild Changing Ownership - [x] Roles Modified - [x] Mod Channel Deleted ## Commands + - [x] Configure Bot function and sub commands + > - [x] botrole (role group) > - [x] committeerole (role group) > - [x] modchannel (channel group) @@ -61,18 +81,25 @@ > - [x] student role (role group) > - [x] help notifications (notification group) > - [x] signup notifications (notification group) + - [x] Set up timeslots - [x] Delete timeslots + > - [x] Base command > - [x] ~~Delete all games with the timeslot~~ Do the opposite: block deleting timeslots with existing games. + - [x] List timeslots - [x] Set up command permissions + > - [x] Slash Commands >> - [x] Admin Commands >> - [x] Game Management Commands + > - [x] Native Bot Commands + - [ ] Migrate existing bot commands + > - [x] setupgame > - [x] ~~definebotrole~~ config > - [x] deletegame @@ -82,13 +109,17 @@ Do the opposite: block deleting timeslots with existing games. > - [x] ~~addplayer~~ `/player add` > - [x] ~~leavegame~~ `/player leave` > - [ ] Pitch command and sub-commands + > > - [ ] run > > - [ ] clear ## Misc + - [ ] Review documentation + > - [ ] Finalise README.md > - [ ] CHANGELOG.md > - [ ] COMMANDS.md > - [ ] resources.md -- [ ] Make sure to document **not using discord_components and staying with discord-py-slash-commands library alone**. \ No newline at end of file + +- [ ] Make sure to document **not using discord_components and staying with discord-py-slash-commands library alone**. diff --git a/app/cogs/slashcommands/config.py b/app/cogs/slashcommands/config.py index 4463ee1..1ffd33c 100644 --- a/app/cogs/slashcommands/config.py +++ b/app/cogs/slashcommands/config.py @@ -337,7 +337,7 @@ class Configuration(commands.Cog, name='Configuration Commands'): 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[guildStr]['membership']]): + if any([ctx.guild.get_role(m).name == name for m in conf[str(ctx.guild.id)]['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: diff --git a/app/cogs/slashcommands/secondary/game_management.py b/app/cogs/slashcommands/secondary/game_management.py index 812ed85..6cdb900 100644 --- a/app/cogs/slashcommands/secondary/game_management.py +++ b/app/cogs/slashcommands/secondary/game_management.py @@ -222,7 +222,7 @@ class GameManagement(commands.Cog, name='Game Management'): 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[guildStr].values()] and time in [x['time'] for x in lookup[guildStr].values()]: + 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'```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"]) @@ -418,7 +418,7 @@ class GameManagement(commands.Cog, name='Game Management'): if 'timeslots' not in conf[guildStr]: conf[guildStr]['timeslots'] = {} - tsDict = {k: conf[guildStr]['timeslots'][k] for k in conf[guildStr]['timeslots'] if data[k]} + tsDict = {k: conf[guildStr]['timeslots'][k] for k in conf[guildStr]['timeslots'] if data[guildStr][k]} optionsList = [create_select_option(label=tsDict[x], value=x, description=x) for x in tsDict].insert(0, create_select_option(label='All Timeslots', value='--all', description='--all')) try: m = await ctx.send( diff --git a/app/cogs/slashcommands/secondary/game_setup.py b/app/cogs/slashcommands/secondary/game_setup.py index 4077516..52038bd 100644 --- a/app/cogs/slashcommands/secondary/game_setup.py +++ b/app/cogs/slashcommands/secondary/game_setup.py @@ -133,7 +133,7 @@ class GameSetup(commands.Cog, name='Game Setup'): 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()]: + 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'```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 if guildStr not in data: @@ -241,7 +241,7 @@ class GameSetup(commands.Cog, name='Game Setup'): 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']) 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}']) + 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']) @@ -280,7 +280,7 @@ class GameSetup(commands.Cog, name='Game Setup'): 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 + categories[guildStr][str(c.id)] = r.id yaml_dump(data,dataFile) yaml_dump(lookup,lookupFile) yaml_dump(gms,gmFile) diff --git a/app/cogs/slashcommands/secondary/player_commands.py b/app/cogs/slashcommands/secondary/player_commands.py index 59734d4..6791210 100644 --- a/app/cogs/slashcommands/secondary/player_commands.py +++ b/app/cogs/slashcommands/secondary/player_commands.py @@ -59,8 +59,8 @@ class PlayerCommands(commands.Cog, name='Player Commands'): 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]]): + if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[str(ctx.guild.id)]['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[str(ctx.guild.id)] if gms[str(ctx.guild.id)][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']: @@ -70,11 +70,11 @@ class PlayerCommands(commands.Cog, name='Player Commands'): 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'] + t = lookup[guildStr][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) + hm = data[guildStr][t][rStr]['header_message'] + tc = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['text_channel'],ctx.guild.channels) if tc is not None: p = await tc.pins() if p: @@ -133,24 +133,24 @@ class PlayerCommands(commands.Cog, name='Player Commands'): 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)] + game = discord.utils.find(lambda x: x.id == categories[guildStr][str(ctx.channel.category.id)], ctx.guild.roles) 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]]): + if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[str(ctx.guild.id)]['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[str(ctx.guild.id)] if gms[str(ctx.guild.id)][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 + t = lookup[guildStr][rStr]['time'] + if 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) + hm = data[guildStr][t][rStr]['header_message'] + tc = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['text_channel'],ctx.guild.channels) if tc is not None: p = await tc.pins() if p: @@ -206,11 +206,11 @@ class PlayerCommands(commands.Cog, name='Player Commands'): 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 + t = lookup[guildStr][rStr]['time'] + if 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) + hm = data[guildStr][t][rStr]['header_message'] + tc = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['text_channel'],ctx.guild.channels) if tc is not None: p = await tc.pins() if p: diff --git a/app/data/categories.yml b/app/data/categories.yml index 4cf0d48..fccd3f5 100644 --- a/app/data/categories.yml +++ b/app/data/categories.yml @@ -1,2 +1,3 @@ '864651943820525609': '866788661026488352': 866788659839238164 + '867516972496060446': 867516971017175070 diff --git a/app/data/data.yml b/app/data/data.yml index da1d382..cf18a9c 100644 --- a/app/data/data.yml +++ b/app/data/data.yml @@ -12,3 +12,15 @@ role: 866788659839238164 system: null text_channel: 866788663194812446 + '867516971017175070': + category: 867516972496060446 + current_players: 1 + game_title: Masks + gm: 493694762210033664 + header_message: 867517905620303894 + max_players: 5 + min_players: null + platform: null + role: 867516971017175070 + system: null + text_channel: 867516974212055060 diff --git a/app/data/gm.yml b/app/data/gm.yml index 7a24859..a48a7e8 100644 --- a/app/data/gm.yml +++ b/app/data/gm.yml @@ -1,4 +1,5 @@ '864651943820525609': '493694762210033664': - 866788659839238164 + - 867516971017175070 '864649599671205914': [] diff --git a/app/data/lookup.yml b/app/data/lookup.yml index 0bae420..e09304b 100644 --- a/app/data/lookup.yml +++ b/app/data/lookup.yml @@ -5,3 +5,9 @@ gm: 493694762210033664 text_channel: 866788663194812446 time: suneve + '867516971017175070': + category: 867516972496060446 + game_title: Masks + gm: 493694762210033664 + text_channel: 867516974212055060 + time: suneve diff --git a/resources.md b/resources.md index d7ba9ac..d1d914b 100644 --- a/resources.md +++ b/resources.md @@ -1,21 +1,27 @@ # Resources for Maintaining the Bot ## Documentation + 1. [Discord Py Documentation](https://discordpy.readthedocs.io/en/stable/index.html) + > 1. [Quickstart Guide](https://discordpy.readthedocs.io/en/stable/quickstart.html) > 2. [Set up of Discord Bot Account](https://discordpy.readthedocs.io/en/stable/discord.html) > 3. [**Important**: Primer to Gateway Intents](https://discordpy.readthedocs.io/en/stable/intents.html) `N.B.: this is an important security feature of Discord that is now mandatory to configure and imposes restructions on some of the Bot's functionality unless appropriately configured. Keep an eye on this.` > 4. [Repository with example code](https://github.com/Rapptz/discord.py/tree/v1.7.3/examples) > 5. [Logging Setup](https://discordpy.readthedocs.io/en/stable/logging.html) + 2. [Discord Py Slash Command Documentation](https://discord-py-slash-command.readthedocs.io/en/latest/index.html) + > 1. [Discord Py Slash Command Authentication](https://discord-py-slash-command.readthedocs.io/en/latest/quickstart.html) `N.B.: this is an important security feature in Discord's API, and commands will not be configured unless the applications.commands scope is configured correctly.` > 2. [How to add Slash Commands, including sub-commands](https://discord-py-slash-command.readthedocs.io/en/latest/faq.html#:~:text=If%20your%20slash%20commands%20don,commands%20scope%20in%20that%20guild.) > 3. [Slash Command Cogs Module](https://discord-py-slash-command.readthedocs.io/en/latest/discord_slash.cog_ext.html?highlight=cog#discord_slash.cog_ext.cog_subcommand) + 3. [Discord Components Documentation](https://discord-components.readthedocs.io/en/0.5.2.4/) ## YouTube Tutorials + 1. [Starting from the basics by Lucas](https://www.youtube.com/watch?v=nW8c7vT6Hl4) 2. [Introduction to Cogs](https://www.youtube.com/watch?v=vQw8cFfZPx0) 3. [Dynamic prefixes for different servers](https://www.youtube.com/watch?v=yrHbGhem6I4) @@ -26,10 +32,12 @@ ## Communities ### Discord Py + 1. [Discord Py server](https://discord.gg/r3sSKJJ): Discord server to talk to others in the community 2. [Discord Py Github issue tracker](https://github.com/Rapptz/discord.py/issues): place to report bugs and issues with the API 3. [Discord Py discussion page](https://github.com/Rapptz/discord.py/discussions): wiki for any other discussions ### Discord Py Slash Commands + 1. [Discord Py Slash Commands Discord Server](https://discord.gg/KkgMBVuEkx): Discord server with a forum to ask questions in -2. [Discord Py Slash Commands Issue Tracker on GitHub](https://github.com/discord-py-slash-commands/discord-py-interactions/issues) \ No newline at end of file +2. [Discord Py Slash Commands Issue Tracker on GitHub](https://github.com/discord-py-slash-commands/discord-py-interactions/issues)