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

15
.gitignore vendored
View File

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

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

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

View File

@@ -1,70 +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 am committing to the repository is version 2.1, and I previously handled the version control manually, so migrating old versions to Git would be a pain.
## Bot Setup
The Bot is dockerised and uses docker-compose for deployment, so it is fairly straightforward to set up.
Clone the repository, install Docker and Docker Compose, navigate to the root directory (that contains the `docker-compose.yml` file), and use `docker-compose up -d` to set up and run the bot.
The bot uses two containers that are networked internally:
> 1. A python app that runs the bot, and
> 2. A MongoDB database that stores the bot's data for persistence.
The database is not exposed externally to the network, and can only be accessed by the Bot in the network of containers.
The bot authenticates using an API key, which I have kept private in a `.env` file that I have not uploaded to the repository.
In order to set up your own instance of the bot, you will need to create two copies of the `.env` file, one in the root directory and one in the `app` folder, and enter the respective values for the API keys for the Geas Server Bot and the Test Bot.
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:
```
BOT_TOKEN=
TEST_TOKEN=
MONGO_INITDB_ROOT_USERNAME=
MONGO_INITDB_ROOT_PASSWORD=
BOT_VERSION=2.1.1
```
The correct API keys need to be entered in the environment variables in the `.env` file, and for a copy of this file to be placed in the root and the `app` directories.
Also create a new folder in the root directory called `db`. This is to ensure there is persistence of data when the bot is updated.
**N.B.**: 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:
```
client.run(os.getenv('TEST_TOKEN'))
```
to
```
client.run(os.getenv('BOT_TOKEN'))
```
in order for to authenticate as the correct bot.
## Bot Structure
The bot is divided into the following files:
```
app folder
| bot.py -- bot core functionality and code entrypoint
| Dockerfile -- Docker instructions on building the bot
| requirements.txt -- Dependencies to be installed
----cogs -- Individual modules for specific features
GameManagement.py -- adding or kicking players
HelpNotifier.py -- notifications for Help channel
MembershipRestriction.py -- restrictions unverified users
MembershipVerification.py -- membership verification system
PitchMenu.py -- automation for generating menus for game pitches
```
Many of the specific features, such as the bot's prefix, the roles it recognises as Committee, the channels it recognises as the Help or Membership Verification channels, are all hard-coded into the Bot.
This is because the bot was only ever supposed to be used on one server, so did not need the flexibility of adapting to multiple channels.
In the future, if I ever tinker with this in the future, I might try and add flexibility in the channels and roles it defines for its various functions.
I might also, in future incarnations, not use a database.
It was fun to learn how to use a database, but it is overkill.
## Bot Commands
A full list of bot commands can be retrieved using the `-help` command in the bot, and this might be an easier way of retrieving the commands than having a separate copy in the documentation.
The 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.

View File

@@ -1,6 +0,0 @@
FROM python:3.8.6-buster
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
CMD python3 -u ./bot.py

View File

@@ -1,458 +0,0 @@
# Import Dependencies
import os
import pymongo
import discord
from discord.ext import commands, tasks
# Set Intents
intents = discord.Intents.all()
intents.typing = True
intents.presences = True
intents.members = True
caches = discord.MemberCacheFlags.all()
# Set Prefix
p = '¬'
# Define Global State Dictionary
state = {}
# Create Clients
dbClient = pymongo.MongoClient(host='geasbot-db', username=os.environ['MONGO_INITDB_ROOT_USERNAME'], password=os.environ['MONGO_INITDB_ROOT_PASSWORD'], authsource='admin', serverSelectionTimeoutMS=1000)
client = commands.Bot(command_prefix=p, description=f'Geas Server Bot v {os.getenv("BOT_VERSION")}. This is a bot to facilitate setting up game channels on the Geas server. The prefix for the bot is {p}. You can interact with and manipulate game channels that have been created with this bot by @-mentioning the relevant role associated with the game.', intents=intents)
# Define Game Times Dictionary
gameTimes = {
'wed': 'WED',
'sunaft': 'SUN AFT',
'suneve': 'SUN EVE',
'oneshot': 'ONE SHOT'
}
# Reference Time Codes
def gameTime(arg):
return gameTimes.get(arg, 'OTHER')
# List Time Codes
def timeSlotList():
l = []
for t in gameTimes:
l.append(gameTimes[t])
l.append('OTHER')
return l
# Lookup Game Time Slot
def dbFindTimeslot(guild, role):
db = dbClient[str(guild.id)]
if role.name.split(': ',maxsplit=1)[0] not in timeSlotList():
raise commands.CommandError(f'Invalid lookup value: {role.mention} is not a valid game role.')
try:
for c in db.list_collection_names():
ret = db[c].find_one({'role':role.id})
if ret != None and c != 'settings':
return c
except:
return role.name.split(': ',maxsplit=1)[0]
# Lookup Category from Role
def dbLookupRole(guild, role):
db = dbClient[str(guild.id)]
colName = dbFindTimeslot(guild, role)
try:
catID = db[colName].find_one({'role':role.id})['category']
except:
for cat in guild.categories:
if cat.name == role.name:
catID = cat.id
break
try:
return guild.get_channel(catID)
except:
raise commands.CommandError('Error: The game\'s corresponding category cannot be matched.')
# Get Settings from DB
def dbGetSettings():
try:
for db in dbClient.list_database_names():
if db not in ['admin','config','local']:
state[db] = {
'settings': dbClient[db]['settings'].find_one({'_id':0}).copy()
}
print('Imported settings from database to local state.',state)
except Exception as err:
print('Failed to get Settings from Database due to error: ',err)
# Get list of game role IDs on server
def gameRoleIDList(guild):
l = []
for r in guild.roles:
if r.name.split(': ',maxsplit=1)[0] in timeSlotList():
l.append(r.id)
return l
# Get list of game role IDs in DB
def gameRoleDBList(guild):
dbName = str(guild.id)
db = dbClient[dbName]
l = []
try:
for ts in timeSlotList():
for e in db[ts].find():
l.append(e['role'])
return l
except:
pass
# Get list of game category IDs in DB
def gameCategoryDBList(guild):
dbName = str(guild.id)
db = dbClient[dbName]
l = []
try:
for ts in timeSlotList():
for e in db[ts].find():
l.append(e['category'])
return l
except:
pass
# Get list of GM IDs in DB
def gmDBList(guild):
dbName = str(guild.id)
db = dbClient[dbName]
l = []
try:
for ts in timeSlotList():
for e in db[ts].find():
l.append(e['gm'])
return l
except:
pass
# Sync Games on Server
def syncGames(guild):
dbName = str(guild.id)
db = dbClient[dbName]
try:
for ts in timeSlotList():
for e in db[ts].find():
if e['role'] not in gameRoleIDList(guild):
db[ts].delete_many({'role':e['role']})
for r in guild.roles:
if r.name.split(': ',maxsplit=1)[0] in timeSlotList():
if r.id not in gameRoleDBList(guild):
gameName = r.name.split(': ',maxsplit=1)[1]
colName = r.name.split(': ',maxsplit=1)[0]
for c in guild.categories:
if c.name == r.name:
break
permissions = c.overwrites
for p in permissions:
if isinstance(p,discord.Member) and permissions[p].manage_channels:
break
g = {
'game': gameName,
'gm': p.id,
'category': c.id,
'capacity': 5,
'role': r.id
}
db[colName].replace_one({'role':g['role']}, g, upsert=True)
for c in guild.categories:
if c.name.split(': ',maxsplit=1)[0] in timeSlotList():
if c.id not in gameCategoryDBList(guild):
for r in guild.roles:
if r.name == c.name:
break
if r.name != c.name:
break
colName = r.name.split(': ',maxsplit=1)[0]
db[colName].update_one({'role':r.id},{'$set':{'category':c.id}})
for p in c.overwrites:
if isinstance(p,discord.Member) and c.overwrites[p].manage_channels:
break
if p.id not in gmDBList(guild):
for r in guild.roles:
if r.name == c.name:
break
if r.name != c.name:
break
colName = r.name.split(': ',maxsplit=1)[0]
db[colName].update_one({'role':r.id},{'$set':{'gm':p.id}})
print(f'Synced database for server {guild.name}')
except:
pass
# Sync for Each Guild
@tasks.loop(hours=1.0)
async def syncGuilds():
try:
for guild in client.guilds:
syncGames(guild)
print('Synced database with server games list.')
except:
pass
@client.event
async def on_command_error(ctx,error):
if isinstance(error, commands.CommandNotFound):
await ctx.send(f'Invalid command. Please use `{p}help` to see a list of available commands.')
else:
await ctx.channel.send(error)
# On Ready
@client.event
async def on_ready():
await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f'{p} commands'))
print(f'Bot has logged in as {client.user.name} responding to prefix {p}')
print(f'Geas Server Bot version {os.getenv("BOT_VERSION")} by Vivek Santayana')
dbGetSettings()
syncGuilds.start()
# Check Bot Role is Defined
def botrole_is_defined():
async def predicate(ctx):
try:
return state[str(ctx.guild.id)]['settings']['botrole'] != None
except:
raise commands.CommandError(f'Bot role has not been defined. Please set the bot role using the `{p}definebotrole` command first.')
return commands.check(predicate)
# Check if invoked in valid game channel
def in_game_channel(ctx):
categoriesList = []
db = dbClient[str(ctx.guild.id)]
try:
try:
for c in db.list_collection_names():
ret = db[c].find({})
for e in ret:
if ctx.channel.category == ctx.guild.get_channel(e['category']):
return True
except:
if ctx.channel.category.name.split(': ',maxsplit=1)[0] in timeSlotList():
return True
except:
pass
if ctx.command.name == 'deletegame':
raise commands.CommandError('Error: you must invoke this command in a text channel corresponding to the game you are attempting to delete.')
raise commands.CommandError(f'Error: you must invoke this command in the text channel of your game.')
# setupGame command: <when>, <@GM> <capacity>, <Name of game>
@client.command(name='setupgame', aliases=['setup','gamesetup','creategame','gamecreate','create'], description='Use this command to set up the roles and channels for a new game. The syntax is `setup {wed|sunaft|suneve|oneshot|other} {@GM Name} {Capacity} {Name of game}`')
@commands.has_permissions(administrator=True)
@botrole_is_defined()
async def setupGame(ctx,arg1,arg2, arg3: int, *, arg4):
if not (arg2.startswith('<@') and not (arg2.startswith('<@&'))):
raise commands.CommandError('Invalid argument. The second parameter must @ the GM.')
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = gameTime(arg1.lower())
if colName == 'OTHER':
await ctx.channel.send('Time code not recognised. Game will be categorised as \'Other\'.')
await ctx.channel.trigger_typing()
gm = int(arg2.replace('<@', '').replace('>', '').replace('!', ''))
gmMember = await ctx.guild.fetch_member(gm)
cap = int(arg3)
gameTitle = f'{colName}: {arg4}'
roleExists = False
for r in ctx.guild.roles:
if r.name == gameTitle:
roleExists = True
break
if not roleExists:
r = await ctx.guild.create_role(name=gameTitle)
await r.edit(mentionable=True)
await gmMember.add_roles(r)
categoryExists = False
for c in ctx.guild.categories:
if c.name == gameTitle:
categoryExists = True
break
ret = state[dbName]['settings']
bots = ctx.guild.get_role(ret['botrole'])
permissions = {
ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False),
r: discord.PermissionOverwrite(read_messages=True),
bots: discord.PermissionOverwrite(read_messages=True),
gmMember: discord.PermissionOverwrite(read_messages=True, manage_messages=True, manage_channels=True, manage_permissions=True,priority_speaker=True,move_members=True,mute_members=True,deafen_members=True),
}
if not categoryExists:
c = await ctx.guild.create_category(name=gameTitle, overwrites=permissions)
await c.create_voice_channel(name=f'voice: {gameTitle}', topic=f'Default voice channel for {gameTitle}')
t = await c.create_text_channel(name=f'text: {gameTitle}', topic=f'Default text channel for {gameTitle}')
await ctx.channel.send(f'Game {r.mention} has been created with GM {gmMember.mention} and space for {cap} players.')
await t.send(f'Hello, {gmMember.mention}! Your game channels for {gameTitle} have now been set up.\nYou can also ping your players or interact with the bot commands by mentioning the {r.mention} role.')
else:
await c.edit(overwrites=permissions)
tPos = len(ctx.guild.channels)
tFirst = None
vExists = False
for t in c.text_channels:
if t.position <= tPos:
tFirst = t
tPos = t.position
await t.edit(sync_permissions=True)
for v in c.voice_channels:
await v.edit(sync_permissions=True)
vExists = True
await ctx.channel.send(f'The category for game {r.mention} has been reset for GM {gmMember.mention} with space for {cap} players.')
if tFirst == None:
tFirst = await c.create_text_channel(name=f'text: {gameTitle}', topic=f'Default text channel for {gameTitle}')
if not vExists:
await c.create_voice_channel(name=f'voice: {gameTitle}', topic=f'Default voice channel for {gameTitle}')
await tFirst.send(f'Hello, {gmMember.mention}! Your game channels for {gameTitle} have now been set up.\nYou can also ping your players or interact with the bot commands by mentioning the {r.mention} role.')
g = {
'game': arg4,
'gm': gm,
'capacity': cap,
'category': c.id,
'role': r.id
}
try:
db[colName].replace_one({'role':g['role']}, g , upsert=True)
except:
pass
@setupGame.error
async def clear_error(ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.channel.send('The number of players in the game must be an integer.')
raise error
# defineBotrole command
@client.command(name='definebotrole', aliases=['setbotrole','botrole','botroleset','botroledefine','br'], description='This is a set-up command to define which role is the botrole that the dice bot use. The syntax is `botrole {@Role}`')
@commands.has_permissions(administrator=True)
async def defineBotrole(ctx, arg):
if not arg.startswith('<@&'):
raise commands.CommandError('Invalid argument. The argument must @ a role.')
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = 'settings'
r = ctx.guild.get_role(int(arg[3:-1]))
if dbName not in state:
state[dbName] = {}
if 'settings' not in state[dbName]:
state[dbName]['settings'] = {}
ret = state[dbName]['settings']
ret['_id'] = 0
if 'botrole' not in ret:
await ctx.channel.send(f'Bot role for server {ctx.guild.name} not set. Setting {r.mention} to bot role now.')
else:
await ctx.channel.send(f'Bot role for server {ctx.guild.name} has already been set to {ctx.guild.get_role(ret["botrole"]).mention}. Updating to {r.mention}.')
ret['botrole'] = r.id
try:
db[colName].update_one({'_id':0},{'$set':{'botrole': r.id}}, upsert=True)
except:
pass
# deleteGame command
@client.command(name='deletegame', aliases=['delete','del', 'removegame', 'delgame', 'gamedel', 'gamedelete'], description='Use this command to delete the role and associated channels for a game. The syntax is `delete {@Game Role}`. **It must be called in a text channel inside the relevant game.**')
@commands.has_permissions(administrator=True)
@commands.check(in_game_channel)
async def deleteGame(ctx, arg):
if not arg.startswith('<@&'):
raise commands.CommandError('Invalid argument. The argument must @ a role.')
r = ctx.guild.get_role(int(arg[3:-1]))
cat = dbLookupRole(ctx.guild, r)
dbName = str(ctx.guild.id)
db = dbClient[dbName]
await ctx.channel.trigger_typing()
try:
colName = dbFindTimeslot(ctx.guild,r)
except:
raise commands.CommandError(f'Invalid argument. {r.mention} role is not a valid game role.')
for t in ctx.guild.text_channels:
if t.category == cat:
await t.delete()
for v in ctx.guild.voice_channels:
if v.category == cat:
await v.delete()
await cat.delete()
try:
db[colName].delete_one({'role':r.id})
except:
pass
await r.delete()
# reset command reset wed|sunaft|suneve|oneshot|all|other
@client.command(name='reset', aliases=['deleteall','delall','cleargames','clear'], description='This deletes all games in a particular time slot category. This is a very powerful command. Be careful when you use it. The syntax is `reset {wed|sunaft|suneve|oneshot|other|all}`')
@commands.has_permissions(administrator=True)
async def reset(ctx, *args):
delList = []
dbName = str(ctx.guild.id)
db = dbClient[dbName]
await ctx.channel.trigger_typing()
for a in args:
if a.lower() not in ['all', 'other', 'wed', 'sunaft', 'suneve', 'oneshot']:
raise commands.CommandError(f'Invalid argument. {a} is not a valid flag for the command.')
if a.lower() == 'all':
for l in ['wed', 'sunaft', 'suneve', 'oneshot','other']:
if l not in delList:
delList.append(l)
else:
if a.lower() not in delList:
delList.append(a.lower())
for d in delList:
colName = gameTime(d)
try:
cur = db[colName].find({})
for g in cur:
await ctx.guild.get_role(g['role']).delete()
cat = ctx.guild.get_channel(g['category'])
for c in cat.channels:
await c.delete()
await cat.delete()
db[colName].deleteMany({})
except:
for r in ctx.guild.roles:
if r.name.startswith(colName):
for cat in ctx.guild.categories:
if cat.name == r.name:
for c in cat.channels:
await c.delete()
await cat.delete()
await r.delete()
await ctx.channel.send(f'All games for {colName} have been deleted.')
# Migrate Guild from Old Server Settings to New Settings
@client.command(name='migrate', aliases=['migrategames','migratedata'], description='A set-up command to migrate games from the old server settings to the new server settings using the database for the first time.')
@commands.has_permissions(administrator=True)
async def migrateData(ctx):
await ctx.channel.trigger_typing()
dbName = str(ctx.guild.id)
db = dbClient[dbName]
gNum = 0
for r in ctx.guild.roles:
if r.name.split(': ',maxsplit=1)[0] in timeSlotList():
gNum += 1
await r.edit(mentionable=True)
gameName = r.name.split(': ',maxsplit=1)[1]
colName = r.name.split(': ',maxsplit=1)[0]
for c in ctx.guild.categories:
if c.name == r.name:
break
permissions = c.overwrites
for p in permissions:
if isinstance(p,discord.Member) and permissions[p].manage_channels:
break
g = {
'game': gameName,
'gm': p.id,
'capacity': 5,
'category': c.id,
'role': r.id
}
try:
db[colName].replace_one({'role':g['role']}, g , upsert=True)
except:
raise commands.CommandError('Error: Database connection failed. Could not migrate games to database.')
await ctx.channel.send(f'Finished migrating {gNum} games onto the database.')
# Import Cogs
for cogfile in os.listdir('./cogs'):
if cogfile.endswith('.py'):
client.load_extension(f'cogs.{cogfile[:-3]}')
# Run Bot
client.run(os.getenv('TEST_TOKEN'))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
discord
python-dotenv
wheel
pymongo
datetime

View File

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