Compare commits

..

No commits in common. "master" and "9014bdaac438c1761ce5651ba5cc2a439876a8c3" have entirely different histories.

21 changed files with 168 additions and 164 deletions

View File

@ -1,9 +1,5 @@
# 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

View File

@ -71,12 +71,6 @@ These commands are locked until there is at least one game configured.
| `/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.

View File

@ -17,26 +17,10 @@ It no longer uses a database engine because it never really benefitted from the
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.
You will also need this database to set up a username and password for the MongoDB database.
The specific username and password don't matter as the bot refers back to the environment variable when authenticating.
The following is the template for the `.env` file, with the variable names as are referenced in the bot's code:
`.env` file:
```DotENV
@ -53,7 +37,8 @@ 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:
**N.B.**: When the bot is first run, it is configured to log in as the Test Bot, and not the main Geas Server Bot, as a safety measure.
To change this, navigate to the last line of the file `bot.py` and change the line:
```py
client.run(os.getenv('TEST_TOKEN'))
@ -71,9 +56,7 @@ in order for to authenticate as the correct bot.
```
|-- app
| |-- .env.example
| |-- assets
| | `-- tcard.wav
| |-- .env
| |-- bot.py
| |-- cogs
| | |-- botcommands
@ -105,8 +88,7 @@ in order for to authenticate as the correct bot.
| | |-- game_management.py
| | |-- manipulate_timeslots.py
| | |-- pitch.py
| | |-- player_commands.py
| | `-- tcard.py
| | `-- player_commands.py
| |-- data
| | |-- .gitkeep
| | |-- categories.yml
@ -132,7 +114,7 @@ The `COMMANDS.md` file gives a list of all the commands the Bot uses, as well as
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.
I had initially condsiders a `.ini` file for the configuration settings and `.json` for the data, but I decided to use `.yml` for both just to avoid unnecessary complexity.
### `config.yml` Structure
@ -219,6 +201,5 @@ Programming around this will need a further layer of complexity, involving flags
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 best possible way of setting this up is probably using functions to dynamically set up and remove component call-backs.
Also components do not need to all have unique names, just unique names in the message they are part of.

108
TODO.md
View File

@ -1,11 +1,107 @@
# To Do for Version 3.1
# To Do
## Docker
- [ ] Testing
- [x] Dockerise
- [ ] Infer/Transfer data from old bot
- [ ] Deploy
- [x] Re-build using `python:slim` as base image
## Bot Architecture
- [x] Simplify directory tree
- [x] Split event listeners into individual cogs.
- [x] Update with re-organised data and config structure
- [x] Correct references to data in existing cogs.
- [x] Setup minimally functioning configs of guild on startup
- [x] Synchronise core configuration `/commands` on startup
- [ ] ~~Synchronise secondary `/commands` on complete configuration~~ ``(see below)``
## Bot Functionality and Processes
- [ ] 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.
- [ ] ~~'Delete Commands' Function~~
- [ ] ~~'Register Commands' Function~~
- [x] Infer Permissions from Config
- [x] Dynamic Command Prefixes
- [ ] Infer current games from Server Structure
`Create a separate cog to do this instead of having a migrate command, install the cog temporarily and remove the cog once migration is done.`
- [ ] Re-enable logging
- [x] Delete Dev/Test Functions
- [x] Error handlers
- [x] Debug Features
- [ ] ~~Command Installer/Uninstaller~~
- [x] Help Channel Event Listener
- [x] Add Config key for Help Channel
- [ ] ~~Slash Command Buttons or~~ `This kind of got subsumed into other features.`
- [ ] ~~Reaction listener selectors~~ `So did this.`
- [x] Member Verification
- [x] Add Config key membership signup channels
- [x] Add config keys: Membership Category Roles
- [x] Message Receive listener
- [x] ~~Message React listener~~ or buttons `Used buttons waiting within the same command thread. Possibly update to global listener if performance is affected.`
- [x] Membership Restriction
- [x] Message Receive Listener
- [x] Membership Validation Listener
- [x] Re-synchronise commands after any relevant config changes `(See from above)`
- [ ] Role Delete (~~member~~, ~~admin~~, ~~game~~) `Admin role missing won't cause any issues as server role will still remain in control`
`Deleting membership roles will also trigger a mess. Event listener will trigger and attempt to execute simultaneously with the command to delete the role, which will cascade into several errors. In order to make this work, it is best to have the commands interact solely with the roles, and then subsequently the listeners to sync with the data.`
- [ ] ~~Channel delete (notifications, logs, game text channel)~~ `There aren't any critical settings that depend on this just yet, and it is hard to set this up without circularity`
- [ ] ~~Category delete (games)~~ `Circularity problem: if the category delete event listener is set up, it will react to the bot deleting categories when managing games.`
- [x] Flag for checking completeness of configuration for a guild.
- [x] Function for checking configs for completeness
- [ ] ~~Synchronise game channel on role updates~~
- [ ] ~~Exception to event listener to prevent circularity~~ ~~`unsure what this means`~~
`I remember what this is now: if you have a listener for when roles get deleted, and if the bot deletes a role, then it triggers the listener. It might be worth restructuring this such that the Bot command only deletes the role, which then triggers the listener that deletes the categories and synchronises data. That way, the roles can be deleted manually or via the command. Otherwise, it is not possible in the Discord API to have this exception, as the Bot only responds to the 'Guild' deleting the role, not the user or bot or admin that issued the command.`
## Event Listeners
## Review Configs When
- [x] Guild Changing Ownership
- [x] Roles Modified
- [x] Mod Channel Deleted
## Commands
- [x] Configure Bot function and sub commands
- [x] botrole (role group)
- [x] committeerole (role group)
- [x] modchannel (channel group)
- [x] help channel (channel group)
- [x] signup channel (channel group)
- [x] newcomer role (role group)
- [x] returning player role (role group)
- [x] student role (role group)
- [x] help notifications (notification group)
- [x] signup notifications (notification group)
- [x] Set up timeslots
- [x] Delete timeslots
- [x] Base command
- [x] ~~Delete all games with the timeslot~~
Do the opposite: block deleting timeslots with existing games.
- [x] List timeslots
- [x] Set up command permissions
- [x] Slash Commands
- [x] Admin Commands
- [x] Game Management Commands
- [x] Native Bot Commands
- [x] Migrate existing bot commands
- [x] setupgame
- [x] ~~definebotrole~~ config
- [x] deletegame
- [x] ~~reset~~ purge
- [ ] ~~migrate~~ `See above`
- [x] ~~kickplayer~~ `/player remove`
- [x] ~~addplayer~~ `/player add`
- [x] ~~leavegame~~ `/player leave`
- [x] Pitch command and sub-commands
- [ ] ~~run~~ `Combined both sub-commands into single command and prompt response.`
- [ ] ~~clear~~
## Misc
- [x] Review documentation
- [x] Finalise README.md
- [x] CHANGELOG.md
- [x] COMMANDS.md
- [x] resources.md
- [x] Make sure to document `not using discord_components and staying with discord-py-slash-commands library alone`.

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 +1,5 @@
FROM python:3.9.6-slim
COPY . /usr/src/app
FROM python:3.9.6-buster
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
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
CMD python3 -u ./bot.py

Binary file not shown.

View File

@ -75,8 +75,7 @@ def getPrefix(client, message):
# 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.'
command_prefix=getPrefix
)
slash = SlashCommand(
client,
@ -279,7 +278,6 @@ if yaml_load(configFile):
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')

View File

@ -24,6 +24,9 @@ class Debug(commands.Cog, name='Debug Commands'):
if role.id == conf[str(ctx.guild.id)]['roles']['maintainer']: return True
return ctx.author.id == ctx.guild.owner_id
async def cog_check(self, ctx):
return ctx.author.id == int(os.getenv('BOT_MAINTAINER_ID'))
@commands.command(
name='testconfig',
description='Tests the completeness of the configuration values of the current guild by comparing it to a configuration blueprint.',

View File

@ -16,12 +16,8 @@ class Migrate(commands.Cog, name='Migrate Command'):
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
async def cog_check(self, ctx):
return ctx.author.id == int(os.getenv('BOT_MAINTAINER_ID'))
@commands.command(
name='migrate',

View File

@ -17,15 +17,6 @@ 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)
@ -75,14 +66,13 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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
if ctx.author.id != lookup[guildStr][str(role.id)]['gm']:
data[guildStr][timeslot][str(role.id)]['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'])
@ -94,9 +84,9 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['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)
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
@ -106,15 +96,13 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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)
await ctx.send(f'```Error: You are already in the game `{lookup[guildStr][str(role.id)]["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
data[guildStr][timeslot][str(role.id)]['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'])
@ -126,10 +114,10 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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)
await ctx.send(f'You have joined the game `{lookup[guildStr][str(role.id)]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['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)
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
@ -138,27 +126,15 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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)
await ctx.send(f'```Error: You are not in the game `{lookup[guildStr][str(role.id)]["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
if ctx.author.id != lookup[guildStr][str(role.id)]['gm']:
data[guildStr][timeslot][str(role.id)]['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'
@ -171,10 +147,10 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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)
await ctx.send(f'You have left the game `{lookup[guildStr][str(role.id)]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['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)
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
@ -183,17 +159,6 @@ class PitchListener(commands.Cog, name='Pitch Listener'):
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)

View File

@ -27,14 +27,14 @@ class MemberVerification(commands.Cog, name='Member Verification Cog'):
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.channel.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.grey, label='Alert', emoji='⚠️', custom_id=f'alert_{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}`.```'
@ -89,36 +89,36 @@ class MemberVerification(commands.Cog, name='Member Verification Cog'):
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.```')
await submission.channel.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)
elif ctx.custom_id.startswith('alert_'):
await ctx.send(f'```Membership verification alert raised.```', 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.```')
await submission.channel.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}`.```')
await submission.channel.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}`.```')
await submission.channel.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}`.```')
await submission.channel.send(f'```Your role `{selected_role.name}` has been removed in the guild `{submission.guild.name}`.```')
else:
pass

View File

@ -23,8 +23,7 @@ class RestrictionListener(commands.Cog, name='Membership Restriction Listener'):
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 str(message.channel.category) in categories[guildStr]: return
if (set(message.author.roles) & set([message.guild.get_role(x) for x in conf[guildStr]['roles']['admin']]) or message.author == message.guild.owner): return
if set(message.author.roles) & set([message.guild.get_role(x) for x in conf[guildStr]['membership']]): return
if message.channel.overwrites_for(message.author).manage_channels: return

View File

@ -123,13 +123,10 @@ class Configuration(commands.Cog, name='Configuration Commands'):
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()
if flag: await self.client.slash.sync_all_commands()
@cog_ext.cog_subcommand(
base='config',
@ -304,9 +301,6 @@ class Configuration(commands.Cog, name='Configuration Commands'):
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

View File

@ -290,9 +290,6 @@ class GameCreate(commands.Cog, name='Game Create'):
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

View File

@ -97,7 +97,6 @@ class GameManagement(commands.Cog, name='Game Management'):
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()
@ -554,7 +553,6 @@ class GameManagement(commands.Cog, name='Game Management'):
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()

View File

@ -91,11 +91,14 @@ class ManipulateTimeslots(commands.Cog, name='Manipulate Timeslots'):
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')
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('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)

View File

@ -148,7 +148,7 @@ class PlayerCommands(commands.Cog, name='Player Commands'):
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:
if p:
header = discord.utils.find(lambda x: x.id == hm, p)
if header is not None:
text = header.content.split('\n')
@ -211,7 +211,7 @@ class PlayerCommands(commands.Cog, name='Player Commands'):
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:
if p:
header = discord.utils.find(lambda x: x.id == hm, p)
if header is not None:
text = header.content.split('\n')

View File

@ -37,10 +37,11 @@ class TCardCommand(commands.Cog, name='T-Card Command'):
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
# rStr = str(game.id)
embed = discord.Embed(
title='T-Card',
description='A T-Card Has Been Played',
colour=discord.Color.dark_red(),
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')
@ -67,12 +68,12 @@ class TCardCommand(commands.Cog, name='T-Card Command'):
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)
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."""

View File

@ -2,25 +2,20 @@ 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-slash-command==2.3.2
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
python-dotenv==0.18.0
pytz==2021.1
PyYAML==5.4.1
six==1.16.0
style==1.1.6
style==1.1.0
typing-extensions==3.10.0.0
update==0.0.1
yarl==1.6.3
zope.interface==5.4.0

View File

@ -1,9 +1,8 @@
version: '3.5'
version: '3.1'
services:
geasbot-app:
build: ./app
container_name: geas_bot
volumes:
- ./app:/usr/src/app
restart: unless-stopped
restart: always