Compare commits

...

2 Commits

Author SHA1 Message Date
1cc79c3275 Readme Update 2022-01-24 12:29:31 +00:00
43e10e8034 Re-build using different core libraries. 2022-01-24 12:26:14 +00:00
39 changed files with 10 additions and 3927 deletions

View File

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

View File

@ -1,29 +0,0 @@
# Changelog
## Minor Updates in Version 3.0.1
- Re-builds the Docker image to use a `python:slim` base instead of `python:buster`, reducing the footprint of the image to a quarter of its previous size.
## 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.`

View File

@ -1,120 +0,0 @@
# 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. |

226
README.md
View File

@ -1,224 +1,6 @@
# Geas Server Bot
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 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.
## Setup
The Bot is dockerised and uses docker-compose for deployment, so it is fairly straightforward to deploy an instance of.
Clone the repository, install Docker and Docker Compose, navigate to the root directory (that contains the `docker-compose.yml` file), and use `docker-compose up -d` to set up and run the bot.
The bot runs on one Docker container with the instance of the app as well as storage for its data and configuration.
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 provide the following values in a `.env` file in the root directory.
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.
`.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)
```
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.
**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:
```py
client.run(os.getenv('TEST_TOKEN'))
```
to
```py
client.run(os.getenv('BOT_TOKEN'))
```
in order for to authenticate as the correct bot.
## File Structure
```
|-- 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
```
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.
## Data Structure
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.
Other developers for Discord Components recommended the use of dynamic call-backs, however these do not persist after reboots so are not suitable for this purpose.
Discord will need to make substantial API updates in the future for rolling out the Threads feature.
The bot will need updating again then.
The libraries I had used for making this bot have been deprecated.
This was accompanied by a number of significant changes to the Discord API and its features.
This is a re-build of the bot using new libraries that continue to be maintained.
This version further integrates the new features and API changes.

11
TODO.md
View File

@ -1,11 +0,0 @@
# To Do for Version 3.1
## Docker
- [x] Re-build using `python:slim` as base image
## Bot Functionality and Processes
- [ ] Add support for Discord Threads following Discord Py library update
- [ ] Update permission settings for GMs on game channels to support threads
- [ ] ~~Change component response architecture to use dynamic callback functions instead of global listeners.~~ Not suitable because it does not provide persistence across reboots.

View File

@ -1,9 +0,0 @@
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,7 +0,0 @@
FROM python:3.9.6-slim
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update -y && apt-get upgrade -y && apt-get install libopus0 -y && \
pip install --upgrade pip setuptools wheel && pip install -r requirements.txt && \
apt-get autoremove -y
CMD python3 -u ./bot.py

Binary file not shown.

View File

@ -1,289 +0,0 @@
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
## Define YAML functions
def yaml_load(filepath:str):
### Loads a YAML file
with open(filepath, 'r') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
return data
def yaml_dump(data:dict, filepath:str):
### Dumps a YAML file
with open(filepath, 'w') as file:
yaml.dump(data, file)
# 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'
if not os.path.exists(configFile): yaml_dump({},configFile)
# 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'
if not os.path.exists(dataFile): yaml_dump({},dataFile)
# Locate or create lookup file. Same as above.
lookupFile = os.getenv('LOOKUP') if os.getenv('LOOKUP').endswith('.yml') or os.getenv('LOOKUP').endswith('.yaml') else './data/lookup.yml'
if not os.path.exists(lookupFile): yaml_dump({},lookupFile)
# Locate or create GM lookup file. Same as above.
gmFile = os.getenv('GM') if os.getenv('GM').endswith('.yml') or os.getenv('GM').endswith('.yaml') else './data/gm.yml'
if not os.path.exists(gmFile): yaml_dump({},gmFile)
# Locate 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'
if not os.path.exists(categoriesFile): yaml_dump({},categoriesFile)
# 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'
if not os.path.exists(pitchesFile): yaml_dump({},pitchesFile)
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.')
# 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:
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)
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)
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
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
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))
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))
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))
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'):
loadCog(f'./{cogsDir}/{category}/{cogfile}')
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,33 +0,0 @@
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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,201 +0,0 @@
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 _response_defer(self, ctx:ComponentContext):
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
await ctx.defer(hidden = True)
@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

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

@ -1,54 +0,0 @@
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 message.channel.category is None: return
if str(message.channel.category.id) not 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

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

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

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

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

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

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

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

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

View File

View File

@ -1,30 +0,0 @@
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,26 +0,0 @@
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,9 +0,0 @@
version: '3.5'
services:
geasbot-app:
build: ./app
container_name: geas_bot
volumes:
- ./app:/usr/src/app
restart: unless-stopped

View File

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