Compare commits

...

61 Commits

Author SHA1 Message Date
4f92e83e48 Documentation and description update
Added .env.example file.
Branch now ready to be merged for the first tagged release of the Bot.
2021-08-05 02:54:52 +01:00
94ce0aa31a Fully implemented /tcard command.
Bug fixes for member signup
Added live update of game header message during pitches
Documentation updates
2021-08-05 02:00:03 +01:00
175a911ed4 Try enabling opus 2021-08-05 00:40:58 +01:00
250ad9b593 removed libiffi 2021-08-05 00:35:00 +01:00
8acdadfc79 Trying to add Opus and dependencies 2021-08-05 00:30:40 +01:00
ca281fb34f Remove audio for the time being 2021-08-04 17:19:47 +01:00
c32cef2da5 Change Libopus name 2021-08-04 17:13:30 +01:00
63146bd042 Trying to install Opus lib via Docker instruction 2021-08-04 17:02:36 +01:00
21d5cba5f5 Trying to get Opus installed 2021-08-04 16:49:56 +01:00
90d6132705 Attempt to load Opus 2021-08-04 16:30:01 +01:00
a10ed8ef29 Let's try if this fixes it 2021-08-04 16:25:48 +01:00
330426b2d3 Change sound file 2021-08-04 16:05:31 +01:00
cc0b3c6bb9 Getting better? 2021-08-04 15:24:39 +01:00
dcf0fec7ac Hope this fixes it 2021-08-04 15:17:13 +01:00
e47e08a272 What the hell is going on!? 2021-08-04 15:11:34 +01:00
497441d841 Somehow this has made everything worse 2021-08-04 15:04:47 +01:00
3ad556bb3b Fix for requirements bug 2021-08-04 14:52:27 +01:00
833cfb1278 Trying to fix bizarre Requirements bug 2021-08-04 14:43:58 +01:00
5944f6fa85 Updated requirements 2021-08-04 14:33:01 +01:00
cf912db336 Hotfix colour bug 2021-08-04 14:19:11 +01:00
9656249655 Hotfix to enable /tcard 2021-08-04 14:14:38 +01:00
e441ba63a0 Hotfix for enabling the /tcard command 2021-08-04 14:12:14 +01:00
9014bdaac4 Updated bot.
Bugfix: player count on GM leaving
Added /tcard command with audio
Updated debug commands
2021-08-04 13:15:18 +01:00
40daa58326 Bug fix for player add and kick 2021-07-27 11:53:29 +01:00
7dff3f74ec Bug fixes 2021-07-27 11:51:02 +01:00
d38ead1c49 Hotfix pitch menu bug due to Null current_players 2021-07-27 11:34:47 +01:00
46fb9cf1fd Permission Hotfixes 2021-07-26 21:29:20 +01:00
a772c06313 Debug 2021-07-26 21:21:41 +01:00
89a92586ff Hotfix 2021-07-26 21:14:50 +01:00
7916b1fca6 Pre-Deployment Still bugged as hell. 2021-07-26 18:54:28 +01:00
e30e89e7e3 Debugged membership sign-ups and pitch menu.
Ready for more rigorous testing.
2021-07-24 17:58:23 +01:00
173aeb2a3c Debug Pitch Menu 2021-07-24 13:15:01 +01:00
1826b9d72b Added migrate command. Bot ready for testing. 2021-07-24 09:25:46 +01:00
882e0b3ab8 Updated Readme. Refreshed requirements.
Purged guild.
Ready for further testing.
2021-07-24 01:01:48 +01:00
5bb9af12c9 Finished the bot. Requires testing.
Wrote up documentation. Readme needs finishing.
Future development needs to use global listeners for processes.
2021-07-23 15:55:27 +01:00
2f974f9f8f Finished writing Pitch command.
Needs further testing.
Prepare to write member verification and event listeners next.
2021-07-23 00:44:21 +01:00
51513c89a0 Issue present with Purge command 2021-07-21 23:05:13 +01:00
f1d12691c0 Updated vscode settings. 2021-07-21 12:10:15 +01:00
ede87e799c Wrote player commands, still not tested. 2021-07-21 11:49:29 +01:00
615e3fa169 Finished making purge command (untested)
Made all feedback from /commands hidden messages.
2021-07-21 01:41:08 +01:00
f80a37f949 Changed how delete time slot functions 2021-07-20 21:43:22 +01:00
c6113e31eb Added categories lookup file 2021-07-20 17:28:38 +01:00
45f7edb75d Fixed the game modift command.
Working on purge and GM commands next.
2021-07-20 00:44:03 +01:00
1a00afcf71 Modify Game command really badly broken.
A lot of silent and unhelpful errors.
2021-07-19 21:22:25 +01:00
304dd9f9de Finessed game create and delete commands 2021-07-19 15:30:04 +01:00
4277ef4f91 Wrote Create and Delete Game commands
Untested.
2021-07-19 02:26:35 +01:00
e62342bc9a Finished Config command group 2021-07-18 23:16:58 +01:00
b6203566b3 Added config lock, but not working. 2021-07-18 20:11:05 +01:00
c5be0002ac Finished abstraction of /config subcommands 2021-07-18 01:28:06 +01:00
3d937fffea Finished /config subcommands
Prior to abstraction to generic /config commands with multiple keyed arguments
2021-07-18 00:43:44 +01:00
241403f5fb Removed async await test function 2021-07-17 14:00:23 +01:00
78bc73c023 Started writing /commands.
Completed channel group of config subcommands
Fixed bugs in config initialisation function
2021-07-17 13:56:04 +01:00
51a01bab49 Added config checking, event listeners, etc.
Can track added features by comparing TODO list.
2021-07-16 23:53:31 +01:00
1d355e3b2d Updated documentation.
Changed virtual environment settings.
Beginning change to data structure.
2021-07-16 08:37:06 +01:00
ac1b6b0169 Another update to TODO 2021-07-15 23:55:01 +01:00
e90fb6ae10 Updated ToDo with more detail 2021-07-15 23:53:37 +01:00
c460ce4296 Fixed more formatting errors 2021-07-15 23:26:53 +01:00
ea0d49f57c Fixed formatting errors in TODO.md 2021-07-15 23:23:48 +01:00
1fa5029212 Changed file structure.
Moved code to main bot and cog files.
2021-07-15 23:13:01 +01:00
b0b417a8d2 15 July Nightly Commit
Split cogs into different files
About to change file structuring to move dev file to main file
2021-07-15 22:54:09 +01:00
ef6c49b5f8 15 July Build
Implemented YAML
Implemented basic client introspection for guild metadata
Added todo tracker
2021-07-15 09:03:44 +01:00
45 changed files with 3959 additions and 1172 deletions

15
.gitignore vendored
View File

@ -1,5 +1,8 @@
db/*
**/data/*.yml
!**/data/config_blueprint.yml
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
@ -88,14 +91,14 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
@ -140,6 +143,14 @@ dmypy.json
# Cython debug symbols
cython_debug/
# Local Dev Env Configs
Scripts/
pyvenv.cfg
app/cogs/template.py.tmp
app/cogs/events.py.tmp
app/old_code/
app/run_venv.sh
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"editor.insertSpaces": false,
"python.pythonPath": "./app/Scripts/python",
"python.autoComplete.extraPaths": ["./app"]
}

25
CHANGELOG.md Normal file
View File

@ -0,0 +1,25 @@
# 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.
I went with .yml files over .json because I preferred the readability of the former, and to align it with the Docker-Compose files elsewhere in the directory.
I cannot believe how much I got into the debate between these two formats.
Seriously, the database was overkill and such a bad idea.
It was fun to learn but was such a bad idea.`
- Implements client-side `/commands` as the core method of interaction
`This takes advantage of a new Discord feature to programme /commands into the console via Bot integration rather than natively at the bot.
This offers more robust command handling, with client-side data validation, option entries, input menus, etc.
This does have the drawback of requiring a more complex API interaction in order for the bot to function, but it should improve the bot's functionality overall.`
- Uses Discord Buttons and Select Menus to replace Reaction Role features
`In keeping with adopting more robust Discord features to deprecate old ones, this new version of the Bot tries to make use of more powerful Discord features instead of contorting Reaction Roles and other previous features into the shape of something they were not built for.
While it is unclear what the direction of such developments in Discord will be going forward, and it is unclear how future API changes will affect this, one way or another this is using new Discord features in the way they are meant to be used rather than excessive hacking.`
- Provides a level of abstraction in many of its recognition of the various roles and channels referenced in its functioning.
`The bot no longer has specific channels (like help or committee notification channels) or roles (like administrators or different categories of membership) hard-coded into it for it to interact with them.
This makes the code a lot more complex to debug and maintain, especially when handling defaults or fail-safes for when the relevant roles or channels are deleted, but has some added flexibility and makes the bot a lot less dependent a precise server setup, thus making it a lot more brittle in the long run.`
- 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.`

120
COMMANDS.md Normal file
View File

@ -0,0 +1,120 @@
# Bot Commands
A full list of native 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 will not apply to `/commands`, and so this reference file will provide a list of all the commands, the cogs that control them, as well as their syntax or usage notes.
`N.B.: Do not delete any roles, channels, or categories that are created by the Bot unless you are certain you need to.
The Bot has no way of synchronising manual actions with its data files at present, and any conflict between the guild setup and the Bot's data will cause errors.
In a future version, I may try and make the bot more flexible to work with both its own commands and user actions.`
The prefix for the Bot's native commands is dynamic.
It can be changed using the relevant command below.
The default prefix for the bot is `-`.
In the syntax description below, mandatory arguments are given in `<angled brackets>`, and optional arguments are given in `[square brackets]`.
For most purposes, the bot will primarily rely on the new `/command` framework, and as such those commands will be listed first. The native bot commands will be provided below.
## `/commands`
`/commands` operate through a level of base commands, groups, and sub-commands.
Sub-commands inherit their permissions from their base commands.
All permissions are thus set at the level of the base command.
The commands are given in different cogs in order to keep the design modular, and to ensure that different modules can be activated or deactivated depending on what the configuration of the guild is like.
### Configuration Commands
These commands are to set up the bot for the guild.
The base command for the bot is `/config`.
Configuration commands are `admin only`.
| Command | Description |
|---|---|
| `/config roles <key> <role exists> [role]` | Sets up the various key roles referenced by the bot, such as `bot` for dice bots, `committee`, `students`, as well as `newcomer`/`returning-player`. If a role already exists, the bot will assign that role. If it does not, the bot will create one. The bot will not be able to create games until a dice bot role has been defined.
| `/config channels <key> <channel exists> [channel]` | Sets up the various key channels referenced by the bot, such as the `mod` channel for notifications to Committee, as well as the `help` channel for the bot to monitor, and the `signup` channel for membership confirmations. If a channel already exists, the bot will assign it. If it does not, the bot will create one. The bot will not be able to use these features until the channels are defined. |
| `/config membership add <name> <role exists> [role]` | Creates a new membership type for the Bot to administer in the Guild. If a role exists, it will assign that role to the membership type. If it does not, the bot will create it. |
| `/config membership list` | Shows the current types of membership set up. |
| `/config membership remove <membership role>` | Deletes a membership type set up in the Guild. The command is locked if there are no membership types configured. |
| `/config notifications <key> <value>` | Whether or not the bot notifies the Committee on messages being posted here. This includes monitoring for the `help` channel and `membership signups` channel, etc. |
|`/config restrict <value>` | Enables or disables membership restriction in the Guild. |
| `/config timeslots add <key> <name>` | Creates a new timeslot for the Guild to host games in. The bot will not be able to create games until at least one time slot is configured. |
| `/config timeslots list` | Shows the current timeslots with their keys and names that are configured for the Guild. This command will only display if there is at least one timeslot configured. If you delete the last time slot, this command will be locked. |
| `/config timeslots modify <key> <name>` | Edits the name of a configured timeslot. You cannot change the key of a timeslot. This command will only display if there is at least one timeslot configured. If you delete the last time slot, this command will be locked. |
| `/config timeslots remove` | Opens a prompt to delete a configured timeslot. You cannot delete a time slot that has games configured for it. This command will only display if there is at least one timeslot configured. If you delete the last time slot, this command will be locked. |
### Game Commands
These commands are for setting up and managing games configured on the Guild.
The base command for the bot is `/game`.
The commands are `admin only`.
The base command is locked and only become available when at least one timeslot is set up, and a bot role has been assigned.
The sub-commands have additional restrictions.
| Command | Description |
|---|---|
| `/game create ...` | This is the base command to create games. This command takes several parameters that are far too many for me to list here. The Prompt system will make it clear what arguments need to be entered and when. |
| `/game delete <@game role>` | Deletes the game associated with the role that is mentioned. This command is locked until there is at least one game configured. If the last game that is configured is deleted by the bot, the command is locked again. `This command must be issued in a text channel associated with the game being deleted (in addition to mentioning the game role).` |
| `/game modify <@game role> ...` | This command also takes several parameters, just like `/game create`, but all of them are optional as long as at least one is entered. It changes the various parameters of the game. |
| `/game purge` | Opens a prompt to delete all games in a given time slot. It also has the option of deleting all games for all time slots. `Use this command with extreme caution.` |
### Player Commands
These commands are for managing players and their membership of games.
The base command is `/player`.
Permissions for this command are open to all users (i.e. all users who have the `use slash command` permission enabled on the Guild settings).
These commands are locked until there is at least one game configured.
| Command | Description |
|---|---|
| `/player add <@player> <@game>` | Adds a player to a game. It can only be used by the GM of the game being mentioned, or by an admin. |
| `/player leave` | Removes the user issuing the command from the game they are in. This command `must be issued in a text channel associated with the game you are trying to leave`. |
| `/player remove <@player>` | Removes the user mentioned from the game. This command `must be issued in a text channel associated with the game you are trying to leave`. |
While this is not strictly a `/player` command, and it is housed in a separate cog, it has the same level of permissions and prerequisites as all the `/player` commands.
| Command | Description |
|---|---|
|`/tcard`| Invokes a T-Card in the game. This command also posts a graphic of the T-Card, tags the game's role, and pings a message in the appropriate voice channel.|
### Pitch Command
The `/pitch` command is used to run pitches for games on the server.
It is locked until at least one game is set up.
The command is `admin only`.
It turns the current text channel where it is issued into a pitch listing channel, setting its permissions appropriately.
It opens a prompt to select which timeslot to pitch for.
The command further generates a menu of the games running, and also gives the admins a control panel with which to control access to the game staggered by the various roles (newcomers, returning players) if they are defined.
The pitch menu runs until it is closed using the relevant button.
## Native Bot `-commands`
These commands are issued using the bot's text prefix.
The prefix may vary depending on the bot's configuration.
### Default Commands
These commands are default to the `Discord.py` library and are automatically enabled in the bot
| Command | Permissions | Syntax and Aliases | Description |
|---|---|---| --- |
| help | All Users | `-help [command]`| Default Discord helper command. Gives a list of all of the commands enabled in the Guild. And when used with an optional argument that gives a command name, it provides the help text, description, and syntax of the command.|
### Debug Commands
These commands are found in the file `./cogs/controlcommands/debug.py` and provide meta-controls for how the bot and its functionality work.
The permissions for these commands has been set up at the Cog level for those with the Bot Maintainer role only or for the owner of the Guild. If no bot maintainer role is defined, the command will fail.
Information about debug commands will be displayed to users authorised to use them via the `-help` command.
### Prefix Command
This command is in the file `./cogs/botcommands/prefix.py`.
It allows for the dynamic control of the bot's command prefix for native commands.
The permission has been set up at the Cog level.
| Command | Permissions | Syntax and Aliases | Description |
|---|---|---| --- |
| changeprefix | Admin Only | `-changeprefix [str]` or `-prefix [str]` | Sets the given string as a prefix. The argument is option, and if not provided an argument it sets the prefix to `-` as the default. |

237
README.md
View File

@ -3,68 +3,221 @@
This is a bot I wrote to manage the Discord server for Geas, the Edinburgh University Table-Top Role-Playing Society, during our move to an on-line format.
The bot is designed to create and manage channels and roles for gaming groups in order to replicate our in-person pitch events on a Discord space as far as possible.
The bot is written in Python, and was the first Python coding project I wrote, so it has a special place in my heart.
The first version I am committing to the repository is version 2.1, and I previously handled the version control manually, so migrating old versions to Git would be a pain.
The first version I committed to the repository is version 2.1, and I previously handled the version control manually, so migrating old versions to Git would be a pain.
Version 3 was the second major upgrade, taking advantage of some of the recent changes to the Discord API.
## Bot Setup
The Bot is dockerised and uses docker-compose for deployment, so it is fairly straightforward to set up.
## 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 uses two containers that are networked internally:
> 1. A python app that runs the bot, and
> 2. A MongoDB database that stores the bot's data for persistence.
The database is not exposed externally to the network, and can only be accessed by the Bot in the network of containers.
The bot runs on one Docker container with the instance of the app as well as storage for its data and configuration.
The bot uses docker-compose to mount an external volume to allow for persisting file storage and easy migration.
It no longer uses a database engine because it never really benefitted from the various database manipulation tools in the earlier version, and was not worth the complexity.
The bot authenticates using an API key, which I have kept private in a `.env` file that I have not uploaded to the repository.
In order to set up your own instance of the bot, you will need to create two copies of the `.env` file, one in the root directory and one in the `app` folder, and enter the respective values for the API keys for the Geas Server Bot and the Test Bot.
In order to set up your own instance of the bot, you will need to provide the following values in a `.env` file in the root directory.
You will also need this database to set up a username and password for the MongoDB database.
The specific username and password don't matter as the bot refers back to the environment variable when authenticating.
The following is a step-by-step guide on installation:
### Prerequisites
1. **Prerequisites**:
1. Command line access to your computer.
2. Make sure that the Bot has the correct authorisation scopes in your Discord Developer section. The bot needs to have `application.commands` and `bot` enabled.
3. Invite the Bot to join your guild.
4. Install and run [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git), [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
2. Make a folder to house the project. `mkdir geas-bot && cd geas-bot`
3. Clone the repository. Type `git clone https://git.vsnt.uk/viveksantayana/geas-bot.git .`
4. Navigate to the `app` folder and use the `.env.example` file to make the environment variables file: `sudo mv .env.example .env`
5. Edit the `.env` file and add the relevant API keys (see below). You can do this using any text editor, like `sudo nano .env`.
6. Place any data files from a previous install in the relevant `/app/data` folder. They will be copied in to the Docker container in the next step. Make sure the `.env` files refer to them correctly.
7. Navigate back to the root folder `geas-bot` and execute docker-compose to spin up the Bot. Do this in detached mode so you do not get locked into the terminal (unless you want to debug and read the console outputs.) `sudo docker-compose up -d`
8. Sign in to Discord and finish configuring the bot using the various `/config` commands.
The following is the template for the `.env` file, with the variable names as are referenced in the bot's code.
You can find an example template of this in the `.env.example` file.
The following is the template for the `.env` file, with the variable names as are referenced in the bot's code:
`.env` file:
```DotENV
BOT_TOKEN=(API token for the production version of the bot.)
TEST_TOKEN=(API token for any test instance.)
CONFIG=(Path to config file. The bot defaults to './data/config.yml' if not provided.)
DATA=(Path to data file. The bot defaults to './data/data.yml' if not provided.)
LOOKUP=(Path to the game role lookup file. The bot defaults to './data/lookup.yml' if not provided.)
GM=(Path to the GM lookup file. The bot defaults to './data/gm.yml' if not provided.)
CATEGORIES=(Path to the channel category lookup file. The bot defaults to './data/categories.yml' if not provided.)
PITCHES=(Path to the pitches data file. The bot defaults to './data/pitches.yml' if not provided.)
BOT_VERSION=(verson string)
```
BOT_TOKEN=
TEST_TOKEN=
MONGO_INITDB_ROOT_USERNAME=
MONGO_INITDB_ROOT_PASSWORD=
BOT_VERSION=2.1.1
```
The correct API keys need to be entered in the environment variables in the `.env` file, and for a copy of this file to be placed in the root and the `app` directories.
Also create a new folder in the root directory called `db`. This is to ensure there is persistence of data when the bot is updated.
**N.B.**: You can switch between which Both the code executes, the main bot or the test bot, by changing the variable referenced in the `client.run()` command in the `bot.py` file:
**N.B.**: When the bot is first run, it is configured to log in as the Test Bot, and not the main Geas Server Bot, as a safety measure.
To change this, navigate to the last line of the file `bot.py` and change the line:
```
```py
client.run(os.getenv('TEST_TOKEN'))
```
to
```
```py
client.run(os.getenv('BOT_TOKEN'))
```
in order for to authenticate as the correct bot.
## Bot Structure
The bot is divided into the following files:
## File Structure
```
app folder
| bot.py -- bot core functionality and code entrypoint
| Dockerfile -- Docker instructions on building the bot
| requirements.txt -- Dependencies to be installed
----cogs -- Individual modules for specific features
GameManagement.py -- adding or kicking players
HelpNotifier.py -- notifications for Help channel
MembershipRestriction.py -- restrictions unverified users
MembershipVerification.py -- membership verification system
PitchMenu.py -- automation for generating menus for game pitches
|-- app
| |-- .env.example
| |-- assets
| | `-- tcard.wav
| |-- bot.py
| |-- cogs
| | |-- botcommands
| | | `-- prefix.py
| | |-- controlcommands
| | | `-- debug.py
| | |-- events
| | | |-- on_command_error.py
| | | |-- on_connect.py
| | | |-- on_guild_channel_delete.py
| | | |-- on_guild_join.py
| | | |-- on_guild_remove.py
| | | |-- on_guild_role_create.py
| | | |-- on_guild_role_delete.py
| | | |-- on_guild_role_update.py
| | | |-- on_guild_update.py
| | | |-- on_message.py
| | | |-- on_ready.py
| | | `-- secondary
| | | `-- pitch_listener.py
| | |-- membership
| | | |-- membership_verification.py
| | | `-- restriction_listener.py
| | `-- slashcommands
| | |-- config.py
| | `-- secondary
| | |-- edit_membership.py
| | |-- game_create.py
| | |-- game_management.py
| | |-- manipulate_timeslots.py
| | |-- pitch.py
| | |-- player_commands.py
| | `-- tcard.py
| |-- data
| | |-- .gitkeep
| | |-- categories.yml
| | |-- config_blueprint.yml
| | |-- config.yml
| | |-- data.yml
| | |-- gm.yml
| | `-- lookup.yml
| |-- Dockerfile
| `-- requirements.txt
|-- CHANGELOG.md
|-- COMMANDS.md
|-- docker-compse.yml
|-- LICENSE
|-- README.md
|-- resources.md
`-- TODO.md
```
Many of the specific features, such as the bot's prefix, the roles it recognises as Committee, the channels it recognises as the Help or Membership Verification channels, are all hard-coded into the Bot.
This is because the bot was only ever supposed to be used on one server, so did not need the flexibility of adapting to multiple channels.
In the future, if I ever tinker with this in the future, I might try and add flexibility in the channels and roles it defines for its various functions.
The `COMMANDS.md` file gives a list of all the commands the Bot uses, as well as a reference to the various cogs or base commands that are associated with them. The code for the command should be housed in the respective files within the file tree.
I might also, in future incarnations, not use a database.
It was fun to learn how to use a database, but it is overkill.
## Data Structure
## Bot Commands
A full list of bot commands can be retrieved using the `-help` command in the bot, and this might be an easier way of retrieving the commands than having a separate copy in the documentation.
The bot holds data in two `.yml` files, `config.yml` for client configurations for each guild it is in and `data.yml` to hold the actual data regarding game and channel set-up.
I was considering merging them into one file, but given how different the two concerns were I ended up splitting the files.
I had initially considered 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.
Remember to convert the guild ID to strings during several operations, and be careful to compare like for like in any logics.
```yml
guild id string:
channels:
help: int
mod: int
signup: int
configured: bool
membership:
- role id int
name: str
notifications:
help: bool
signup: bpp;
owner: owner id int
prefix: '-' by default
roles:
admin:
- role id int
bot:
committee:
newcomer:
returning_player:
student:
timeslots:
key: name
```
### `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.
```yml
guild id string:
timeslot:
role:
category: category id int
current_players: int
header_message: message id int
game_title: str
gm: gm role id int
max_players: int
min_players: int
platform: str
role: role id int
system: str
text_channel: channel id int
```
### Other Data Files
In addition to the above data file, the bot also uses storage in additional reference files to quickly look up values when needed for its various functions.
The purpose of these lookup files is more to act as dictionaries facilitating arbitrary look-ups of key information when required.
They are not intended to act as storage.
Most of these lookup files are not particularly readable because they have raw values without informative keys.
They are constructed and manipulated in tandem with the core data files.
## In the Future
### Restructure command execution using global event listeners
As it stands, there is a conundrum with the Bot:
any kind of manual interaction to manipulate roles or categories will cause conflicts to emerge between the Bot's data and the guild settings.
In order for the bot to be adaptable, and to respond to user interactions, it will need event listeners for things like channel, role, or category changes/creation/depetion, etc.
Having such listeners will cause a circularity between the Bot's edit actions, which would then trigger the listener.
There is currently no way of having an exception for the Bot's edits.
To reconcile this, the bot would need to work such that the command process that modified games only acted upon the roles, which would then trigger the event listeners to synchronise these changes with the categories, and subsequently the data.
Having the bot edit the data in the main command process would mean that there would be conflicts with the simuntaneous execution of parallel threads.
This works for individual commands, but it breaks down when trying to use the `purge` command because of conflicts causedb by simultaneous changes to the data files.
Programming around this will need a further layer of complexity, involving flags checking for R/W operations and a time-out.
### Membership sign up performance issues
I have set the member verification prompt to use a global listener to avoid a situation where it creates several backlogged processes when multiple people post sign-ups at the same time.
This should also mean that the sign-up prompts should persist over reboots.
The best possible way of setting this up is probably using functions to dynamically set up and remove component call-backs.
Also components do not need to all have unique names, just unique names in the message they are part of.

107
TODO.md Normal file
View File

@ -0,0 +1,107 @@
# To Do
- [ ] Testing
- [x] Dockerise
- [ ] Infer/Transfer data from old bot
- [ ] Deploy
## 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
- [x] Dynamic Command Prefixes
- [ ] Infer current games from Server Structure
`Create a separate cog to do this instead of having a migrate command, install the cog temporarily and remove the cog once migration is done.`
- [ ] Re-enable logging
- [x] Delete Dev/Test Functions
- [x] Error handlers
- [x] Debug Features
- [ ] ~~Command Installer/Uninstaller~~
- [x] Help Channel Event Listener
- [x] Add Config key for Help Channel
- [ ] ~~Slash Command Buttons or~~ `This kind of got subsumed into other features.`
- [ ] ~~Reaction listener selectors~~ `So did this.`
- [x] Member Verification
- [x] Add Config key membership signup channels
- [x] Add config keys: Membership Category Roles
- [x] Message Receive listener
- [x] ~~Message React listener~~ or buttons `Used buttons waiting within the same command thread. Possibly update to global listener if performance is affected.`
- [x] Membership Restriction
- [x] Message Receive Listener
- [x] Membership Validation Listener
- [x] Re-synchronise commands after any relevant config changes `(See from above)`
- [ ] Role Delete (~~member~~, ~~admin~~, ~~game~~) `Admin role missing won't cause any issues as server role will still remain in control`
`Deleting membership roles will also trigger a mess. Event listener will trigger and attempt to execute simultaneously with the command to delete the role, which will cascade into several errors. In order to make this work, it is best to have the commands interact solely with the roles, and then subsequently the listeners to sync with the data.`
- [ ] ~~Channel delete (notifications, logs, game text channel)~~ `There aren't any critical settings that depend on this just yet, and it is hard to set this up without circularity`
- [ ] ~~Category delete (games)~~ `Circularity problem: if the category delete event listener is set up, it will react to the bot deleting categories when managing 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`~~
`I remember what this is now: if you have a listener for when roles get deleted, and if the bot deletes a role, then it triggers the listener. It might be worth restructuring this such that the Bot command only deletes the role, which then triggers the listener that deletes the categories and synchronises data. That way, the roles can be deleted manually or via the command. Otherwise, it is not possible in the Discord API to have this exception, as the Bot only responds to the 'Guild' deleting the role, not the user or bot or admin that issued the command.`
## 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)
- [x] help channel (channel group)
- [x] signup channel (channel group)
- [x] newcomer role (role group)
- [x] returning player role (role group)
- [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
- [x] Migrate existing bot commands
- [x] setupgame
- [x] ~~definebotrole~~ config
- [x] deletegame
- [x] ~~reset~~ purge
- [ ] ~~migrate~~ `See above`
- [x] ~~kickplayer~~ `/player remove`
- [x] ~~addplayer~~ `/player add`
- [x] ~~leavegame~~ `/player leave`
- [x] Pitch command and sub-commands
- [ ] ~~run~~ `Combined both sub-commands into single command and prompt response.`
- [ ] ~~clear~~
## Misc
- [x] Review documentation
- [x] Finalise README.md
- [x] CHANGELOG.md
- [x] COMMANDS.md
- [x] resources.md
- [x] Make sure to document `not using discord_components and staying with discord-py-slash-commands library alone`.

9
app/.env.example Normal file
View File

@ -0,0 +1,9 @@
BOT_TOKEN=
TEST_TOKEN=
CONFIG=./data/config.yml
DATA=./data/data.yml
LOOKUP=./data/lookup.yml
GM=./data/gm.yml
CATEGORIES=./data/categories.yml
PITCHES=./data/pitches.yml
BOT_VERSION=3.0.0

View File

@ -1,4 +1,5 @@
FROM python:3.8.6-buster
FROM python:3.9.6-buster
RUN apt-get update -y && apt-get upgrade -y && apt-get install libopus0 -y
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN pip install --upgrade pip

BIN
app/assets/tcard.wav Normal file

Binary file not shown.

View File

@ -1,458 +1,289 @@
# Import Dependencies
import os
import pymongo
import discord
from discord.ext import commands, tasks
import os, sys # OS Locations
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import yaml # Parser for yaml files for config settings.
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 # Slash Command features
# import discord_components # Additional Discord functions: buttons, menus, etc
from deepdiff import DeepDiff
import logging
# Set Intents
intents = discord.Intents.all()
intents.typing = True
intents.presences = True
intents.members = True
caches = discord.MemberCacheFlags.all()
## Define YAML functions
# Set Prefix
p = '¬'
def yaml_load(filepath:str):
### Loads a YAML file
with open(filepath, 'r') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
return data
# Define Global State Dictionary
state = {}
def yaml_dump(data:dict, filepath:str):
### Dumps a YAML file
with open(filepath, 'w') as file:
yaml.dump(data, file)
# Create Clients
dbClient = pymongo.MongoClient(host='geasbot-db', username=os.environ['MONGO_INITDB_ROOT_USERNAME'], password=os.environ['MONGO_INITDB_ROOT_PASSWORD'], authsource='admin', serverSelectionTimeoutMS=1000)
client = commands.Bot(command_prefix=p, description=f'Geas Server Bot v {os.getenv("BOT_VERSION")}. This is a bot to facilitate setting up game channels on the Geas server. The prefix for the bot is {p}. You can interact with and manipulate game channels that have been created with this bot by @-mentioning the relevant role associated with the game.', intents=intents)
# Locate or create config file. Read from environment variables to locate file, and if missing or not valid then use default location.
configFile = os.getenv('CONFIG') if ((os.getenv('CONFIG').endswith('.yml') or os.getenv('CONFIG').endswith('.yaml')) and not os.getenv('CONFIG').endswith('config_blueprint.yml')) else './data/config.yml'
# Define Game Times Dictionary
gameTimes = {
'wed': 'WED',
'sunaft': 'SUN AFT',
'suneve': 'SUN EVE',
'oneshot': 'ONE SHOT'
}
if not os.path.exists(configFile): yaml_dump({},configFile)
# Reference Time Codes
def gameTime(arg):
return gameTimes.get(arg, 'OTHER')
# Locate or create data file. Same as above.
dataFile = os.getenv('DATA') if ((os.getenv('DATA').endswith('.yml') or os.getenv('DATA').endswith('.yaml')) and not os.getenv('DATA').endswith('data_blueprint.yml')) else './data/data.yml'
# List Time Codes
def timeSlotList():
l = []
for t in gameTimes:
l.append(gameTimes[t])
l.append('OTHER')
return l
if not os.path.exists(dataFile): yaml_dump({},dataFile)
# Lookup Game Time Slot
def dbFindTimeslot(guild, role):
db = dbClient[str(guild.id)]
if role.name.split(': ',maxsplit=1)[0] not in timeSlotList():
raise commands.CommandError(f'Invalid lookup value: {role.mention} is not a valid game role.')
try:
for c in db.list_collection_names():
ret = db[c].find_one({'role':role.id})
if ret != None and c != 'settings':
return c
except:
return role.name.split(': ',maxsplit=1)[0]
# 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'
# Lookup Category from Role
def dbLookupRole(guild, role):
db = dbClient[str(guild.id)]
colName = dbFindTimeslot(guild, role)
try:
catID = db[colName].find_one({'role':role.id})['category']
except:
for cat in guild.categories:
if cat.name == role.name:
catID = cat.id
break
try:
return guild.get_channel(catID)
except:
raise commands.CommandError('Error: The game\'s corresponding category cannot be matched.')
if not os.path.exists(lookupFile): yaml_dump({},lookupFile)
# Get Settings from DB
def dbGetSettings():
try:
for db in dbClient.list_database_names():
if db not in ['admin','config','local']:
state[db] = {
'settings': dbClient[db]['settings'].find_one({'_id':0}).copy()
}
print('Imported settings from database to local state.',state)
except Exception as err:
print('Failed to get Settings from Database due to error: ',err)
# 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'
# Get list of game role IDs on server
def gameRoleIDList(guild):
l = []
for r in guild.roles:
if r.name.split(': ',maxsplit=1)[0] in timeSlotList():
l.append(r.id)
return l
if not os.path.exists(gmFile): yaml_dump({},gmFile)
# Get list of game role IDs in DB
def gameRoleDBList(guild):
dbName = str(guild.id)
db = dbClient[dbName]
l = []
try:
for ts in timeSlotList():
for e in db[ts].find():
l.append(e['role'])
return l
except:
pass
# Locate or create Categories lookup file. Same as above.
categoriesFile = os.getenv('CATEGORIES') if os.getenv('CATEGORIES').endswith('.yml') or os.getenv('CATEGORIES').endswith('.yaml') else './data/categories.yml'
# Get list of game category IDs in DB
def gameCategoryDBList(guild):
dbName = str(guild.id)
db = dbClient[dbName]
l = []
try:
for ts in timeSlotList():
for e in db[ts].find():
l.append(e['category'])
return l
except:
pass
if not os.path.exists(categoriesFile): yaml_dump({},categoriesFile)
# Get list of GM IDs in DB
def gmDBList(guild):
dbName = str(guild.id)
db = dbClient[dbName]
l = []
try:
for ts in timeSlotList():
for e in db[ts].find():
l.append(e['gm'])
return l
except:
pass
# Locate or create Pitches data file. Same as above.
pitchesFile = os.getenv('PITCHES') if os.getenv('PITCHES').endswith('.yml') or os.getenv('PITCHES').endswith('.yaml') else './data/pitches.yml'
# Sync Games on Server
def syncGames(guild):
dbName = str(guild.id)
db = dbClient[dbName]
try:
for ts in timeSlotList():
for e in db[ts].find():
if e['role'] not in gameRoleIDList(guild):
db[ts].delete_many({'role':e['role']})
for r in guild.roles:
if r.name.split(': ',maxsplit=1)[0] in timeSlotList():
if r.id not in gameRoleDBList(guild):
gameName = r.name.split(': ',maxsplit=1)[1]
colName = r.name.split(': ',maxsplit=1)[0]
for c in guild.categories:
if c.name == r.name:
break
permissions = c.overwrites
for p in permissions:
if isinstance(p,discord.Member) and permissions[p].manage_channels:
break
g = {
'game': gameName,
'gm': p.id,
'category': c.id,
'capacity': 5,
'role': r.id
}
db[colName].replace_one({'role':g['role']}, g, upsert=True)
for c in guild.categories:
if c.name.split(': ',maxsplit=1)[0] in timeSlotList():
if c.id not in gameCategoryDBList(guild):
for r in guild.roles:
if r.name == c.name:
break
if r.name != c.name:
break
colName = r.name.split(': ',maxsplit=1)[0]
db[colName].update_one({'role':r.id},{'$set':{'category':c.id}})
for p in c.overwrites:
if isinstance(p,discord.Member) and c.overwrites[p].manage_channels:
break
if p.id not in gmDBList(guild):
for r in guild.roles:
if r.name == c.name:
break
if r.name != c.name:
break
colName = r.name.split(': ',maxsplit=1)[0]
db[colName].update_one({'role':r.id},{'$set':{'gm':p.id}})
print(f'Synced database for server {guild.name}')
except:
pass
if not os.path.exists(pitchesFile): yaml_dump({},pitchesFile)
# Sync for Each Guild
@tasks.loop(hours=1.0)
async def syncGuilds():
try:
for guild in client.guilds:
syncGames(guild)
print('Synced database with server games list.')
except:
pass
l = [dataFile, lookupFile, gmFile, categoriesFile, configFile, pitchesFile]
if len(set(l)) != len(l): raise Exception('Config Error: there is a clash between two file names.')
@client.event
async def on_command_error(ctx,error):
if isinstance(error, commands.CommandNotFound):
await ctx.send(f'Invalid command. Please use `{p}help` to see a list of available commands.')
# Locate Cogs Directory
cogsDir = 'cogs'
## Logging configuration imported boilerplate from Discord Py Docs
logger = logging.getLogger('discord')
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
logger.addHandler(handler)
#### Dynamic Prefixes
def getPrefix(client, message):
conf = yaml_load(configFile)
return conf[str(message.guild.id)]['prefix']
# Define Clients
client = commands.Bot(
intents=discord.Intents.all(),
command_prefix=getPrefix,
description=f'Geas Server Bot v.{os.getenv("BOT_VERSION")}.\n\nThis bot designed to automate the management of key features of the Geas Discord Server. It is written by Vivek Santayana. You can find the source code at https://git.vsnt.uk/viveksantayana/geas-bot.'
)
slash = SlashCommand(
client,
sync_commands = True,
sync_on_cog_reload = True
)
# sync_on_reload is an important parameter that will become relevant when having to reload cogs on changing bot configs.
# Define Config keys
def setConfig(guild:discord.Guild):
#### Check if the bot is missing any config entries for the guilds it is in, and if it is then add it in.
#### N.B.: The way the commands work, the bot will have to list specific guilds in which it will synchronise the commands when it is defining them. So it needs to give a list of all the guilds it is part of when the bot loads, which it draws from the config files.
#### Because the bot connects to Discord after it loads, it will not be able to introspect and see what guilds it is part of before the commands are first loaded, and it will only add new guilds to the config files after it has already connected.
#### The Bot will first need to set up all of its configurations, and then begin loading all other commands once it is ready.
conf = yaml_load(configFile)
guildStr = str(guild.id)
if guildStr not in conf:
conf[guildStr] = {}
gDict = conf[guildStr]
if 'channels' not in gDict or type(gDict['channels']) is not dict or None in gDict['channels']:
gDict['channels'] = {}
cDict = gDict['channels']
if 'mod' not in cDict:
if guild.system_channel is None:
p = len(guild.channels)
c = None
for t in guild.text_channels:
if t.position < p:
p = t.position
cDict['mod'] = t.id
else:
await ctx.channel.send(error)
cDict['mod'] = guild.system_channel.id
if 'configured' not in gDict or type(gDict['configured']) is not bool:
gDict['configured'] = False
if 'membership' not in gDict or type(gDict['membership']) is not list or None in gDict['membership']:
gDict['membership'] = []
if 'name' not in gDict or gDict['name'] != guild.name:
gDict['name'] = guild.name
if 'owner' not in gDict or gDict['owner'] != guild.owner_id:
gDict['owner'] = guild.owner_id
if 'prefix' not in gDict:
gDict['prefix'] = '-'
if 'roles' not in gDict or (type(gDict['roles']) is not dict or None in gDict['roles']):
gDict['roles'] = {}
rDict = gDict['roles']
if 'admin' not in rDict or (type(rDict['admin']) is not list or len(rDict['admin']) == 0 or None in rDict['admin']):
rDict['admin'] = []
for role in guild.roles:
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'] = {}
yaml_dump(conf, configFile)
# On Ready
@client.event
async def on_ready():
await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f'{p} commands'))
print(f'Bot has logged in as {client.user.name} responding to prefix {p}')
print(f'Geas Server Bot version {os.getenv("BOT_VERSION")} by Vivek Santayana')
dbGetSettings()
syncGuilds.start()
def clearConfig(guildKey:str):
#### Delete Configs for Guilds that the Bot is no longer in
conf = yaml_load(configFile)
if discord.utils.find(lambda g: str(g.id) == guildKey, client.guilds) is None:
del conf[guildKey]
yaml_dump(conf, configFile)
# Check Bot Role is Defined
def botrole_is_defined():
async def predicate(ctx):
try:
return state[str(ctx.guild.id)]['settings']['botrole'] != None
except:
raise commands.CommandError(f'Bot role has not been defined. Please set the bot role using the `{p}definebotrole` command first.')
return commands.check(predicate)
def checkConfig(guild:discord.Guild):
#### Checks the completeness of the configurations of the current guild. returns (bool of config state, [list of missing keys])
#### The bot does this by comparing the keys and structure in the ./data/config_blueprint.yml file. This offers the bot some level of abstraction in doing this.
#### DeepDiff between the blueprint and the configs, and see if any values have been removed (thus missing)
guildStr = str(guild.id)
conf = yaml_load(configFile)
unconfigured = []
blueprint = yaml_load('./data/config_blueprint.yml')
diff = DeepDiff(blueprint['guild_id_string'], conf[guildStr])
if 'dictionary_item_removed' in diff:
for i in diff['dictionary_item_removed']:
s = i.split("'")
for item in list(i.split("'")):
if '[' in item or ']' in item:
s.remove(item)
if 'notifications' not in '.'.join(s):
unconfigured.append('.'.join(s))
if 'type_changes' in diff:
for i in diff['type_changes']:
s = i.split("'")
for item in list(i.split("'")):
if '[' in item or ']' in item:
s.remove(item)
if 'notifications' not in '.'.join(s):
unconfigured.append('.'.join(s))
if 'iterable_item_removed' in diff:
for i in diff['iterable_item_removed']:
s = i.split("'")
for item in list(i.split("'")):
if '[' in item or ']' in item:
s.remove(item)
unconfigured.append('.'.join(s))
for i in blueprint['guild_id_string']:
if i not in blueprint['guild_id_string']['meta']['strict'] and isinstance(blueprint['guild_id_string'][i], dict):
if i in conf[guildStr] and isinstance(conf[guildStr][i], dict) and len(conf[guildStr][i]) < 1:
unconfigured.append(i)
if 'meta' in unconfigured:
unconfigured.remove('meta')
if 'initialised' in unconfigured:
unconfigured.remove('initialised')
if 'configured' in unconfigured:
unconfigured.remove('configured')
output = list(set(unconfigured))
if len(output) > 0:
conf[guildStr]['configured'] = False
elif len(output) == 0:
conf[guildStr]['configured'] = True
yaml_dump(conf,configFile)
return conf[guildStr]['configured'], output
# Check if invoked in valid game channel
def in_game_channel(ctx):
categoriesList = []
db = dbClient[str(ctx.guild.id)]
try:
try:
for c in db.list_collection_names():
ret = db[c].find({})
for e in ret:
if ctx.channel.category == ctx.guild.get_channel(e['category']):
return True
except:
if ctx.channel.category.name.split(': ',maxsplit=1)[0] in timeSlotList():
return True
except:
pass
if ctx.command.name == 'deletegame':
raise commands.CommandError('Error: you must invoke this command in a text channel corresponding to the game you are attempting to delete.')
raise commands.CommandError(f'Error: you must invoke this command in the text channel of your game.')
def parseConfigCheck(missingKeys: list):
output = 'Configuration values for the following mandatory parameters have not been defined:\n\n'
for entry in missingKeys:
if '.' in entry:
e1, e2 = entry.split('.')
if e1 == 'channels':
if e2 == 'help':
output = ''.join([output, f"- The `help channel` for the Bot to monitor and notify Committee\n"])
if e2 == 'mod':
output = ''.join([output, f"- The `moderation channel` for the bot's outputs\n"])
if e2 == 'signup':
output = ''.join([output, f"- The `sign-up channel` for the membershp registration\n"])
if e1 == 'roles':
if e2 == 'admin':
output = ''.join([output, f"- The `administrator` role(s) for the guild\n"])
if e2 == 'committee':
output = ''.join([output, f"- The `Committee` role for the guild\n"])
if e2 == 'bot':
output = ''.join([output, f"- The `Bot` role for the guild\n"])
if e2 == 'newcomer':
output = ''.join([output, f"- The `Newcomer` role for the guild\n"])
if e2 == 'returning':
output = ''.join([output, f"- The `Returning Player` role for the guild\n"])
if e2 == 'student':
output = ''.join([output, f"- The `Student` role for the guild\n"])
if entry == 'membership':
output = ''.join([output, f"- `Membership roles`: the Channel needs at least one membership role\n"])
if entry == 'name':
output = ''.join([output, f"- The guild's `name`\n"])
if entry == 'owner':
output = ''.join([output, f"- The guild's `owner`\n"])
if entry == 'prefix':
output = ''.join([output, f"- The guild's `prefix` for native (non-`/`) commands.\n"])
if entry == 'timeslots':
output = ''.join([output, f"- Available `timeslots` for server games.\n"])
return output
# setupGame command: <when>, <@GM> <capacity>, <Name of game>
@client.command(name='setupgame', aliases=['setup','gamesetup','creategame','gamecreate','create'], description='Use this command to set up the roles and channels for a new game. The syntax is `setup {wed|sunaft|suneve|oneshot|other} {@GM Name} {Capacity} {Name of game}`')
@commands.has_permissions(administrator=True)
@botrole_is_defined()
async def setupGame(ctx,arg1,arg2, arg3: int, *, arg4):
if not (arg2.startswith('<@') and not (arg2.startswith('<@&'))):
raise commands.CommandError('Invalid argument. The second parameter must @ the GM.')
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = gameTime(arg1.lower())
if colName == 'OTHER':
await ctx.channel.send('Time code not recognised. Game will be categorised as \'Other\'.')
await ctx.channel.trigger_typing()
gm = int(arg2.replace('<@', '').replace('>', '').replace('!', ''))
gmMember = await ctx.guild.fetch_member(gm)
cap = int(arg3)
gameTitle = f'{colName}: {arg4}'
roleExists = False
for r in ctx.guild.roles:
if r.name == gameTitle:
roleExists = True
break
if not roleExists:
r = await ctx.guild.create_role(name=gameTitle)
await r.edit(mentionable=True)
await gmMember.add_roles(r)
categoryExists = False
for c in ctx.guild.categories:
if c.name == gameTitle:
categoryExists = True
break
ret = state[dbName]['settings']
bots = ctx.guild.get_role(ret['botrole'])
permissions = {
ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False),
r: discord.PermissionOverwrite(read_messages=True),
bots: discord.PermissionOverwrite(read_messages=True),
gmMember: 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),
}
if not categoryExists:
c = await ctx.guild.create_category(name=gameTitle, overwrites=permissions)
await c.create_voice_channel(name=f'voice: {gameTitle}', topic=f'Default voice channel for {gameTitle}')
t = await c.create_text_channel(name=f'text: {gameTitle}', topic=f'Default text channel for {gameTitle}')
await ctx.channel.send(f'Game {r.mention} has been created with GM {gmMember.mention} and space for {cap} players.')
await t.send(f'Hello, {gmMember.mention}! Your game channels for {gameTitle} have now been set up.\nYou can also ping your players or interact with the bot commands by mentioning the {r.mention} role.')
else:
await c.edit(overwrites=permissions)
tPos = len(ctx.guild.channels)
tFirst = None
vExists = False
for t in c.text_channels:
if t.position <= tPos:
tFirst = t
tPos = t.position
await t.edit(sync_permissions=True)
for v in c.voice_channels:
await v.edit(sync_permissions=True)
vExists = True
await ctx.channel.send(f'The category for game {r.mention} has been reset for GM {gmMember.mention} with space for {cap} players.')
if tFirst == None:
tFirst = await c.create_text_channel(name=f'text: {gameTitle}', topic=f'Default text channel for {gameTitle}')
if not vExists:
await c.create_voice_channel(name=f'voice: {gameTitle}', topic=f'Default voice channel for {gameTitle}')
await tFirst.send(f'Hello, {gmMember.mention}! Your game channels for {gameTitle} have now been set up.\nYou can also ping your players or interact with the bot commands by mentioning the {r.mention} role.')
g = {
'game': arg4,
'gm': gm,
'capacity': cap,
'category': c.id,
'role': r.id
}
try:
db[colName].replace_one({'role':g['role']}, g , upsert=True)
except:
pass
def loadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.load_extension('.'.join(path))
@setupGame.error
async def clear_error(ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.channel.send('The number of players in the game must be an integer.')
raise error
def unloadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.unload_extension('.'.join(path))
# defineBotrole command
@client.command(name='definebotrole', aliases=['setbotrole','botrole','botroleset','botroledefine','br'], description='This is a set-up command to define which role is the botrole that the dice bot use. The syntax is `botrole {@Role}`')
@commands.has_permissions(administrator=True)
async def defineBotrole(ctx, arg):
if not arg.startswith('<@&'):
raise commands.CommandError('Invalid argument. The argument must @ a role.')
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = 'settings'
r = ctx.guild.get_role(int(arg[3:-1]))
if dbName not in state:
state[dbName] = {}
if 'settings' not in state[dbName]:
state[dbName]['settings'] = {}
ret = state[dbName]['settings']
ret['_id'] = 0
if 'botrole' not in ret:
await ctx.channel.send(f'Bot role for server {ctx.guild.name} not set. Setting {r.mention} to bot role now.')
else:
await ctx.channel.send(f'Bot role for server {ctx.guild.name} has already been set to {ctx.guild.get_role(ret["botrole"]).mention}. Updating to {r.mention}.')
ret['botrole'] = r.id
try:
db[colName].update_one({'_id':0},{'$set':{'botrole': r.id}}, upsert=True)
except:
pass
def reloadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.reload_extension('.'.join(path))
# deleteGame command
@client.command(name='deletegame', aliases=['delete','del', 'removegame', 'delgame', 'gamedel', 'gamedelete'], description='Use this command to delete the role and associated channels for a game. The syntax is `delete {@Game Role}`. **It must be called in a text channel inside the relevant game.**')
@commands.has_permissions(administrator=True)
@commands.check(in_game_channel)
async def deleteGame(ctx, arg):
if not arg.startswith('<@&'):
raise commands.CommandError('Invalid argument. The argument must @ a role.')
r = ctx.guild.get_role(int(arg[3:-1]))
cat = dbLookupRole(ctx.guild, r)
dbName = str(ctx.guild.id)
db = dbClient[dbName]
await ctx.channel.trigger_typing()
try:
colName = dbFindTimeslot(ctx.guild,r)
except:
raise commands.CommandError(f'Invalid argument. {r.mention} role is not a valid game role.')
for t in ctx.guild.text_channels:
if t.category == cat:
await t.delete()
for v in ctx.guild.voice_channels:
if v.category == cat:
await v.delete()
await cat.delete()
try:
db[colName].delete_one({'role':r.id})
except:
pass
await r.delete()
# reset command reset wed|sunaft|suneve|oneshot|all|other
@client.command(name='reset', aliases=['deleteall','delall','cleargames','clear'], description='This deletes all games in a particular time slot category. This is a very powerful command. Be careful when you use it. The syntax is `reset {wed|sunaft|suneve|oneshot|other|all}`')
@commands.has_permissions(administrator=True)
async def reset(ctx, *args):
delList = []
dbName = str(ctx.guild.id)
db = dbClient[dbName]
await ctx.channel.trigger_typing()
for a in args:
if a.lower() not in ['all', 'other', 'wed', 'sunaft', 'suneve', 'oneshot']:
raise commands.CommandError(f'Invalid argument. {a} is not a valid flag for the command.')
if a.lower() == 'all':
for l in ['wed', 'sunaft', 'suneve', 'oneshot','other']:
if l not in delList:
delList.append(l)
else:
if a.lower() not in delList:
delList.append(a.lower())
for d in delList:
colName = gameTime(d)
try:
cur = db[colName].find({})
for g in cur:
await ctx.guild.get_role(g['role']).delete()
cat = ctx.guild.get_channel(g['category'])
for c in cat.channels:
await c.delete()
await cat.delete()
db[colName].deleteMany({})
except:
for r in ctx.guild.roles:
if r.name.startswith(colName):
for cat in ctx.guild.categories:
if cat.name == r.name:
for c in cat.channels:
await c.delete()
await cat.delete()
await r.delete()
await ctx.channel.send(f'All games for {colName} have been deleted.')
# Migrate Guild from Old Server Settings to New Settings
@client.command(name='migrate', aliases=['migrategames','migratedata'], description='A set-up command to migrate games from the old server settings to the new server settings using the database for the first time.')
@commands.has_permissions(administrator=True)
async def migrateData(ctx):
await ctx.channel.trigger_typing()
dbName = str(ctx.guild.id)
db = dbClient[dbName]
gNum = 0
for r in ctx.guild.roles:
if r.name.split(': ',maxsplit=1)[0] in timeSlotList():
gNum += 1
await r.edit(mentionable=True)
gameName = r.name.split(': ',maxsplit=1)[1]
colName = r.name.split(': ',maxsplit=1)[0]
for c in ctx.guild.categories:
if c.name == r.name:
break
permissions = c.overwrites
for p in permissions:
if isinstance(p,discord.Member) and permissions[p].manage_channels:
break
g = {
'game': gameName,
'gm': p.id,
'capacity': 5,
'category': c.id,
'role': r.id
}
try:
db[colName].replace_one({'role':g['role']}, g , upsert=True)
except:
raise commands.CommandError('Error: Database connection failed. Could not migrate games to database.')
await ctx.channel.send(f'Finished migrating {gNum} games onto the database.')
# Import Cogs
for cogfile in os.listdir('./cogs'):
def loadCogs(cogClass:str = '--all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == '--all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
client.load_extension(f'cogs.{cogfile[:-3]}')
loadCog(f'./{cogsDir}/{category}/{cogfile}')
# Run Bot
client.run(os.getenv('TEST_TOKEN'))
def unloadCogs(cogClass:str = '--all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == '--all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
unloadCog(f'./{cogsDir}/{category}/{cogfile}')
def reloadCogs(cogClass:str = '--all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == '--all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
reloadCog(f'./{cogsDir}/{category}/{cogfile}')
loadCogs('controlcommands')
loadCogs('events')
loadCogs('membership')
loadCogs('botcommands')
loadCogs('slashcommands')
if yaml_load(configFile):
if any([yaml_load(configFile)[x]['timeslots'] for x in yaml_load(configFile)]):
loadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py')
if any(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]):
loadCog(f'./{cogsDir}/slashcommands/secondary/game_create.py')
if yaml_load(lookupFile):
if any([x for x in yaml_load(lookupFile).values()]):
loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
loadCog(f'./{cogsDir}/slashcommands/secondary/tcard.py')
loadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
if yaml_load(pitchesFile):
loadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
if any([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]):
loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py')
client.run(os.getenv('BOT_TOKEN'))

View File

@ -1,177 +0,0 @@
import os
import pymongo
import discord
from discord.ext import commands
from bot import dbClient, p, state, gameTimes, gameTime, timeSlotList, dbFindTimeslot, dbLookupRole, in_game_channel
# Lookup GMs
def gmLookup(guild, member):
gamesList = []
db = dbClient[str(guild.id)]
try:
for c in db.list_collection_names():
ret = db[c].find({'gm':member.id})
for e in ret:
gamesList.append(guild.get_role(e['role']))
except:
for cat in guild.categories:
if cat.name.split(': ',maxsplit=1)[0] in timeSlotList():
for p in cat.overwrites:
if cat.overwrites[member].manage_channels:
for r in guild.roles:
if r.name == cat.name:
gamesList.append(r)
break
break
finally:
return gamesList
# Check if User is a GM
def user_is_GM(ctx):
if ctx.author.guild_permissions.administrator:
return True
if len(gmLookup(ctx.guild,ctx.author)) > 0:
return True
# Check if User is a Player
def user_is_Player(ctx):
gamesList = []
db = dbClient[str(ctx.guild.id)]
try:
for c in db.list_collection_names():
ret = db[c].find({})
for e in ret:
gamesList.append(ctx.guild.get_role(e['role']))
except:
for r in ctx.guild.roles:
if r.name.split(': ',maxsplit = 1)[0] in timeSlotList():
gamesList.append(r)
if set(gamesList) & set(ctx.author.roles):
return True
raise commands.CommandError('Error: You are not currently playing in any game.')
class GameManagement(commands.Cog, name='Game Management Commands'):
def __init__(self, client):
self.client = client
# GM Kick Command
@commands.command(name='kickplayer', aliases=['kick','removeplayer','dropplayer','drop', 'remove'],description='This removes a player from your game. Can only be invoked by the GM or a server admin. The syntax is `kickplayer {@Player}`. **This command must be called inside the text channel of the game you are kicking the player from.**. *The action gets logged with the Committee so we can keep track of who is in wose game.*')
@commands.check(user_is_GM)
@commands.check(in_game_channel)
async def kickPlayer(self, ctx, arg):
if not (arg.startswith('<@') and not (arg.startswith('<@&'))):
raise commands.CommandError('Invalid argument. The second parameter must @ the Player.')
if not ctx.author.permissions_in(ctx.channel.category).manage_channels:
raise commands.CommandError('You are not authorised to use this command here as you are not the GM.')
await ctx.message.delete()
await ctx.channel.trigger_typing()
permissions = ctx.channel.category.overwrites
for p in permissions:
if isinstance(p,discord.Role) and p.name == ctx.channel.category.name:
break
u = await ctx.guild.fetch_member(int(arg.replace('<@', '').replace('>', '').replace('!', '')))
await u.remove_roles(p)
isPlayer = False
for playerRoles in u.roles:
if playerRoles.name.split(': ',maxsplit=1)[0] in timeSlotList():
isPlayer = True
break
if not isPlayer:
for r in ctx.guild.roles:
if r.name == 'Players':
break
if r.name == 'Players':
await u.remove_roles(r)
for cr in ctx.guild.roles:
if cr.name == 'Committee':
break
await ctx.channel.send(f'{u.mention} has been kicked from the game. This has been logged with the {cr.mention}.')
for tc in ctx.guild.text_channels:
if tc.name.split('-',maxsplit=1)[1] == 'moderator-logs':
break
await tc.send(f'Hey {cr.mention}, {u.mention} has been kicked from the {p.mention} game by GM {ctx.author.mention}.')
@kickPlayer.error
async def clear_kick_error(self, ctx, error):
if isinstance(error, commands.CheckFailure):
await ctx.channel.send('You are not authorised to use this command as you are not a GM.')
raise error
# GM Add Command
@commands.command(name='addplayer', aliases=['add'],description='This command adds a player to your game. Can only be invoked by the GM for the game. The syntax is `addplayer {@Player} {@Game Role}`. As you cannot @-mention someone who cannot already see your channel, you are **not restricted** to use this command in a text channel belonging to your game. *The action gets logged with the Committee so we can keep track of who is in wose game.*')
@commands.check(user_is_GM)
async def addPlayer(self, ctx, arg1, arg2):
if not (arg1.startswith('<@') and not (arg1.startswith('<@&'))):
raise commands.CommandError('Invalid argument. The first parameter must @ the Player.')
if not arg2.startswith('<@&'):
raise commands.CommandError('Invalid argument. The second parameter must @ the game role.')
r = ctx.guild.get_role(int(arg2[3:-1]))
if r.name.split(': ',maxsplit=1)[0] not in timeSlotList():
raise commands.CommandError('Error: the role is not a valid game role.')
cat = dbLookupRole(ctx.guild,r)
if not ctx.author.permissions_in(cat).manage_channels:
raise commands.CommandError('You are not authorised to use this command as you are not the GM for the game.')
await ctx.message.delete()
await ctx.channel.trigger_typing()
u = await ctx.guild.fetch_member(int(arg1.replace('<@', '').replace('>', '').replace('!', '')))
for rl in ctx.guild.roles:
if rl.name == 'Players':
break
await u.add_roles(r,rl)
tPos = len(ctx.guild.channels)
tFirst = None
for t in cat.text_channels:
if t.position <= tPos:
tFirst = t
tPos = t.position
for cr in ctx.guild.roles:
if cr.name == 'Committee':
break
for tc in ctx.guild.text_channels:
if tc.name.split('-',maxsplit=1)[1] == 'moderator-logs':
break
await tFirst.send(f'{u.mention} has joined the game. Welcome! This has been logged with the {cr.mention}.')
await ctx.channel.send(f'{u.mention} has been added to the game {r.mention}.')
await tc.send(f'Hey {cr.mention}, {u.mention} was added to the {r.mention} game by {ctx.author.mention}.')
@addPlayer.error
async def clear_add_error(self, ctx, error):
if isinstance(error, commands.CheckFailure):
await ctx.channel.send('You are not authorised to use this command as you are not a GM.')
raise error
# Leave Game Command
@commands.command(name='leave',aliases=['leavegame','quit','quitgame','dropout'],description='This command is to leave the game you are in. **It must be invoked in the text channel of the game you are in.** *The action gets logged with the Committee so we can keep track of who is in wose game.*')
@commands.check(user_is_Player)
@commands.check(in_game_channel)
async def leaveGame(self, ctx):
await ctx.message.delete()
await ctx.channel.trigger_typing()
permissions = ctx.channel.category.overwrites
for p in permissions:
if isinstance(p,discord.Role) and p.name == ctx.channel.category.name:
break
await ctx.author.remove_roles(p)
isPlayer = False
for playerRoles in ctx.author.roles:
if playerRoles.name.split(': ',maxsplit=1)[0] in timeSlotList():
isPlayer = True
break
if not isPlayer:
for r in ctx.guild.roles:
if r.name == 'Players':
break
if r.name == 'Players':
await ctx.author.remove_roles(r)
for cr in ctx.guild.roles:
if cr.name == 'Committee':
break
await ctx.channel.send(f'{ctx.author.mention} has left the game. This has been logged with the {cr.mention}.')
for tc in ctx.guild.text_channels:
if tc.name.split('-',maxsplit=1)[1] == 'moderator-logs':
break
await tc.send(f'Hey {cr.mention}, {ctx.author.mention} has left the {p.mention} game by GM {ctx.author.mention}.')
# Cog Setup Function
def setup(client):
client.add_cog(GameManagement(client))

View File

@ -1,48 +0,0 @@
import discord
from discord.ext import commands, tasks
from datetime import datetime
def helpChannels(client):
l = []
for guild in client.guilds:
channel = discord.utils.find(lambda c: c.name == '⛑-help', guild.channels)
l.append(channel)
return l
def committeeRoles(client):
l = []
for guild in client.guilds:
role = discord.utils.find(lambda r: r.name == 'Committee', guild.roles)
l.append(role)
return l
def checkCommitteeRoles(author,committee):
if set(author.roles) & set(committee):
return True
class HelpNotifier(commands.Cog, name='Help Notifier Commands'):
def __init__(self,client):
self.client = client
# Message in Help channel event listener.
@commands.Cog.listener()
async def on_message(self,message):
if message.author.bot:
return
if checkCommitteeRoles(message.author, committeeRoles(self.client)):
return
if message.channel not in helpChannels(self.client):
return
committeeChannel = discord.utils.find(lambda t: t.name == '🗞-moderator-logs',message.guild.channels)
committeeRole = discord.utils.find(lambda c: c.name == 'Committee', message.guild.roles)
embed = discord.Embed(
title = message.content,
description = f'[Jump to Message]({message.jump_url})',
colour = discord.Colour.orange(),
)
embed.set_footer(text=datetime.now().strftime('%a %-d %b %y, %-I:%M %p'))
embed.set_author(name=message.author.display_name, icon_url=message.author.avatar_url)
await committeeChannel.send(f'Hey {committeeRole.mention}, {message.author.mention} has just posted the following message in the {message.channel.mention} channel', embed=embed)
def setup(client):
client.add_cog(HelpNotifier(client))

View File

@ -1,112 +0,0 @@
import discord
from discord.ext import commands, tasks
from datetime import datetime
from bot import dbClient, p, state, gameTimes, gameTime, timeSlotList, dbFindTimeslot, dbLookupRole
def gameCategories(client):
l = []
try:
for guild in client.guilds:
dbName = str(guild.id)
db = dbClient[dbName]
for colName in db.list_collection_names():
if colName != 'settings':
ret = db[colName].find()
for e in ret:
l.append(guild.get_channel(e['category']))
except:
for guild in client.guilds:
for cat in guild.categories:
if cat.name.split(': ',maxsplit=1)[0] in timeSlotList():
l.append(cat)
return l
def gameRoles(client):
l = []
try:
for guild in client.guilds:
dbName = str(guild.id)
db = dbClient[dbName]
for colName in db.list_collection_names():
if colName != 'settings':
ret = db[colName].find()
for e in ret:
l.append(guild.get_role(e['role']))
except:
for guild in client.guilds:
for role in guild.roles:
if role.name.split(': ',maxsplit=1)[0] in timeSlotList():
l.append(role)
return l
def committeeRoles(client):
l = []
for guild in client.guilds:
role = discord.utils.find(lambda r: r.name == 'Committee', guild.roles)
l.append(role)
return l
def memberRoles(client):
l = []
for guild in client.guilds:
role = discord.utils.find(lambda r: r.name == 'Life Members', guild.roles)
l.append(role)
role = discord.utils.find(lambda r: r.name == 'Members: Full Year', guild.roles)
l.append(role)
role = discord.utils.find(lambda r: r.name == 'Members: Semester 2', guild.roles)
l.append(role)
role = discord.utils.find(lambda r: r.name == 'Members: Semester 1', guild.roles)
l.append(role)
role = discord.utils.find(lambda r: r.name == 'New Member', guild.roles)
l.append(role)
role = discord.utils.find(lambda r: r.name == 'Temporary Access', guild.roles)
l.append(role)
return l
def checkCommitteeRoles(author,committee):
if set(author.roles) & set(committee):
return True
def checkMemberRoles(author,memberRoles):
if set(author.roles) & set(memberRoles):
return True
class MembershipRestriction(commands.Cog, name='Membership Restriction Protocol'):
def __init__(self,client):
self.client = client
# Event Listener for Message from Non-Member in Game Channels
@commands.Cog.listener()
async def on_message(self,message):
if message.author.bot:
return
guestRole = discord.utils.find(lambda g: g.name == 'Guest', message.guild.roles)
if guestRole.permissions.read_messages:
return
if message.channel.category not in gameCategories(self.client):
return
if checkCommitteeRoles(message.author, committeeRoles(self.client)) or message.guild.owner == message.author:
return
if checkMemberRoles(message.author, memberRoles(self.client)):
return
signupChannel = discord.utils.find(lambda c: c.name == '📋-membership-signups', message.guild.channels)
if message.channel.overwrites_for(message.author).manage_channels:
return
await message.channel.send(f'{message.author.mention} does not have a verified membership of Geas. Please submit your membership confirmation for verification in the {signupChannel.mention} to ensure you have access to your game.')
await message.channel.category.set_permissions(message.author, send_messages = False, connect = False)
await message.delete()
# Event Listener for Reinstating Permissions when Membership is Assigned
@commands.Cog.listener()
async def on_member_update(self,before,after):
if before.roles == after.roles:
return
if checkMemberRoles(after, memberRoles(self.client)):
for g in after.roles:
if g in gameRoles(self.client):
cat = dbLookupRole(after.guild, g)
if not cat.overwrites_for(after).send_messages and not cat.overwrites_for(after).manage_channels:
await cat.set_permissions(after, overwrite = None)
def setup(client):
client.add_cog(MembershipRestriction(client))

View File

@ -1,142 +0,0 @@
import discord
from discord.ext import commands, tasks
from datetime import datetime
def membershipSignupChannels(client):
l = []
for guild in client.guilds:
channel = discord.utils.find(lambda c: c.name == '📋-membership-signups', guild.channels)
l.append(channel)
return l
def committeeRoles(client):
l = []
for guild in client.guilds:
role = discord.utils.find(lambda r: r.name == 'Committee', guild.roles)
l.append(role)
return l
def checkCommitteeRoles(author,committee):
if set(author.roles) & set(committee):
return True
class MembershipVerification(commands.Cog, name='Membership Verification Commands'):
def __init__(self,client):
self.client = client
# Message in Membership Signup event listener.
@commands.Cog.listener()
async def on_message(self,message):
if message.channel in membershipSignupChannels(self.client) and message.author.id != self.client.user.id:
if message.attachments == []:
await message.author.send(f'**Error**: The message you posted in the {message.channel.name} channel of {message.guild.name} was invalid. Your post must contain a screensot of your proof of purchase for membership from EUSA.')
await message.delete()
return
await message.add_reaction('1')
await message.add_reaction('2')
await message.add_reaction('📅')
await message.add_reaction('📚')
await message.add_reaction('⚠️')
await message.add_reaction('🚫')
@commands.Cog.listener()
async def on_raw_reaction_add(self,payload):
if payload.user_id != self.client.user.id and self.client.get_channel(payload.channel_id) in membershipSignupChannels(self.client):
guild = await self.client.fetch_guild(payload.guild_id)
member = await guild.fetch_member(payload.user_id)
channel = await self.client.fetch_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
studentsRole = discord.utils.find(lambda g: g.name == 'Students', guild.roles)
semesterOneRole = discord.utils.find(lambda g: g.name == 'Members: Semester 1', guild.roles)
semesterTwoRole = discord.utils.find(lambda g: g.name == 'Members: Semester 2', guild.roles)
fullYearRole = discord.utils.find(lambda g: g.name == 'Members: Full Year', guild.roles)
channels = await guild.fetch_channels()
committeeChannel = discord.utils.find(lambda t: t.name == '🗞-moderator-logs', channels)
committeeRole = discord.utils.find(lambda c: c.name == 'Committee', guild.roles)
if not checkCommitteeRoles(member, committeeRoles(self.client)):
await member.send(f'**Error**: Only Committee members are authorised to react to posts on the {channel.name} channel for {guild.name}.')
await message.remove_reaction(payload.emoji.name, member)
return
if payload.emoji.name == '1':
await message.author.add_roles(semesterOneRole)
await message.add_reaction('')
await message.author.send(f'Your membership for {guild.name} has been verified and you have been assigned the role **Members: Semester 1**.')
return
if payload.emoji.name == '2':
await message.author.add_roles(semesterTwoRole)
await message.add_reaction('')
await message.author.send(f'Your membership for {guild.name} has been verified and you have been assigned the role **Members: Semester 2**.')
return
if payload.emoji.name == '📅':
await message.author.add_roles(fullYearRole)
await message.add_reaction('')
await message.author.send(f'Your membership for {guild.name} has been verified and you have been assigned the role **Members: Full Year**.')
return
if payload.emoji.name == '📚':
await message.author.add_roles(studentsRole)
await message.author.send(f'You have additionally been assigned the role **Students**.')
return
if payload.emoji.name == '⚠️':
embed = discord.Embed(
title = message.author.name,
description = f'[Jump to Message]({message.jump_url})',
colour = discord.Colour.orange(),
)
await message.author.send(f'Your membership for {guild.name} needs to be reviewed by a Committee member.')
await committeeChannel.send(f'Hey {committeeRole.mention}, there is a problem verifying the membership of {message.author.mention}.\nCould someone verify this person\'s membership manually via the EUSA portal and return to the message?', embed=embed)
return
if payload.emoji.name == '🚫':
embed = discord.Embed(
title = message.author.name,
description = f'[Jump to Message]({message.jump_url})',
colour = discord.Colour.red(),
)
await message.author.send(f'Your membership for {guild.name} could not be verified. Please make sure that your name and the kind of membership you have bought are visible in the screenshot you upload. Please contact a Committee member if you have any difficulties.')
await committeeChannel.send(f'Hey {committeeRole.mention}, verifying the membership of {message.author.mention} failed.', embed=embed)
return
@commands.Cog.listener()
async def on_raw_reaction_remove(self,payload):
if payload.user_id != self.client.user.id and self.client.get_channel(payload.channel_id) in membershipSignupChannels(self.client):
guild = await self.client.fetch_guild(payload.guild_id)
member = await guild.fetch_member(payload.user_id)
channel = await self.client.fetch_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
studentsRole = discord.utils.find(lambda g: g.name == 'Students', guild.roles)
semesterOneRole = discord.utils.find(lambda g: g.name == 'Members: Semester 1', guild.roles)
semesterTwoRole = discord.utils.find(lambda g: g.name == 'Members: Semester 2', guild.roles)
fullYearRole = discord.utils.find(lambda g: g.name == 'Members: Full Year', guild.roles)
channels = await guild.fetch_channels()
committeeChannel = discord.utils.find(lambda t: t.name == '🗞-moderator-logs', channels)
committeeRole = discord.utils.find(lambda c: c.name == 'Committee', guild.roles)
if not checkCommitteeRoles(member, committeeRoles(self.client)):
await message.remove_reaction(payload.emoji.name, member)
return
if payload.emoji.name == '1':
await message.author.remove_roles(semesterOneRole)
await message.remove_reaction('',self.client.user)
await message.author.send(f'Your role **Members: Semester 1** for {guild.name} has been removed.')
return
if payload.emoji.name == '2':
await message.author.remove_roles(semesterTwoRole)
await message.remove_reaction('',self.client.user)
await message.author.send(f'Your role **Members: Semester 2** for {guild.name} has been removed.')
return
if payload.emoji.name == '📅':
await message.author.remove_roles(fullYearRole)
await message.remove_reaction('',self.client.user)
await message.author.send(f'Your role **Members: Full Year** for {guild.name} has been removed.')
return
if payload.emoji.name == '📚':
await message.author.remove_roles(studentsRole)
await message.author.send(f'Your role **Students** for {guild.name} has been removed.')
return
if payload.emoji.name == '⚠️':
await message.author.send(f'Your membership for {guild.name} is being reviewed by a Committee member.')
return
if payload.emoji.name == '🚫':
await message.author.send(f'Your membership for {guild.name} is being reviewed by a Committee member.')
return
def setup(client):
client.add_cog(MembershipVerification(client))

View File

@ -1,190 +0,0 @@
import os
import pymongo
import discord
from discord.ext import commands, tasks
from bot import dbClient, p, state, gameTimes, gameTime, timeSlotList, dbFindTimeslot, dbLookupRole, syncGames
pitchState = {}
def pitchListening():
l = []
for guild in pitchState:
for slot in pitchState[guild]:
l.append(pitchState[guild][slot]['menuMessage'].id)
return l
emojiList = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'🔟',
'🇦',
'🇧',
'🇨',
'🇩',
'🇪',
'🇫',
'🇬',
'🇭',
'🇮',
'🇯'
]
class PitchMenu(commands.Cog, name='Pitch Menu Commands'):
def __init__(self,client):
self.client = client
# Pitch Run Command
@commands.has_permissions(administrator=True)
@commands.group(name='pitch', aliases=['pitches'], description='The command to run pitches. It has two subcommands. Syntax: `pitch {run|clear} {wed|sunaft|suneve|oneshot|other}`')
async def pitch(self, ctx):
if ctx.invoked_subcommand is None:
await ctx.send(f'Invalid subcommand. Please use either `{p}pitch run` or `{p}pitch clear`')
@pitch.command(name='run', aliases=['start','generate','setup'], description='Subcommand to set up pitches. Syntax: `pitch run {wed|sunaft|suneve|oneshot|other}`')
async def pitch_run(self, ctx, arg):
if arg.lower() not in ['all', 'other', 'wed', 'sunaft', 'suneve', 'oneshot']:
raise commands.CommandError('Invalid argument. {arg} is not a valid flag for the command.')
syncGames(ctx.guild)
await ctx.message.delete()
await ctx.channel.trigger_typing()
## Constructing Pitch State Dictionary
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = gameTime(arg.lower())
if dbName not in pitchState:
pitchState[dbName] = {}
if colName not in pitchState[dbName]:
pitchState[dbName][colName] = {}
pitchState[dbName][colName]['entries'] = []
# Try database queries
try:
cur = db[colName].find().sort('game')
for entry in cur:
gameDict = {}
gameDict['game'] = entry['game']
gameDict['gm'] = await ctx.guild.fetch_member(entry['gm'])
gameDict['role'] = discord.utils.find(lambda m: m.id == entry['role'],ctx.guild.roles)
gameDict['capacity'] = entry['capacity'] if entry['capacity'] != None else 5
gameDict['signups'] = 0
cat = discord.utils.find(lambda m: m.id == entry['category'], ctx.guild.categories)
tFirst = None
tPos = len(ctx.guild.channels)
for t in cat.text_channels:
if t.position <= tPos:
tFirst = t
tPos = t.position
gameDict['textchannel'] = tFirst
pitchState[dbName][colName]['entries'].append(dict(gameDict))
# Infer from server if database fails
except:
for r in ctx.guild.roles:
if r.name.startswith(colName):
gameDict = {}
gameDict['game'] = r.name.split(': ',maxsplit=1)[1]
gameDict['role'] = r
gameDict['capacity'] = 5
gameDict['signups'] = 0
cat = discord.utils.find(lambda m: m.name == r.name, ctx.guild.categories)
for p in cat.overwrites:
if isinstance(p,discord.Member) and cat.overwrites[p].manage_channels:
gameDict['gm'] = p
break
tFirst = None
tPos = len(ctx.guild.channels)
for t in cat.text_channels:
if t.position <= tPos:
tFirst = t
tPos = t.position
gameDict['textchannel'] = t
pitchState[dbName][colName]['entries'].append(dict(gameDict))
pitchState[dbName][colName]['entries'].sort(key= lambda m: m['game'])
# Begin Constructing the Menu
pitchState[dbName][colName]['headerMessage'] = await ctx.channel.send(f'**Game listing for {colName}**\n_ _\nThe following are the games that are being pitched. Please select your game by clicking on the emoji reaction at the bottom of the menu.\n_ _')
for e in pitchState[dbName][colName]['entries']:
e['message'] = await ctx.channel.send(f'{emojiList[pitchState[dbName][colName]["entries"].index(e)]} **{e["game"]}** (GM {e["gm"].mention}). {e["capacity"] - e["signups"]} spaces remaining.')
pitchState[dbName][colName]['menuMessage'] = await ctx.channel.send('_ _\n**Please select a game from the above list by clicking on the corresponding emoji reaction below.**')
for option in pitchState[dbName][colName]['entries']:
await pitchState[dbName][colName]['menuMessage'].add_reaction(emojiList[pitchState[dbName][colName]["entries"].index(option)])
@pitch.command(name='clear', aliases=['end','cancel','reset','delete'], description='Subcommand to clear pitches. {wed|sunaft|suneve|oneshot|other}')
async def pitch_clear(self, ctx, arg):
if arg.lower() not in ['all', 'other', 'wed', 'sunaft', 'suneve', 'oneshot']:
raise commands.CommandError(f'Invalid argument. {arg} is not a valid flag for the command.')
await ctx.message.delete()
await ctx.channel.trigger_typing()
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = gameTime(arg.lower())
for e in pitchState[dbName][colName]['entries']:
await e['message'].delete()
await pitchState[dbName][colName]['menuMessage'].delete()
await pitchState[dbName][colName]['headerMessage'].delete()
await ctx.send(f'Pitch menu for {colName} has been reset.')
pitchState[dbName][colName].clear
# Emoji Reaction Event Listeners
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload):
if payload.user_id != self.client.user.id:
if payload.message_id in pitchListening():
guildID = str(payload.guild_id)
guild = discord.utils.find(lambda g: g.id == payload.guild_id, self.client.guilds)
channel = discord.utils.find(lambda c: c.id == payload.channel_id, guild.channels)
author = await guild.fetch_member(payload.user_id)
for slot in pitchState[guildID]:
if pitchState[guildID][slot]['menuMessage'].id == payload.message_id:
break
message = await channel.fetch_message(pitchState[guildID][slot]['menuMessage'].id)
for reaction in message.reactions:
if reaction.emoji != payload.emoji.name:
i = emojiList.index(reaction.emoji)
if author in await reaction.users().flatten():
await reaction.remove(author)
i = emojiList.index(payload.emoji.name)
e = pitchState[guildID][slot]['entries'][i]
playerRole = discord.utils.find(lambda p: p.name == 'Players',guild.roles)
await author.add_roles(playerRole,e['role'])
e['signups'] += 1
contentString = f'{emojiList[i]} **{e["game"]}** (GM {e["gm"].mention}). {e["capacity"] - e["signups"] if e["signups"] <= e["capacity"] else 0} {"space" if e["capacity"] - e["signups"] == 1 else "spaces"} remaining.'
await e['message'].edit(content=f'~~{contentString}~~' if e['signups'] >= e['capacity'] else contentString)
await e['textchannel'].send(f'{author.mention} has joined the game.')
# Emoji Un-React Event Listener
@commands.Cog.listener()
async def on_raw_reaction_remove(self, payload):
if payload.user_id != self.client.user.id:
if payload.message_id in pitchListening():
guildID = str(payload.guild_id)
guild = discord.utils.find(lambda g: g.id == payload.guild_id, self.client.guilds)
channel = discord.utils.find(lambda c: c.id == payload.channel_id, guild.channels)
author = await guild.fetch_member(payload.user_id)
for slot in pitchState[guildID]:
if pitchState[guildID][slot]['menuMessage'].id == payload.message_id:
break
message = await channel.fetch_message(pitchState[guildID][slot]['menuMessage'].id)
i = emojiList.index(payload.emoji.name)
e = pitchState[guildID][slot]['entries'][i]
e['signups'] -= 1
contentString = f'{emojiList[i]} **{e["game"]}** (GM {e["gm"].mention}). {e["capacity"] - e["signups"] if e["signups"] <= e["capacity"] else 0} {"space" if e["capacity"] - e["signups"] == 1 else "spaces"} remaining.'
await e['message'].edit(content=f'~~{contentString}~~' if e['signups'] >= e['capacity'] else contentString)
await e['textchannel'].send(f'{author.mention} has left the game.')
await author.remove_roles(e['role'])
isPlayer = False
for role in author.roles:
if role.name.split(': ',maxsplit=1)[0] in timeSlotList():
isPlayer = True
break
if not isPlayer:
playerRole = discord.utils.find(lambda p: p.name == 'Players',guild.roles)
await author.remove_roles(playerRole)
def setup(client):
client.add_cog(PitchMenu(client))

View File

@ -0,0 +1,33 @@
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 bot import configFile, yaml_load, yaml_dump
### Cog for handling the non-Slash prefix for native Bot commands.
class Prefix(commands.Cog, name='Server Command Prefix'):
def __init__(self, client):
self.client = client
#### Check if user is an administrator
async def cog_check(self, ctx):
for role in ctx.author.roles:
if role.permissions.administrator:
return True
return ctx.author.guild_permissions.administrator
@commands.command(
name = 'changeprefix',
aliases = ['prefix'],
description = 'This command changes the prefix string for the native, non-slash command functionality of the bot. Defaults to `-`. It does not affect the workings of /commands.',
brief = 'Changes the bot prefix.'
)
async def _changePrefix(self, ctx, prefix:str = '-'):
conf = yaml_load(configFile)
conf[str(ctx.guild.id)]['prefix'] = prefix.lower()
yaml_dump(conf, configFile)
await ctx.send(f"```{self.client.user.name}'s prefix for native bot commands has been changed to `{prefix}` for the guild `{ctx.guild.name}`.\n\nNote: This will not affect /commands.```")
def setup(client):
client.add_cog(Prefix(client))

View File

@ -0,0 +1,188 @@
import os
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
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 # Slash Command features
from pprint import pprint
from bot import clearConfig, configFile, checkConfig, loadCog, loadCogs, setConfig, unloadCog, unloadCogs, yaml_dump, yaml_load, reloadCog, reloadCogs, pitchesFile, cogsDir, parseConfigCheck
##### Debug Cog
class Debug(commands.Cog, name='Debug Commands'):
def __init__(self, client):
self.client = client
#### Permission Check: Only available to the bot's maintainer.
async def cog_check(self, ctx:commands.Context):
conf = yaml_load(configFile)
for role in ctx.author.roles:
if 'maintainer' in conf[str(ctx.guild.id)]['roles']:
if role.id == conf[str(ctx.guild.id)]['roles']['maintainer']: return True
return ctx.author.id == ctx.guild.owner_id
@commands.command(
name='testconfig',
description='Tests the completeness of the configuration values of the current guild by comparing it to a configuration blueprint.',
brief='Tests config values for current guild.',
aliases=['configtest']
)
async def _testconfig(self, ctx:commands.Context):
checkConfig(ctx.guild)
status, output = checkConfig(ctx.guild)
conf = yaml_load(configFile)
if not status:
await ctx.reply(f"```The Bot's configurations are incomplete for the guild {ctx.guild.name}. Some limited functions will still be available, but most features cannot be used until the configurations are complete.\n{parseConfigCheck(output)}\nYou can set these configuration values using the `/config` command.```")
elif status:
await ctx.reply(f"```The Bot's configurations for the guild {ctx.guild.name} are in order. The Bot is ready to interact with the guild.```")
@commands.command(
name='reloadcogs',
description='Reloads cogs within the specified category, or provide `all` for all cogs. Default: `all`.',
brief='Reload multiple cogs by category.'
)
async def _reload(self, ctx, cog_category: str='--all'):
reloadCogs(cog_category)
await ctx.reply(f'````{cog_category}` cogs have been reloaded.```')
@commands.command(
name='unloadcogs',
description='Unload cogs within the specified category, or provide `all` for all cogs. Default: `all`.',
brief='Unload multiple cogs by category.'
)
async def _unloadcogs(self, ctx, cog_category: str='--all'):
unloadCogs(cog_category)
loadCogs(cog_category)
await ctx.reply(f'````{cog_category}` cogs have been unloaded.```')
@commands.command(
name='loadcogs',
description='Load cogs within the specified category, or provide `all` for all cogs. Default: `all`.',
brief='Load multiple cogs by category.'
)
async def _loadcogs(self, ctx, cog_category: str='--all'):
unloadCogs(cog_category)
loadCogs(cog_category)
await ctx.reply(f'````{cog_category}` cogs have been loaded.```')
@commands.command(
name='retrievecommands',
aliases=['slashcommands','retrieveslashcommands'],
description='Debugging command that retrieves all slash commands currently registered for this guild and this bot to the Python console.',
brief='Retrieves registered slash commands to console.'
)
async def _retrievecommands(self, ctx:commands.Context):
c = await utils.manage_commands.get_all_commands(
bot_id=self.client.user.id,
bot_token=os.getenv('BOT_TOKEN'),
guild_id=ctx.guild.id
)
pprint(c)
await ctx.reply(f'```All registered `/commands` have been fetched and sent to the Python console.```')
@commands.command(
name='deletecommand',
aliases=['removecommand','delcommand','removeslashcommand', 'clearcommand', 'clearslashcommand'],
description='Debugging command that deletes a specified slash command. Key parameters `--all` for all commands in guild and `--global` for all commands globally',
brief='Deletes slash command. Default: all local commands.'
)
async def _deleteCommand(self, ctx:commands.Context, command: str='--all'):
if command == '--all' or command == '-a':
await utils.manage_commands.remove_all_commands(
bot_id=self.client.user.id,
bot_token=os.getenv('BOT_TOKEN'),
guild_ids=[ ctx.guild.id ]
)
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,
bot_token=os.getenv('BOT_TOKEN'),
guild_ids=None
)
await utils.manage_commands.remove_all_commands(
bot_id=self.client.user.id,
bot_token=os.getenv('BOT_TOKEN'),
guild_ids=[ int(g) for g in yaml_load(configFile)]
)
await ctx.reply('```All slash commands have been deleted globally.```')
else:
c = await utils.manage_commands.get_all_commands(
bot_id=self.client.user.id,
bot_token=os.getenv('BOT_TOKEN'),
guild_id=ctx.guild.id
)
target = list(filter(lambda t: t['name'] == command, c))[0]['id']
await utils.manage_commands.remove_slash_command(
bot_id=self.client.user.id,
bot_token=os.getenv('BOT_TOKEN'),
guild_id=ctx.guild.id,
cmd_id=target
)
await ctx.reply(f'```Slash command {command} has been deleted for the guild {ctx.guild.name}.```')
@commands.command(
name='addcommand',
aliases=['installcommand','addslashcommand'],
description='Adds a slash command to the guild. Use keyword `--global` to add command globally.',
brief='Adds slash command'
)
async def _addCommand(self, ctx:commands.Context, command:str, key:str=''):
await utils.manage_commands.add_slash_command(
bot_id=self.client.user.id,
bot_token=os.getenv('BOT_TOKEN'),
guild_id= None if key == '--global' or key == '-g' else ctx.guild.id,
cmd_name=command,
description='No Description'
)
await ctx.reply(f'```The command /{command} has been added for the guild {ctx.guild.name}.```')
@commands.command(
name='clearconfig',
aliases=['configclear'],
description='Clears any redundant entries in the config file of guilds the bot is not in. Does not require any argument.',
brief='Clears redundant entries from config file.'
)
async def _clearconfig(self, ctx:commands.Context):
conf = yaml_load(configFile)
for key in list(conf):
clearConfig(key)
await ctx.reply(f'```Config entries for unknown guilds have been cleared.```')
@commands.command(
name='setconfig',
aliases=['configsetup'],
description='Creates a config entry for the current guild, and if there are existing entries sets default values. Ignores any values that have already been set. Does not require any argument as it infers the guild from the context in which it was called.',
brief='Sets config entry for the current guild.'
)
async def _setconfig(self, ctx:commands.Context):
setConfig(ctx.guild)
await ctx.reply(f'```Config entry has been added for guild `{ctx.guild.name}`.```')
@commands.command(
name='synccommands',
aliases=['syncallcommands','syncslashcommands','resynccommands','sync','resync','syncall','resyncall'],
description='Syncs all slash commands between the bot and the Server.',
brief='Resyncs slash commands.'
)
async def _synccommands(self, ctx:commands.Context):
await self.client.slash.sync_all_commands()
await ctx.reply(f'```All slash commands have been synced with the Server.```')
@commands.command(
name='pitchreset',
description='Debug feature that resets the pitches in case of any error. Clears pitch disables Pitch listeners.',
brief='Reset running pitches.',
aliases=['resetpitches', 'resetpitch']
)
async def _pitchreset(self, ctx:commands.Context):
yaml_dump({}, pitchesFile)
if self.client.get_cog('Pitch Listener') is not None:
unloadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
await ctx.reply('```Pitches have been hard reset.```')
def setup(client):
client.add_cog(Debug(client))

View File

@ -0,0 +1,129 @@
import os
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
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 # Slash Command features
from pprint import pprint
from bot import clearConfig, configFile, loadCog, loadCogs, setConfig, unloadCog, unloadCogs, yaml_dump, yaml_load, reloadCog, reloadCogs, categoriesFile, gmFile, lookupFile, dataFile
##### Migrate from Old Database Cog
class Migrate(commands.Cog, name='Migrate Command'):
def __init__(self, client):
self.client = client
#### Permission Check: Only available to the bot's maintainer.
async def cog_check(self, ctx:commands.Context):
conf = yaml_load(configFile)
for role in ctx.author.roles:
if 'maintainer' in conf[str(ctx.guild.id)]['roles']:
if role.id == conf[str(ctx.guild.id)]['roles']['maintainer']: return True
return ctx.author.id == ctx.guild.owner_id
@commands.command(
name='migrate',
aliases=['migrategames','migratedata'],
description='A command to migrate games from the old Guild settings to the new data structure by inferring the information of existing games from the existing roles and channels.',
brief='Infer guild data from games and channels'
)
async def _migrate(self, ctx:commands.Context):
guildStr = str(ctx.guild.id)
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
gms = yaml_load(gmFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
await ctx.reply(f'```Preparing to migrate guild `{ctx.guild.name}` from the data setup used by the old bot to the new bot. Inferring information from the channel structure.```')
await ctx.channel.trigger_typing()
gNum = 0
for r in ctx.guild.roles:
if r.name.split(': ', maxsplit=1)[0] in ['WED', 'SUN AFT', 'SUN EVE', 'ONE SHOT', 'OTHER']:
flag = True
game_title = r.name.split(': ', maxsplit=1)[1]
t = r.name.split(': ', maxsplit=1)[0]
if t == 'WED':
timeslot = {
'key': 'wedeve',
'value': 'Wednesday Evenings'
}
elif t == 'SUN AFT':
timeslot = {
'key': 'sunaft',
'value': 'Sunday Afternoons'
}
elif t == 'SUN EVE':
timeslot = {
'key': 'suneve',
'value': 'Sunday Evenings'
}
elif t == 'ONE SHOT':
timeslot = {
'key': 'oneshot',
'value': 'One Shots'
}
elif t == 'OTHER':
timeslot = {
'key': 'other',
'value': 'Other'
}
gNum += 1
await r.edit(
reason=f'`migrate` command issued by {ctx.author.display_name}',
mentionable=True,
colour=discord.Colour.green()
)
c = discord.utils.get(ctx.guild.categories, name=r.name)
if c is None:
raise(f'Channel category for game `{r.name}` was not found.')
await ctx.channel.trigger_typing()
else:
permissions = c.overwrites
for p in permissions:
if isinstance(p,discord.Member) and permissions[p].manage_channels: break
t = None
tPos = len(ctx.guild.channels)
for tc in c.text_channels:
if tc.position <= tPos:
tPos = tc.position
t = tc
if timeslot['key'] not in conf[guildStr]['timeslots']: conf[guildStr]['timeslots'][timeslot['key']] = timeslot['value']
if guildStr not in data: data[guildStr] = {}
if timeslot['key'] not in data[guildStr]: data[guildStr][timeslot['key']] = {}
data[guildStr][timeslot['key']][str(r.id)] = {
'game_title': game_title,
'gm': p.id,
'max_players': 5,
'min_players': None,
'current_players': 0,
'system': None,
'platform': None,
'role': r.id,
'category': c.id,
'text_channel': t.id,
'header_message': None
}
if guildStr not in lookup: lookup[guildStr] = {}
lookup[guildStr][str(r.id)] = {
'category': c.id,
'gm': p.id,
'time': timeslot['key'],
'game_title': game_title,
'text_channel': t.id
}
if guildStr not in gms: gms[guildStr] = {}
if str(p.id) not in gms[guildStr]: gms[guildStr][str(p.id)] = []
gms[guildStr][str(p.id)].append(r.id)
if str(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)
yaml_dump(data, dataFile)
await ctx.reply(f'```Finished migrating {gNum} games to the new bot. All games were set up using the minimal mandatory information. The maximum number of players in each game has been reset to 5. Where necessary, new timecodes have been created for games. Please reboot the bot to allow for the new configurations and data to sync, and remove the cog `migrate.py` to avoid future data clashes.```')
def setup(client):
client.add_cog(Migrate(client))

View File

@ -0,0 +1,30 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
#### Error Handler Event Listener
class on_command_error(commands.Cog, name='On Command Error'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_command_error(self, ctx, error):
if isinstance(error, discord.DiscordException):
if isinstance(error, commands.CommandNotFound):
print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} entered an invalid command <{ctx.message.clean_content}> in the guild {ctx.guild.name}.')
await ctx.reply(f'```Error: This is not a valid command.```')
elif isinstance(error, commands.CheckFailure):
print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} is not authorised to issue the command <{ctx.command.name}> in the guild {ctx.guild.name}.')
await ctx.reply(f'```Error: You are not authorised to issue this command.```')
else:
print(f'User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} received error: "{error}" when attempting to issue command <{ctx.command.name}> in the guild {ctx.guild.name}.')
await ctx.reply(f'```Error: {error}```')
def setup(client):
client.add_cog(on_command_error(client))

View File

@ -0,0 +1,30 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
#### Actions for the Bot to take on connecting to Discord.
class on_connect(commands.Cog, name='On Connect Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_connect(self): ## Actions for when bot logs in and enters ready state
print('Bot has connected.')
# logging.info('Bot has connected.')
await self.client.change_presence(
status = discord.Status.online,
activity = discord.Activity(
type = discord.ActivityType.listening,
name = f'/commands'
)
)
# for g in self.client.guilds:
def setup(client):
client.add_cog(on_connect(client))

View File

@ -0,0 +1,33 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever a channel in a guild is deleted
class on_guild_channel_delete(commands.Cog, name='On Guild Channel Delete Events'):
def __init__(self, client):
self.client = client
#### What to do if a mod channel gets deleted: try and pull default system channel, and if not then top-most channel
@commands.Cog.listener()
async def on_guild_channel_delete(self, channel):
conf = yaml_load(configFile)
if conf[str(channel.guild.id)]['channels']['mod'] == channel.id:
if channel.guild.system_channel is None:
p = len(channel.guild.channels)
c = None
for t in channel.guild.text_channels:
if t.position < p:
p = t.position
conf[str(channel.guild.id)]['channels']['mod'] = t.id
else:
conf[str(channel.guild.id)]['channels']['mod'] = channel.guild.system_channel.id
yaml_dump(conf, configFile)
def setup(client):
client.add_cog(on_guild_channel_delete(client))

View File

@ -0,0 +1,26 @@
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 # Slash Command features
import logging
# logger and handler
from bot import checkConfig, parseConfigCheck, configFile, setConfig, yaml_load, yaml_dump
#### Actions for the bot to take when the Bot joins a guild.
class on_guild_join(commands.Cog, name='On Guild Join Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_guild_join(self, guild):
setConfig(guild)
status, output = checkConfig(guild)
conf = yaml_load(configFile)
if not status:
await guild.get_channel(conf[str(guild.id)]['channels']['mod']).send(f"```The Bot's configurations are incomplete for the guild `{guild.name}`. Some limited functions will still be available, but most features cannot be used until the configurations are complete.\n{parseConfigCheck(output)}\nYou can set these configuration values using the `/config` command.```")
def setup(client):
client.add_cog(on_guild_join(client))

View File

@ -0,0 +1,22 @@
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 # Slash Command features
import logging
# logger and handler
from bot import clearConfig, configFile, yaml_load, yaml_dump
#### Actions for the bot to take when removed from a guild.
class on_guild_remove(commands.Cog, name='On Guild Remove Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_guild_remove(self, guild): ## Actions for when the bot is removed from a guild.
clearConfig(str(guild.id))
def setup(client):
client.add_cog(on_guild_remove(client))

View File

@ -0,0 +1,27 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever there is a new role created.
class on_guild_role_create(commands.Cog, name='On Guild Role Create Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_guild_role_create(self, role):
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 not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator:
conf[str(role.guild.id)]['roles']['admin'].append(role.id)
yaml_dump(conf, configFile)
#### If the role is created with admin privileges, the bot adds the role to its configs.
def setup(client):
client.add_cog(on_guild_role_create(client))

View File

@ -0,0 +1,27 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever there is a new role deleted.
class on_guild_role_delete(commands.Cog, name='On Guild Role Delete Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_guild_role_delete(self, role):
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']['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.
def setup(client):
client.add_cog(on_guild_role_delete(client))

View File

@ -0,0 +1,34 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever there is a new role deleted.
class on_guild_role_update(commands.Cog, name='On Guild Role Update Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_guild_role_update(self, before, after):
conf = yaml_load(configFile)
#### If the original role is in the config as an admin role, and it subsequently is run by a bot or is not an admin, remove it from config
if before.id in conf[str(before.guild.id)]['roles']['admin']:
if after.is_bot_managed() or after.is_integration() or not after.permissions.administrator:
conf[str(after.guild.id)]['roles']['admin'].remove(after.id)
yaml_dump(conf, configFile)
#### If the new role is an admin and is not already in the config, add it.
if not (after.is_bot_managed() or after.is_integration()) and after.permissions.administrator:
if after.id not in conf[str(after.guild.id)]['roles']['admin']:
conf[str(after.guild.id)]['roles']['admin'].remove(after.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.
def setup(client):
client.add_cog(on_guild_role_update(client))

View File

@ -0,0 +1,25 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever the guild info or ownership are updated.
class on_guild_update(commands.Cog, name='On Guild Update Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_guild_update(self, before, after):
conf = yaml_load(configFile)
conf[str(after.id)]['name'] = after.name
conf[str(after.id)]['owner'] = after.owner_id
# Updates guild name and channel
yaml_dump(conf,configFile)
def setup(client):
client.add_cog(on_guild_update(client))

View File

@ -0,0 +1,46 @@
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, tasks # 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 # Slash Command features
from datetime import datetime
import logging
# logger and handler
from bot import configFile, yaml_load
#### Actions the bot will take on messages being sent in the channel.
##### Message Listener Cog
class on_message(commands.Cog, name='On Message Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_message(self,message):
if message.author.bot or message.author.id == message.guild.owner_id:
return
for role in message.author.roles:
if role.permissions.administrator:
return
conf = yaml_load(configFile)
guild = message.guild
guildStr = str(guild.id)
if 'notifications' in conf[guildStr]:
if conf[guildStr]['notifications'].get('help', False):
if 'help' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
if message.channel.id == conf[guildStr]['channels']['help'] and isinstance(guild.get_role(conf[guildStr]['roles']['committee']), discord.Role):
modChannel = self.client.get_channel(conf[guildStr]['channels']['mod'])
committeeRole = guild.get_role(conf[guildStr]['roles']['committee'])
embed = discord.Embed(
title = f'[New Query in Help]({message.jump_url})',
description = message.content,
colour = discord.Colour.orange()
)
embed.set_footer(text=datetime.now().strftime('%a %-d %b %y, %-I:%M %p'))
embed.set_author(name=message.author.display_name, icon_url=message.author.avatar_url)
await modChannel.send(f'{committeeRole.mention}\n```There has been a new help query posted.```\n{message.author.mention}` posted in `{message.channel.mention}`.`', embed = embed)
def setup(client):
client.add_cog(on_message(client))

View File

@ -0,0 +1,44 @@
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 # Slash Command features
# import discord_components
import logging
# logger and handler
from bot import checkConfig, clearConfig, configFile, parseConfigCheck, setConfig, yaml_dump, yaml_load, loadCogs, unloadCogs
#### Actions for the Bot to take once it is ready to interact with commands.
class on_ready(commands.Cog, name='On Ready Events'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_ready(self):
# discord_components.DiscordComponents(self.client)
#### Create any missing config entries for guilds
for guild in self.client.guilds:
setConfig(guild)
#### Delete any extra config entries for guilds the bot is not in
conf = yaml_load(configFile)
for key in list(conf):
clearConfig(key)
#### Check completeness of configurations
for guild in self.client.guilds:
status, output = checkConfig(guild)
conf = yaml_load(configFile)
if not status:
await guild.get_channel(conf[str(guild.id)]['channels']['mod']).send(f"```The Bot's configurations are incomplete for the guild `{guild.name}`. Some limited functions will still be available, but most features cannot be used until the configurations are complete.\n{parseConfigCheck(output)}\nYou can set these configuration values using the `/config` command.```")
# #### Reload the /commands after the configs have finished loading.
# unloadCogs('slashcommands')
# loadCogs('slashcommands')
def setup(client):
client.add_cog(on_ready(client))

View File

@ -0,0 +1,192 @@
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, ComponentContext # 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
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile, pitchesFile, configFile, dataFile, lookupFile, unloadCog
#### Pitch Command
class PitchListener(commands.Cog, name='Pitch Listener'):
def __init__(self, client):
self.client = client
@commands.Cog.listener(name='on_component')
async def _pitch_listener(self, ctx:ComponentContext):
conf = yaml_load(configFile)
data = yaml_load(dataFile)
lookup = yaml_load(lookupFile)
pitches = yaml_load(pitchesFile)
guildStr = str(ctx.guild.id)
if not pitches.get(guildStr, {}): return # If no pitches for current guild, ignore.
[timeslot] = [*pitches[guildStr]]
if ctx.origin_message.id not in pitches[guildStr][timeslot]['messages'] + [pitches[guildStr][timeslot]['control']]: return # If the context id is not in the pitch menu, ignore
newcomer = returning_player = None
if 'newcomer' in conf[guildStr]['roles']: newcomer = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['newcomer'], ctx.guild.roles)
if 'returning_player' in conf[guildStr]['roles']: returning_player = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['returning_player'], ctx.guild.roles)
control = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['control'])
header_message = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['header_message'])
if ctx.origin_message.id == control.id:
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):
await ctx.send(f'```Error: You are not authorised to do this. The control panel may only be issued by an administrator.```',hidden=True)
else:
if ctx.custom_id == 'allow_returning':
await ctx.channel.set_permissions(reason=f'/pitch control switch triggered by {ctx.author.display_name}', target=returning_player, read_messages=True)
await ctx.send(f'```Returning Players have now been allowed access to the pitch menu.```', hidden=True)
if ctx.custom_id == 'allow_newcomers':
await ctx.channel.set_permissions(reason=f'/pitch control switch triggered by {ctx.author.display_name}', target=newcomer, read_messages=True)
await ctx.send(f'```Newcomers have now been allowed access to the pitch menu.```', hidden=True)
if ctx.custom_id == 'allow_all':
await ctx.channel.set_permissions(reason=f'/pitch control switch triggered by {ctx.author.display_name}', target=ctx.guild.default_role, read_messages= True, send_messages=False)
await ctx.send(f'```All members have now been allowed access to the pitch menu.```', hidden=True)
if ctx.custom_id == 'close_pitches':
await ctx.send(f'```Please wait: closing pitches.```', hidden=True)
await header_message.delete()
for message in pitches[guildStr][timeslot]['messages']:
m = await ctx.channel.fetch_message(message)
await m.delete()
await control.delete()
await ctx.channel.edit(reason=f'/pitch command issued by {ctx.author.display_name}', overwrites={})
await ctx.channel.send('```Pitch menu cleared. Pitches have now concluded.```')
del pitches[guildStr][timeslot]
if not pitches[guildStr]: del pitches[guildStr]
yaml_dump(pitches,pitchesFile)
if not pitches and self.client.get_cog('Pitch Listener') is not None:
unloadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
#### Deactivate global pitch listener
else:
index = int(ctx.custom_id.split('_',1)[1])
if ctx.custom_id.startswith('join_'):
if set([x.id for x in ctx.author.roles]) & set(pitches[guildStr][timeslot]['roles'].values()):
for r in list(set([x.id for x in ctx.author.roles]) & set(pitches[guildStr][timeslot]['roles'].values())):
role = ctx.guild.get_role(r)
rStr = str(role.id)
if role.id != pitches[guildStr][timeslot]['roles'][index]:
await ctx.author.remove_roles(role,reason=f'/pitch interaction by {ctx.author.display_name}')
i = pitches[guildStr][timeslot]['indices'][role.id]
element = pitches[guildStr][timeslot]['entries'][i]
gm = await self.client.fetch_user(element['gm'])
if ctx.author.id != lookup[guildStr][rStr]['gm']:
data[guildStr][timeslot][rStr]['current_players'] -= 1
element['current_players'] -= 1
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
m = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['messages'][i])
await m.edit(content=o)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{ctx.author.display_name} has left the game.```')
role = ctx.guild.get_role(pitches[guildStr][timeslot]['roles'][index])
rStr = str(role.id)
if role in ctx.author.roles:
await ctx.send(f'```Error: You are already in the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True)
else:
await ctx.author.add_roles(role,reason=f'/pitch interaction by {ctx.author.display_name}')
element = pitches[guildStr][timeslot]['entries'][index]
if ctx.author.id != lookup[guildStr][rStr]['gm']:
data[guildStr][timeslot][rStr]['current_players'] += 1
element['current_players'] += 1
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
m = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['messages'][index])
await m.edit(content=o)
await ctx.send(f'You have joined the game `{lookup[guildStr][rStr]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{ctx.author.display_name} has joined the game.```')
ts = lookup[guildStr][rStr]['time']
p = await tc.pins()
if p is not None:
header = discord.utils.find(lambda x: x.id == data[guildStr][ts][rStr]['header_message'], p)
if header is not None:
text = header.content.split('\n')
for line, item in enumerate(text):
if 'Current Players: ' in item:
text[line] = f'Current Players: {str(data[guildStr][ts][rStr]["current_players"]) if data[guildStr][ts][rStr]["current_players"] is not None else str(0)}'
break
await header.edit(content='\n'.join(text))
elif ctx.custom_id.startswith('leave_'):
role = ctx.guild.get_role(pitches[guildStr][timeslot]['roles'][index])
rStr = str(role.id)
if role not in ctx.author.roles:
await ctx.send(f'```Error: You are not in the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True)
else:
await ctx.author.remove_roles(role,reason=f'/pitch interaction by {ctx.author.display_name}')
element = pitches[guildStr][timeslot]['entries'][index]
if ctx.author.id != lookup[guildStr][rStr]['gm']:
data[guildStr][timeslot][rStr]['current_players'] -= 1
element['current_players'] -= 1
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
me = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['messages'][index])
await me.edit(content=o)
await ctx.send(f'You have left the game `{lookup[guildStr][rStr]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][rStr]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{ctx.author.display_name} has left the game.```')
ts = lookup[guildStr][rStr]['time']
p = await tc.pins()
if p is not None:
header = discord.utils.find(lambda x: x.id == data[guildStr][ts][rStr]['header_message'], p)
if header is not None:
text = header.content.split('\n')
for line, item in enumerate(text):
if 'Current Players: ' in item:
text[line] = f'Current Players: {str(data[guildStr][ts][rStr]["current_players"]) if data[guildStr][ts][rStr]["current_players"] is not None else str(0)}'
break
await header.edit(content='\n'.join(text))
yaml_dump(data, dataFile)
yaml_dump(pitches, pitchesFile)
def setup(client):
client.add_cog(PitchListener(client))

View File

@ -0,0 +1,126 @@
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, ComponentContext # Slash Command Library
from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component, create_button, create_actionrow
from discord_slash.utils.manage_commands import create_choice, create_option
from discord_slash.model import ButtonStyle
import logging
# logger and handler
from bot import configFile, yaml_load, categoriesFile, configFile, lookupFile
##### Membership Verification Cog
class MemberVerification(commands.Cog, name='Member Verification Cog'):
def __init__(self, client):
self.client = client
@commands.Cog.listener(name='on_message')
async def _submission_listener(self, message):
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(message.guild.id)
lookup = yaml_load(lookupFile)
if conf[guildStr]['channels'].get('signup', None) is None: return
if message.author.bot: return
if message.channel.id != conf[guildStr]['channels']['signup']: return
if not (message.attachments):
await message.author.send(f'```Error: The message you posted in the `{message.channel.name}` channel of the guild `{message.guild.name}` was invalid. Your post must contain a screensot of your proof of purchase for membership.```')
await message.delete()
return
membership = [discord.utils.get(message.guild.roles, id=x) for x in conf[guildStr]['membership']]
membership_options = [create_select_option(label=x.name, value=str(x.id), description='Membership type.') for x in membership]
admin_buttons = []
if conf[guildStr]['roles'].get('student', None) is not None: admin_buttons.append(create_button(style=ButtonStyle.blurple, label='Student', emoji='📚', custom_id=f'student_{message.id}'))
admin_buttons.append(create_button(style=ButtonStyle.grey, label='Review', emoji='⚠️', custom_id=f'review_{message.id}'))
admin_buttons.append(create_button(style=ButtonStyle.red, label='Deny', emoji='✖️', custom_id=f'deny_{message.id}'))
admin_buttons.append(create_button(style=ButtonStyle.green, label='Done', emoji='▶️', custom_id=f'done_{message.id}'))
o = f'```For Administrators: Please verify the membership request submitted by `{message.author.display_name}`.```'
admins = '|'.join([discord.utils.get(message.guild.roles, id=x).mention for x in conf[guildStr]['roles']['admin']])
o = ''.join((admins,o))
m = await message.reply(
content= o,
components=[
create_actionrow(
create_select(
options=membership_options,
custom_id=f'membership_{message.id}',
placeholder=f'Please select a membership to assign to {message.author.display_name}',
min_values=1,
max_values=1
)
),
create_actionrow(
*admin_buttons
)
]
)
if conf[guildStr]['notifications'].get('signup', False):
embed = discord.Embed(
title = f'Member Verification Request',
description = f'User: {message.author.name}\n\n[Jup to Message]({m.jump_url})',
colour = discord.Colour.blue(),
)
if conf[guildStr]['channels'].get('mod', None) is not None:
await message.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```New membership verification request.```\n{admins}', embed=embed)
@commands.Cog.listener(name='on_component')
async def _verification_response(self, ctx:ComponentContext):
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
admins = '|'.join([discord.utils.get(ctx.guild.roles, id=x).mention for x in conf[guildStr]['roles']['admin']])
lookup = yaml_load(lookupFile)
if ctx.channel.id != conf[guildStr]['channels']['signup']: return
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):
await ctx.send(f'```Error: You are not authorised to assign memberships for guild `{ctx.guild.name}`. Only administrators may assign memberships using this interface.```', hidden=True)
else:
submission = await ctx.channel.fetch_message(int(ctx.custom_id.split('_',1)[1]))
if ctx.custom_id.startswith('done_'):
await ctx.send(f'```Membership verification complete.```', hidden=True)
await ctx.origin_message.delete()
elif ctx.custom_id.startswith('deny_'):
await ctx.send(f'```Membership verification denied.```', hidden=True)
embed = discord.Embed(
title = submission.author.name,
description = f'[Jup to Message]({submission.jump_url})',
colour = discord.Colour.red(),
)
await submission.author.send(f'```Your membership for guild `{submission.guild.name}` could not be verified. Please make sure your name and the kind of membership that you have bought are visible in the screenshot you upload. Please contact a Committee member if you have any difficulties.```')
if conf[guildStr]['channels'].get('mod', None) is not None:
await submission.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```Verifying the membership of {submission.author.display_name} failed.```\n{admins}', embed=embed)
await ctx.origin_message.delete()
elif ctx.custom_id.startswith('review_'):
await ctx.send(f'```Membership review requested.```', hidden=True)
embed = discord.Embed(
title = submission.author.name,
description = f'[Jup to Message]({submission.jump_url})',
colour = discord.Colour.orange()
)
await submission.author.send(f'```Your membership for guild `{submission.guild.name}` needs to be reviewed by a Committee member.```')
if conf[guildStr]['channels'].get('mod', None) is not None:
await submission.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```There is a problem verifying the membership of {submission.author.display_name}.\nCould someone verify this person\'s membership manually via the EUSA portal?.```\n{admins}', embed=embed)
elif ctx.custom_id.startswith('student_'):
await ctx.send(f'````Student` role granted.```', hidden=True)
student_role = submission.guild.get_role(conf[guildStr]['roles']['student'])
await submission.author.add_roles(student_role, reason=f'Membership Verification: Student role assigned by `{ctx.author.display_name}`.')
await submission.author.send(f'```You have additionally been assigned the role `Student` in the guild `{submission.guild.name}`.```')
elif ctx.custom_id.startswith('membership_'):
[selected_membership] = ctx.selected_options
selected_role = ctx.guild.get_role(int(selected_membership))
if selected_role not in submission.author.roles:
await ctx.send(f'```Membership `{selected_role.name}` added to member `{submission.author.display_name}`.```', hidden=True)
await submission.author.add_roles(selected_role, reason=f'Membership Verification: Membership verified by `{ctx.author.display_name}`.')
await submission.author.send(f'```Your membership for guild `{submission.guild.name}` has been verified and you have been assigned the role `{selected_role.name}`.```')
else:
await ctx.send(f'```Membership `{selected_role.name}` removed from member `{submission.author.display_name}`.```', hidden=True)
await submission.author.remove_roles(selected_role, reason=f'Membership Verification: Membership removed by `{ctx.author.display_name}`.')
await submission.author.send(f'```Your role `{selected_role.name}` has been removed in the guild `{submission.guild.name}`.```')
else:
pass
def setup(client):
client.add_cog(MemberVerification(client))

View File

@ -0,0 +1,53 @@
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 # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, categoriesFile, configFile, lookupFile
##### Membership Restriction Message Listener Cog
class RestrictionListener(commands.Cog, name='Membership Restriction Listener'):
def __init__(self, client):
self.client = client
# Block non-verified user from posting messages.
@commands.Cog.listener(name='on_message')
async def _restriction_listener(self,message):
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(message.guild.id)
lookup = yaml_load(lookupFile)
if not conf[guildStr].get('restrict',False): return
if message.author.bot: return
if str(message.channel.category) in categories[guildStr]: return
if (set(message.author.roles) & set([message.guild.get_role(x) for x in conf[guildStr]['roles']['admin']]) or message.author == message.guild.owner): return
if set(message.author.roles) & set([message.guild.get_role(x) for x in conf[guildStr]['membership']]): return
if message.channel.overwrites_for(message.author).manage_channels: return
signupChannel = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['signup'], message.guild.channels)
await message.channel.send(f'```{message.author.display_name} does not have a verified membership of `{message.guild.name}`. Please submit your membership confirmation for verification in the {signupChannel.name} to ensure you have access to your game.```\n{message.author.mention} | {signupChannel.mention}')
await message.channel.category.set_permissions(message.author, send_messages = False, connect = False, reason=f'Membership Restriction: {message.author.display_name} is not a verified member.')
await message.delete()
# Reinstate on verification
@commands.Cog.listener(name='on_member_update')
async def _reinstate_listener(self, before, after):
if before.roles == after.roles: return
if len(set(after.roles) - set(before.roles)) != 1: return
[d] = list(set(after.roles) - set(before.roles))
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(after.guild.id)
if d.id not in conf[guildStr]['membership']: return
lookup = yaml_load(lookupFile)
if not set(after.roles) & set([after.guild.get_role(x) for x in conf[guildStr]['membership']]): return
for game in list(set(after.roles) & set([after.guild.get_role(int(x)) for x in lookup[guildStr]])):
c = discord.utils.get(after.guild.categories, id=lookup[guildStr][str(game.id)]['category'])
if c is not None:
if c.overwrites_for(after).send_messages is False: await c.set_permissions(after, overwrite = None, reason= f'Membership Restriction: {after.display_name} has been verified and reinstated.')
def setup(client):
client.add_cog(RestrictionListener(client))

View File

@ -0,0 +1,416 @@
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
import re
from bot import configFile, yaml_load, yaml_dump, loadCog, unloadCog, reloadCog, cogsDir, slash, lookupFile
##### Configuration Cog
class Configuration(commands.Cog, name='Configuration Commands'):
def __init__(self, client):
self.client = client
guild_ids=[int(guildKey) for guildKey in yaml_load(configFile)]
permissions = {}
conf = yaml_load(configFile)
for gStr in conf:
permissions[int(gStr)] = []
permissions[int(gStr)].append(create_permission(id=conf[gStr]['owner'],id_type=SlashCommandPermissionType.USER,permission=True))
for admin in conf[gStr]['roles']['admin']: permissions[int(gStr)].append(create_permission(id=admin,id_type=SlashCommandPermissionType.ROLE,permission=True))
@cog_ext.cog_subcommand(
base='config',
# subcommand_group='role',
name='roles',
description='Designates the various key roles referenced by the Bot.',
base_description='Commands for configuring the various parameters of the Guild.',
base_default_permission=False,
base_permissions=permissions,
# subcommand_group_description='Designates the various key command roles for the guild.',
guild_ids=guild_ids,
options=[
create_option(
name='key',
description='The name of the role parameter being assigned.',
option_type=3,
required=True,
choices=[
create_choice(
name='`Bot` role',
value='bot'
),
create_choice(
name='`Committee` role',
value='committee'
),
create_choice(
name='`Newcomer` role',
value='newcomer'
),
create_choice(
name='`Returning Player` role',
value='returning_player'
),
create_choice(
name='`Student` role',
value='student'
),
create_choice(
name='`Bot Maintainer` role',
value='maintainer'
)
]
),
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=False
)
]
)
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. Please select a role to assign.```',hidden=True)
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 does not exist, do not assign a role.```',hidden=True)
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' or key == 'bot' else None,
)
conf = yaml_load(configFile)
guildStr = str(ctx.guild.id)
if 'roles' not in conf[guildStr]:
conf[guildStr]['roles'] = {}
conf[guildStr]['roles'][key] = int(r.id)
yaml_dump(conf, configFile)
await ctx.send(f'```The `{key}` role for the guild `{ctx.guild.name}` has been set to `{r.name}`.```\n{r.mention}',hidden=True)
if any(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]):
if any([yaml_load(configFile)[x]['timeslots'] for x in yaml_load(configFile)]):
flag = False
if self.client.get_cog('Game Create') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/game_create.py')
flag = True
if any([x for x in yaml_load(lookupFile).values()]):
if self.client.get_cog('Game Management') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
flag = True
if self.client.get_cog('Player Commands') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
flag = True
if self.client.get_cog('T-Card Command') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/tcard.py')
flag = True
if self.client.get_cog('Pitch Command') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
flag = True
if flag: await self.client.slash.sync_all_commands()
@cog_ext.cog_subcommand(
base='config',
# subcommand_group='channel',
name='channels',
description='Designate the various key channels for the Bot to interact with.',
# base_description='Commands for configuring the various parameters of the Guild',
base_default_permission=False,
# base_permissions=permissions,
# subcommand_group_description='Designates the various key Bot channels for the guild.',
guild_ids=guild_ids,
options=[
create_option(
name='key',
description='The name of the channel parameter being assigned.',
option_type=3,
required=True,
choices=[
create_choice(
name='Help Channel',
value='help'
),
create_choice(
name='Mod Channel',
value='mod'
),
create_choice(
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=False
)
]
)
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. Please select a role to assign.```',hidden=True)
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 does not exist, do not assign a channel.```',hidden=True)
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)
guildStr = str(ctx.guild.id)
if 'channels' not in conf[guildStr]:
conf[guildStr]['channels'] = {}
conf[guildStr]['channels'][key] = int(c.id)
yaml_dump(conf, configFile)
await ctx.send(f'```The `{key}` channel for the guild `{ctx.guild.name}` has been set to `{c.name}`.\n\nAll permission overrides for the channel have been reset. Remember to set the appropriate permissions for your guild.```\n{c.mention}',hidden=True)
@cog_ext.cog_subcommand(
base='config',
# subcommand_group='notifications',
name='notifications',
description='Configure monitoring and notifications to Committee for member query channels.',
# base_description='Commands for configuring the various parameters of the Guild',
base_default_permission=False,
# base_permissions=permissions,
# subcommand_group_description='Configures whether the bot monitors and responds to posts in key channels.',
guild_ids=guild_ids,
options=[
create_option(
name='channel',
description='Select which channel to change notifications for.',
option_type=3,
required=True,
choices=[
create_choice(
name='Help Channel',
value='help'
),
create_choice(
name='Signup Channel',
value='signup'
)
]
),
create_option(
name='notifications',
description='Whether or not the bot monitors the channel for posts.',
option_type=5,
required=True
)
]
)
async def _config_notifications(self, ctx:SlashContext, channel:str, notifications:bool):
conf = yaml_load(configFile)
guildStr = str(ctx.guild.id)
if 'notifications' not in conf[guildStr]:
conf[guildStr]['notifications'] = {}
conf[guildStr]['notifications'][channel] = notifications
yaml_dump(conf, configFile)
await ctx.send(f'```Notifications for posts in the `{channel}` channel for the guild `{ctx.guild.name}` have been set to `{notifications}`.```',hidden=True)
@cog_ext.cog_subcommand(
base='config',
subcommand_group='timeslots',
name='add',
description='Add a timeslot at which the Guild will host games.',
# base_description='Commands for configuring the various parameters of the Guild',
base_default_permission=False,
# base_permissions=permissions,
subcommand_group_description='Manages timeslots available for games on the guild.',
guild_ids=guild_ids,
options=[
create_option(
name='key',
description='Alphanumeric time code 10 chars max.',
option_type=3,
required=True,
),
create_option(
name='name',
description='A longer, descriptive name of when the timeslot is',
option_type=3,
required=True
)
]
)
async def _config_timeslots_add(self, ctx:SlashContext, key:str, name:str):
sanitisedKey = re.sub(r"\W+",'', key[:9].lower())
if not key.isalnum():
await ctx.send(f'```Key value {key} is not a valid alphanumeric time code. Sanitising to `{sanitisedKey}`.```',hidden=True)
conf = yaml_load(configFile)
guildStr = str(ctx.guild.id)
if 'timeslots' not in conf[guildStr]:
conf[guildStr]['timeslots'] = {}
if sanitisedKey in conf[guildStr]['timeslots']:
await ctx.send(f'```Key value {sanitisedKey} has already been defined for guild `{ctx.guild.name}` for `{conf[guildStr]["timeslots"][sanitisedKey]}`. Please use the `remove` or `modify` sub-commands to amend it.```',hidden=True)
return
conf[guildStr]['timeslots'][sanitisedKey] = name
yaml_dump(conf, configFile)
await ctx.send(f'```Timeslot `{name}` with the key `{sanitisedKey}` has been added for the guild `{ctx.guild.name}`.```',hidden=True)
if any([yaml_load(configFile)[x]['timeslots'] for x in yaml_load(configFile)]):
flag = False
if self.client.get_cog('Manipulate Timeslots') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py')
flag = True
if any(['bot' in yaml_load(configFile)[x]['roles'] for x in yaml_load(configFile)]):
if self.client.get_cog('Game Create') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/game_create.py')
Flag = True
if yaml_load(lookupFile):
if any([x for x in yaml_load(lookupFile).values()]):
if self.client.get_cog('Game Management') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
Flag = True
if self.client.get_cog('Player Commands') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
Flag = True
if self.client.get_cog('T-Card Command') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/tcard.py')
Flag = True
if self.client.get_cog('Pitch Command') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
Flag = True
if flag: await self.client.slash.sync_all_commands()
@cog_ext.cog_subcommand(
base='config',
subcommand_group='membership',
name='add',
description='Add a membership type for the Guild.',
# base_description='Commands for configuring the various parameters of the Guild',
base_default_permission=False,
# base_permissions=permissions,
subcommand_group_description='Manages the different categories of membership available to the Guild.',
guild_ids=guild_ids,
options=[
create_option(
name='name',
description='Name of membership type.',
option_type=3,
required=True,
),
create_option(
name='role_exists',
description='Does the role for this member type already exist?',
option_type=5,
required=True
),
create_option(
name='role',
description='Assign the role if it already exists.',
option_type=8,
required=False
)
]
)
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 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.```',hidden=True)
return
conf = yaml_load(configFile)
guildStr = str(ctx.guild.id)
if 'membership' not in conf[guildStr]:
conf[guildStr]['membership'] = []
if role is not None:
if role.id in conf[guildStr]['membership']:
await ctx.send(f'```The role {name} has already been assigned to a membership type for guild `{ctx.guild.name}`. Please use the `remove` sub-command to delete it or assign a different role.```',hidden=True)
return
if any([ctx.guild.get_role(m).name == name for m in conf[str(ctx.guild.id)]['membership']]):
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:
r = await ctx.guild.create_role(
name=name,
permissions=discord.Permissions(read_messages=True,use_slash_commands=True),
mentionable=False,
reason=f'`/config membership add` command issued by {ctx.author.display_name}'
)
if role is not None:
await role.edit(
name=name,
permissions=discord.Permissions(read_messages=True,use_slash_commands=True),
mentionable=False,
reason=f'`/config membership add` command issued by {ctx.author.display_name}'
)
conf[guildStr]['membership'].append(role.id) if role is not None else conf[guildStr]['membership'].append(r.id)
yaml_dump(conf, configFile)
await ctx.send(f'```Membership type `{role.name if role is not None else r.name}` has been registered for the guild `{ctx.guild.name}`.```',hidden=True)
if any([yaml_load(configFile)[x]['membership'] for x in yaml_load(configFile)]):
if self.client.get_cog('Edit Membership') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py')
await self.client.slash.sync_all_commands()
@cog_ext.cog_subcommand(
base='config',
# subcommand_group='notifications',
name='restrict',
description='Toggle membership estriction for the guild.',
# base_description='Commands for configuring the various parameters of the Guild',
base_default_permission=False,
# base_permissions=permissions,
# subcommand_group_description='Configures whether the bot monitors and responds to posts in key channels.',
guild_ids=guild_ids,
options=[
create_option(
name='value',
description='Enable membership restrictions for the guild?',
option_type=5,
required=True
)
]
)
async def _restrict(
self,
ctx:SlashContext,
value:bool
):
conf = yaml_load(configFile)
conf[str(ctx.guild.id)]['restrict'] = value
yaml_dump(conf, configFile)
await ctx.send(f'```Membership restrictions for the guild `{ctx.guild.name}` have been set to `{value}`.```',hidden=True)
def setup(client):
client.add_cog(Configuration(client))

View File

@ -0,0 +1,95 @@
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
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog
#### Separate cog to remove and modify membership registrations that is reloaded if timeslots are added or removed
class EditMembership(commands.Cog, name='Edit Membership'):
def __init__(self, client):
self.client = client
#### Only emable for guilds with registered membership types
#### N.B.: if there are no guilds with any membership types, then this will throw an exception.
#### The solution I have implemented is that this will be classed as a 'secondary' cog: it will not be loaded by default, and will only be loaded if at least one guild has a membership role registered.
#### If the deletion of membership roles removes memberships from all guilds, it will unload the cog and delete the commands until a new membership role is defined.
guild_ids=[int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['membership']]
conf = yaml_load(configFile)
permissions = {}
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))
@cog_ext.cog_subcommand(
base='config',
subcommand_group='membership',
name='remove',
description='Remove a registered membership role.',
# base_description='Commands for configuring the various parameters of 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='role',
description='The role of the membership type you want to delete.',
option_type=8,
required=True
)
]
)
async def _config_membership_remove(self, ctx:SlashContext, role:discord.Role):
conf = yaml_load(configFile)
if 'membership' not in conf[str(ctx.guild.id)]:
conf[str(ctx.guild.id)]['timeslots'] = {}
if role.id in conf[str(ctx.guild.id)]['membership']:
conf[str(ctx.guild.id)]['membership'].remove(role.id)
yaml_dump(conf, configFile)
await ctx.send(f'```Membership type {role.name} has been deleted for the guild `{ctx.guild.name}`.```', hidden=True)
await role.delete(reason=f'`/config membership remove` command issued by `{ctx.author.display_name}`.')
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 conf[str(ctx.guild.id)]['membership']:
output = f'Role `{role.name}` is not a registered membership role in the guild `{ctx.guild.name}`. Please select a valid membership role.\n\n Eligible roles are:\n'
for m in conf[str(ctx.guild.id)]['membership']:
output = ''.join([output, f'\n{ctx.guild.get_role(m).name}'])
await ctx.send(''.join(['```',output,'```']),hidden=True)
else:
await ctx.send(f'```No roles have been registered as membership types for the guild `{ctx.guild.name}`.```',hidden=True)
@cog_ext.cog_subcommand(
base='config',
subcommand_group='membership',
name='list',
description='List the existing game memberships on the server.',
# base_description='Commands for configuring the various parameters of 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 _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'] = {}
if conf[str(ctx.guild.id)]['membership']:
output = f'The following membership types have been registered for the guild {ctx.guild.name}:\n'
for m in conf[str(ctx.guild.id)]['membership']:
output = ''.join([output, f'\n{ctx.guild.get_role(m).name}'])
await ctx.send(''.join(['```',output,'```']),hidden=True)
else:
await ctx.send(f'```No roles have been registered as membership types for the guild `{ctx.guild.name}`.```',hidden=True)
def setup(client):
client.add_cog(EditMembership(client))

View File

@ -0,0 +1,302 @@
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, cogsDir, unloadCog, dataFile, lookupFile, gmFile, loadCog, categoriesFile
#### Game Role and Channel Setup Command
class GameCreate(commands.Cog, name='Game Create'):
def __init__(self, client):
self.client = client
conf=yaml_load(configFile)
permissions={}
guild_ids = list(set.intersection(set([int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['timeslots']]),set([int(guildKey) for guildKey in yaml_load(configFile) if 'bot' in yaml_load(configFile)[guildKey]['roles'] and type(yaml_load(configFile)[guildKey]['roles']['bot']) is int])))
for guildID in guild_ids:
permissions[guildID] = []
permissions[guildID].append(create_permission(id=conf[str(guildID)]['owner'],id_type=SlashCommandPermissionType.USER,permission=True))
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 game 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 game can take.',
option_type=4,
required=False
),
create_option(
name='current_players',
description='The number of players currently in this 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,
current_players: 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)
categories = yaml_load(categoriesFile)
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'```Error: `Bot` role for guild `{ctx.guild.name}` has not been defined. Cannot configure game.```',hidden=True)
return
if 'timeslots' not in conf[guildStr]:
conf[guildStr]['timeslots'] = {}
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 min_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 current_players > max_players:
await ctx.send(f'```Error: The number of reserved spaces cannot exceed the maximum number of players.```',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 guildStr not in lookup:
lookup[guildStr] = {}
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: 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: 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 not r:
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(),
colour=discord.Colour.green()
)
else:
rExists = True
await r.edit(
mentionable=True,
permissions=discord.Permissions.none(),
reason=f'/game create command issued by `{ctx.author.display_name}`',
colour=discord.Colour.green()
)
if r not in gm.roles:
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 not c:
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 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 create 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 is not None:
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 {current_players if current_players else "0"} currently occupied).\n'
if 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\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'])
if 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'])
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: {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 create command issued by `{ctx.author.display_name}`')
data[guildStr][time][str(r.id)] = {
'game_title': game_title,
'gm': gm.id,
'max_players': max_players,
'min_players': min_players,
'current_players': current_players if current_players is not None else 0,
'system': system,
'platform': platform,
'role': r.id,
'category': c.id,
'text_channel': t.id,
'header_message': o.id
}
lookup[guildStr][str(r.id)] = {
'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)] = []
if str(guildStr) not in categories: categories[guildStr] = {}
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)
# Enable the Game Management and Player commands
if any([x for x in yaml_load(lookupFile).values()]):
flag = False
if self.client.get_cog('Game Management') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
flag = True
if self.client.get_cog('Player Commands') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
flag = True
if self.client.get_cog('T-Card Command') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/tcard.py')
flag = True
if self.client.get_cog('Pitch Command') is None:
loadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
flag = True
if flag: await self.client.slash.sync_all_commands()
def setup(client):
client.add_cog(GameCreate(client))

View File

@ -0,0 +1,562 @@
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))

View File

@ -0,0 +1,170 @@
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
from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component
# from discord_components import *
from bot import configFile, yaml_load, yaml_dump, reloadCog, cogsDir, unloadCog, dataFile
#### Separate cog to remove and modify timeslots that is reloaded if timeslots are added or removed
class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'):
def __init__(self, client):
self.client = client
#### Only emable for guilds with timeslots
#### N.B.: if there are no guilds with any timeslots, then this will throw an exception.
#### The solution I have implemented is that this will be classed as a 'secondary' cog: it will not be loaded by default, and will only be loaded if at least one guild has a timeslot configured.
#### If the deletion of timeslots removes timeslots from all guilds, it will unload the cog and delete the commands until a new timeslot is defined.
guild_ids=[int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['timeslots']]
conf = yaml_load(configFile)
permissions = {}
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))
@cog_ext.cog_subcommand(
base='config',
subcommand_group='timeslots',
name='remove',
description='Remove a configured game timeslot.',
# base_description='Commands for configuring the various parameters of 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 you wish to delete.',
# option_type=3,
# required=True
# )
# ]
)
async def _config_timeslots_remove(self, ctx:SlashContext,):# timeslot:str):
conf = yaml_load(configFile)
data = yaml_load(dataFile)
guildStr = str(ctx.guild.id)
if 'timeslots' not in conf[guildStr]:
conf[guildStr]['timeslots'] = {}
tsDict = conf[guildStr]['timeslots'].copy()
optionsList = [create_select_option(label=tsDict[x], value=x, description=x) for x in tsDict]
try:
m = await ctx.send(
content='```Select which time slot you would like to delete.```',
components=[
create_actionrow(
create_select(
placeholder='Time Slot',
options= optionsList,
min_values=1,
max_values=1
)
)
],
delete_after=5,
# hidden=True Can't have hidden responses for menus apparently.
)
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 not in data[guildStr] or not data[guildStr][timeslot]:
await ctx.send(f'```Timeslot {conf[guildStr]["timeslots"][timeslot]} with the key `{timeslot}` has been deleted for the guild `{ctx.guild.name}`.```',hidden=True)
conf[guildStr]['timeslots'].pop(timeslot, None)
yaml_dump(conf, configFile)
if not any([x['timeslots'] for x in yaml_load(configFile).values()]):
unloadCog(f'./{cogsDir}/slashcommands/secondary/manipulate_timeslots.py')
if self.client.get_cog('Game Management') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
if self.client.get_cog('Game Create') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/game_create.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()
else:
await ctx.send('```Error: You cannot delete a timeslot that has existing game entries. Please delete all games first.```',hidden=True)
@cog_ext.cog_subcommand(
base='config',
subcommand_group='timeslots',
name='modify',
description='Modify the value of a configured gametime slot.',
# base_description='Commands for configuring the various parameters of 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='key',
description='Key of timeslot being modified',
option_type=3,
required=True,
),
create_option(
name='name',
description='New value for timeslot name',
option_type=3,
required=True
)
]
)
async def _config_timeslots_modify(self, ctx:SlashContext, key:str, name:str):
conf = yaml_load(configFile)
guildStr = str(ctx.guild.id)
if 'timeslots' not in conf[guildStr]:
conf[guildStr]['timeslots'] = {}
if key in conf[guildStr]['timeslots']:
await ctx.send(f'```Timeslot {conf[guildStr]["timeslots"][key]} with the key `{key}` has been renamed to {name} for the guild `{ctx.guild.name}`.```',hidden=True)
conf[guildStr]['timeslots'][key] = name
yaml_dump(conf, configFile)
elif conf[guildStr]['timeslots']:
output = f'```Timeslot `{key}` was not found in the guild `{ctx.guild.name}`. Please enter a valid key.\n\n Available timeslots are:\n(key): (timeslot name)'
for c in conf[guildStr]['timeslots']:
output = ''.join([output, f'\n {c}: {conf[guildStr]["timeslots"][c]}'])
await ctx.send(''.join([output,'```']),hidden=True)
else:
await ctx.send(f'```No timeslots have been defined for the guild `{ctx.guild.name}`.```',hidden=True)
@cog_ext.cog_subcommand(
base='config',
subcommand_group='timeslots',
name='list',
description='List the existing game timeslots on the server.',
# base_description='Commands for configuring the various parameters of 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 _config_timeslots_list(self, ctx:SlashContext):
conf = yaml_load(configFile)
guildStr = str(ctx.guild.id)
if 'timeslots' not in conf[guildStr]:
conf[guildStr]['timeslots'] = {}
if conf[guildStr]['timeslots']:
output = f'```The following timeslots have been configured for the guild {ctx.guild.name}:\n(key): (timeslot name)'
for c in conf[guildStr]['timeslots']:
output = ''.join([output, f'\n {c}: {conf[guildStr]["timeslots"][c]}'])
await ctx.send(''.join([output,'```']))
else:
await ctx.send(f'```No timeslots have been defined for the guild `{ctx.guild.name}`.```',hidden=True)
def setup(client):
client.add_cog(ManipulateTimeslots(client))

View File

@ -0,0 +1,186 @@
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
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile, pitchesFile, loadCog
#### Pitch Command
class Pitch(commands.Cog, name='Pitch Command'):
def __init__(self, client):
self.client = client
conf=yaml_load(configFile)
permissions={}
guild_ids = list(set.intersection(set([int(guildKey) for guildKey in yaml_load(configFile) if yaml_load(configFile)[guildKey]['timeslots']]),set([int(guildKey) for guildKey in yaml_load(configFile) if 'bot' in yaml_load(configFile)[guildKey]['roles'] and type(yaml_load(configFile)[guildKey]['roles']['bot']) is int])))
for guildID in guild_ids:
permissions[guildID] = []
permissions[guildID].append(create_permission(id=conf[str(guildID)]['owner'],id_type=SlashCommandPermissionType.USER,permission=True))
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_slash(
name='pitch',
description='Initialises the menu for game pitches.',
default_permission=False,
permissions=permissions,
guild_ids=guild_ids,
)
async def _pitch(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)
pitches = yaml_load(pitchesFile)
if guildStr not in pitches: pitches[guildStr] = {}
if pitches[guildStr]:
await ctx.send(f'```Error: pitches are already running for the guild `{ctx.guild.name}`. Please close the existing pitches first before issuing this command.```')
return
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]
try:
m = await ctx.send(
content='```Select which time slot for which you would like to run pitches for.```',
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
await ctx.channel.trigger_typing()
p = discord.PermissionOverwrite()
p.read_messages = False
p.send_messages = False
await ctx.channel.edit(
reason=f'/pitch command issued by {ctx.author.display_name}',
overwrites = {
ctx.guild.default_role: p
}
)
if timeslot not in pitches[guildStr]: pitches[guildStr][timeslot] = {}
pitches[guildStr][timeslot]['indices'] = {}
pitches[guildStr][timeslot]['entries'] = [x for x in data[guildStr][timeslot].values()]
pitches[guildStr][timeslot]['entries'].sort(key= lambda x: x['game_title'])
header_message = await ctx.channel.send(
content=f'**Game listing for {conf[guildStr]["timeslots"][timeslot]}**\n_ _```The following are the games that are being pitched. Please select which game you would like to join by clicking on the `Join` button below.```'
)
pitches[guildStr][timeslot]['header_message'] = header_message.id
pitches[guildStr][timeslot]['messages'] = []
pitches[guildStr][timeslot]['roles'] = {}
for index, element in enumerate(pitches[guildStr][timeslot]['entries']):
gm = await self.client.fetch_user(element["gm"])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
m = await ctx.channel.send(
content=o,
components=[
create_actionrow(
create_button(
style=ButtonStyle.green,
label='Join',
emoji='🉑',
custom_id=f'join_{index}'
),
create_button(
style=ButtonStyle.red,
label='Leave',
emoji='🈳',
custom_id=f'leave_{index}'
)
)
]
)
pitches[guildStr][timeslot]['messages'].append(m.id)
r = discord.utils.find(lambda x: x.id == element['role'],ctx.guild.roles)
pitches[guildStr][timeslot]['roles'][index] = r.id
pitches[guildStr][timeslot]['indices'][r.id] = index
newcomer = returning_player = None
if 'newcomer' in conf[guildStr]['roles']: newcomer = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['newcomer'], ctx.guild.roles)
if 'returning_player' in conf[guildStr]['roles']: returning_player = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['returning_player'], ctx.guild.roles)
buttons = []
if returning_player is not None:
buttons.append(
create_button(
style= ButtonStyle.grey,
label= 'Allow Returning Players',
emoji= '🔁',
custom_id='allow_returning'
)
)
if newcomer is not None:
buttons.append(
create_button(
style= ButtonStyle.grey,
label= 'Allow Newcomers',
emoji= '🆕',
custom_id='allow_newcomers'
)
)
buttons.append(
create_button(
style= ButtonStyle.green,
label= 'Allow All',
emoji='🚪',
custom_id='allow_all'
)
)
buttons.append(
create_button(
style= ButtonStyle.red,
label= 'Close Pitches',
emoji='🔒',
custom_id='close_pitches'
)
)
control = await ctx.channel.send(
content='_ _\n```Control Panel:\nFor Admin Use Only```',
components=[
create_actionrow(
*buttons
)
]
)
pitches[guildStr][timeslot]['control'] = control.id
yaml_dump(pitches,pitchesFile)
if self.client.get_cog('Pitch Listener') is None:
loadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
#### Activate global pitch listener
def setup(client):
client.add_cog(Pitch(client))

View File

@ -0,0 +1,239 @@
import os # OS Locations
import yaml # YAML parser for Bot config files
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option, create_permission # Slash Command features
from discord_slash.model import SlashCommandPermissionType
from bot import configFile, yaml_load, yaml_dump, lookupFile, dataFile, gmFile, categoriesFile
##### Player Add, Remove, and Leave Commands
class PlayerCommands(commands.Cog, name='Player Commands'):
def __init__(self, client):
self.client = client
conf = yaml_load(configFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guild_ids= [ int(x) for x in list(lookup)]
@cog_ext.cog_subcommand(
base='player',
name='add',
description='Add a player to a game.',
base_description='Commands to manage players in a game.',
base_default_permission=True,
guild_ids = guild_ids,
options = [
create_option(
name='player',
description='The player you want to add to the game.',
option_type=6,
required=True
),
create_option(
name='game',
description='The role of the game you want to add the player to.',
option_type=8,
required=True
)
]
)
async def _player_add(
self,
ctx:SlashContext,
player:discord.User,
game:discord.Role
):
await ctx.channel.trigger_typing()
conf = yaml_load(configFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
guildStr = str(ctx.guild.id)
rStr = str(game.id)
if rStr not in lookup[guildStr]:
await ctx.send(f'```Error: This is not a valid game role. Please mention a role that is associated with a game.```',hidden=True)
return
if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[str(ctx.guild.id)]['roles']['admin']]) or ctx.author == ctx.guild.owner):
if ctx.author.id != lookup[guildStr][rStr]['gm']:
await ctx.send(f'```Error: You are not authorised to issue this command. A player may only be added to a game by the GM or by an administrator.```',hidden=True)
return
if game in player.roles:
await ctx.send(f'```Error: Player `{player.display_name}` is already in the game {lookup[guildStr][rStr]["game_title"]}.```',hidden=True)
return
await player.add_roles(game, reason=f'`/player add` command issued by {ctx.author.display_name}`.')
t = lookup[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 = 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:
header = discord.utils.find(lambda x: x.id == hm, p)
if header is not None:
text = header.content.split('\n')
for line, item in enumerate(text):
if 'Current Players: ' in item:
text[line] = f'Current Players: {str(data[guildStr][t][rStr]["current_players"])}'
break
await header.edit(content='\n'.join(text))
else:
tPos = len(ctx.guild.channels)
cat = discord.utils.find(lambda x: x.id==lookup[guildStr][rStr]['category'], ctx.guild.categories)
if cat is not None:
for t in cat.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
await tc.send(f'```Player {player.display_name} has been added to the game by `{ctx.author.display_name}`.```\n{player.mention} | {game.mention}')
if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels)
r = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['committee'], ctx.guild.roles)
await c.send(f'```Player `{player.display_name}` has been added to the game `{lookup[guildStr][rStr]["game_title"]}` by `{ctx.author.display_name}` via the `/player add` command.```')
yaml_dump(data,dataFile)
await ctx.send(content=f'```Player `{player.display_name}` has been added to the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True)
@cog_ext.cog_subcommand(
base='player',
name='remove',
description='Remove a player from a game.',
# base_description='Commands to manage players in a game.',
base_default_permission=True,
guild_ids = guild_ids,
options = [
create_option(
name='player',
description='The player you want to remove from the game.',
option_type=6,
required=True
)
]
)
async def _player_remove(
self,
ctx:SlashContext,
player:discord.User
):
await ctx.channel.trigger_typing()
conf = yaml_load(configFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
if str(ctx.channel.category.id) not in categories[guildStr]:
await ctx.send(f'```Error: This command can only be issued in a text channel associated with a game.```', hidden=True)
return
game = discord.utils.find(lambda x: x.id == categories[guildStr][str(ctx.channel.category.id)], ctx.guild.roles)
rStr = str(game.id)
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 ctx.author.id != lookup[guildStr][rStr]['gm']:
await ctx.send(f'```Error: You are not authorised to issue this command. A player may only be added to a game by the GM or by an administrator.```',hidden=True)
return
if game not in player.roles:
await ctx.send(f'```Error: Player `{player.display_name}` is not in the game {lookup[guildStr][rStr]["game_title"]}.```',hidden=True)
return
await player.remove_roles(game, reason=f'`/player remove` command issued by {ctx.author.display_name}`.')
t = lookup[guildStr][rStr]['time']
if data[guildStr][t][rStr]['current_players'] <= 1: data[guildStr][t][rStr]['current_players'] = 0
else: data[guildStr][t][rStr]['current_players'] -= 1
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 is not None:
header = discord.utils.find(lambda x: x.id == hm, p)
if header is not None:
text = header.content.split('\n')
for line, item in enumerate(text):
if 'Current Players: ' in item:
text[line] = f'Current Players: {str(data[guildStr][t][rStr]["current_players"]) if data[guildStr][t][rStr]["current_players"] is not None else str(0)}'
break
await header.edit(content='\n'.join(text))
else:
tPos = len(ctx.guild.channels)
cat = ctx.channel.category
for t in cat.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
await tc.send(f'```Player {player.display_name} has been removed from the game by `{ctx.author.display_name}`.```\n{game.mention}')
if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels)
r = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['committee'], ctx.guild.roles)
await c.send(f'```Player `{player.display_name}` has been removed from the game `{lookup[guildStr][rStr]["game_title"]}` by `{ctx.author.display_name}` via the `/player remove` command.```')
yaml_dump(data,dataFile)
await ctx.send(content=f'```Player `{player.display_name}` has been removed from the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True)
@cog_ext.cog_subcommand(
base='player',
name='leave',
description='Leave a game.',
# base_description='Commands to manage players in a game.',
base_default_permission=True,
guild_ids = guild_ids
)
async def _player_leave(
self,
ctx:SlashContext
):
await ctx.channel.trigger_typing()
conf = yaml_load(configFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
player = ctx.author
if str(ctx.channel.category.id) not in categories[guildStr]:
await ctx.send(f'```Error: This command can only be issued in a text channel associated with the game you are trying to leave.```', hidden=True)
return
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: You are not in the game {lookup[guildStr][rStr]["game_title"]}.```',hidden=True)
return
if player.id == lookup[guildStr][rStr]['gm']:
await ctx.send(f'```Error: You are the GM of the game {lookup[guildStr][rStr]["game_title"]} and cannot leave until a new GM is assigned.```',hidden=True)
return
await player.remove_roles(game, reason=f'`/player leave` command issued by {ctx.author.display_name}`.')
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 = 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 is not None:
header = discord.utils.find(lambda x: x.id == hm, p)
if header is not None:
text = header.content.split('\n')
for line, item in enumerate(text):
if 'Current Players: ' in item:
text[line] = f'Current Players: {str(data[guildStr][t][rStr]["current_players"]) if data[guildStr][t][rStr]["current_players"] is not None else str(0)}'
break
await header.edit(content='\n'.join(text))
else:
tPos = len(ctx.guild.channels)
cat = ctx.channel.category
for t in cat.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
await tc.send(f'```Player `{player.display_name}` has left the game.```\n{game.mention}')
if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels)
r = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['committee'], ctx.guild.roles)
await c.send(f'```Player `{player.display_name}` has left the game `{lookup[guildStr][rStr]["game_title"]}` via the `/player leave` command.```')
yaml_dump(data,dataFile)
await ctx.send(content=f'```You have left the game `{lookup[guildStr][rStr]["game_title"]}`.```', hidden=True)
def setup(client):
client.add_cog(PlayerCommands(client))

View File

@ -0,0 +1,83 @@
import os # OS Locations
import yaml # YAML parser for Bot config files
import asyncio # Discord Py Dependency
import discord # Main Lib
from datetime import datetime
import time
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option, create_permission # Slash Command features
from discord_slash.model import SlashCommandPermissionType
from bot import configFile, yaml_load, yaml_dump, lookupFile, dataFile, gmFile, categoriesFile
#### T Card Command
class TCardCommand(commands.Cog, name='T-Card Command'):
def __init__(self, client):
self.client = client
conf = yaml_load(configFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guild_ids= [ int(x) for x in list(lookup)]
@cog_ext.cog_slash(
name='tcard',
description='Invokes a T-Card in the current game.',
default_permission=True,
guild_ids=guild_ids,
)
async def _tcard(self, ctx:SlashContext):
conf = yaml_load(configFile)
lookup = yaml_load(lookupFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
embed = discord.Embed(
title='T-Card',
description='A T-Card Has Been Played',
colour=discord.Color.dark_red(),
)
embed.set_footer(text=datetime.now().strftime('%a %-d %b %y, %-I:%M %p'))
embed.set_image(url='http://geas.org.uk/wp-content/uploads/2020/08/tcard-1-e1597167776966.png')
"""If this was called in a game channel"""
if str(ctx.channel.category.id) in categories[guildStr]:
"""Send a T-Card graphic to the channel, tagging the role."""
role = ctx.guild.get_role(categories[guildStr][str(ctx.channel.category.id)])
await ctx.channel.send(f'{role.mention}', embed=embed)
if ctx.author_id == lookup[guildStr][str(role.id)]["gm"]:
"""Behaviour for when the GM issues T-Card command."""
await ctx.send(content=f'```You have invoked the T-Card in the game {lookup[guildStr][str(role.id)]["game_title"]} as the GM.```', hidden=True)
else:
"""Default behaviour for when someone who is not the GM issues the command."""
"""Privately message the GM."""
gm = await ctx.guild.fetch_member(lookup[guildStr][str(role.id)]["gm"])
await gm.send(f'**Important**\n```Player {ctx.author.display_name} has invoked a T-Card in your game {lookup[guildStr][str(role.id)]["game_title"]}. Please check in with them and make sure everything is okay. If you need any further help, please feel free to contact a member of the Committee.```')
"""Notify the issuer of the command privately via hidden reply."""
await ctx.send(content=f'```You have invoked the T-Card in the game {lookup[guildStr][str(role.id)]["game_title"]}. The GM has been notified privately.```', hidden=True)
"""Do the audio thing."""
opus = discord.opus.load_opus('/usr/lib/x86_64-linux-gnu/libopus.so.0')
for vc in ctx.channel.category.voice_channels:
v = await vc.connect()
tcardaudio = discord.PCMAudio(open("./assets/tcard.wav", "rb"))
v.play(tcardaudio)
while v.is_playing(): time.sleep(.1)
await v.disconnect()
else:
"""Send a T-Card to the immediate channel if this is a generic channel."""
await ctx.channel.send(embed=embed)
await ctx.send(content=f'```You have invoked the T-Card in the channel {ctx.channel.name}.```', hidden=True)
def setup(client):
client.add_cog(TCardCommand(client))

0
app/data/.gitkeep Normal file
View File

View File

@ -0,0 +1,30 @@
guild_id_string:
channels: # Dictionary
# help is an optional feature so not necessary in blueprint.
mod: 0
signup: 0
configured: false
membership: [0] # List of integers
# For membership, at least one kind needs to be defined. But no key is mandatory.
name: string
owner: 0
prefix: string
roles: # dictionary
admin:
- 0 # List
# For admins, at least one needs to be defined.
# committee notifications is optional so is not necessary in blueprint.
bot: 0
# newcomer role is optional
# returning player role is optional
# student role is optional
timeslots: {} # Dictionary
# At least one needs to be defined.
meta:
strict:
- channels
- roles
notifications:
help: true
signup: true
initialised: true

View File

@ -1,5 +1,26 @@
discord
python-dotenv
wheel
pymongo
datetime
aiohttp==3.7.4.post0
async-timeout==3.0.1
asyncio==3.4.3
attrs==21.2.0
cffi==1.14.6
chardet==4.0.0
DateTime==4.3
deepdiff==5.5.0
discord==1.7.3
discord-py-slash-command==2.4.0
discord.py==1.7.3
idna==3.2
multidict==5.1.0
ordered-set==4.0.2
packaging==21.0
pycparser==2.20
PyNaCl==1.4.0
pyparsing==2.4.7
python-dotenv==0.19.0
pytz==2021.1
PyYAML==5.4.1
six==1.16.0
style==1.1.6
typing-extensions==3.10.0.0
yarl==1.6.3
zope.interface==5.4.0

View File

@ -1,4 +1,4 @@
version: '3.4'
version: '3.1'
services:
geasbot-app:
@ -6,24 +6,3 @@ services:
volumes:
- ./app:/usr/src/app
restart: always
depends_on:
- geasbot-db
environment:
- BOT_TOKEN=${BOT_TOKEN}
- TEST_TOKEN=${TEST_TOKEN}
- MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD}
- BOT_VERSION=${BOT_VERSION}
geasbot-db:
image: mongo:latest
volumes:
- ./db:/data/db
expose:
- "27017"
environment:
- BOT_TOKEN=${BOT_TOKEN}
- TEST_TOKEN=${TEST_TOKEN}
- MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD}
- BOT_VERSION=${BOT_VERSION}

46
resources.md Normal file
View File

@ -0,0 +1,46 @@
# 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)
> 4. [Discord Py Slach Commands library Components Documentation](https://discord-py-slash-command.readthedocs.io/en/latest/components.html)
`N.B.: Components are what Discord calls buttons and drop down menus.
There are multiple libraries that offer Components integration, but the Discord Py Slash Command is adequate in doing so, and has the clearest documentation so far.
It's best to stick to one library to keep it consistent.
Avoid the Discord_components library as it causes conflicts with Discord Py Slash Commands.`
## 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)
4. [Using the new Slash Command API](https://www.youtube.com/watch?v=CLQ8gfb2jh4)
## 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)