forked from viveksantayana/geas-bot
		
	v2.1.1 of the bot, which is currently in use on the Geas server.
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,5 @@ | |||||||
|  | db/* | ||||||
|  |  | ||||||
| # ---> Python | # ---> Python | ||||||
| # Byte-compiled / optimized / DLL files | # Byte-compiled / optimized / DLL files | ||||||
| __pycache__/ | __pycache__/ | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,3 +1,68 @@ | |||||||
| # geas-bot | # Geas Server Bot | ||||||
|  |  | ||||||
| Bot for managing the Discord server for Geas, the Edinburgh table-top role-playing society. | 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 only thing that remains is for the correct API keys 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.**: 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. | ||||||
							
								
								
									
										6
									
								
								app/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | 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 | ||||||
							
								
								
									
										466
									
								
								app/bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										466
									
								
								app/bot.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,466 @@ | |||||||
|  | # 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.') | ||||||
|  |  | ||||||
|  | # Easter egg for Alan | ||||||
|  | @client.command() | ||||||
|  | async def alan(ctx): | ||||||
|  |     if ctx.author.id != 355857207205691392: | ||||||
|  |         alan = await ctx.guild.fetch_member(355857207205691392) | ||||||
|  |         raise commands.CommandError(f'Error: This is a powerful, super secret master command that only {alan.mention} can use. Do **NOT** invoke this if you are not Alan. ~~It is a command that can do great evil in the wrong hands.~~') | ||||||
|  |     await ctx.send(f'Hi {alan.mention}. In order to execute this script, you must click the following link:\n<http://vsnt.uk/alancommand>\nUse it wisely.') | ||||||
|  |  | ||||||
|  | # 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')) | ||||||
							
								
								
									
										177
									
								
								app/cogs/GameManagement.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								app/cogs/GameManagement.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | 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)) | ||||||
							
								
								
									
										48
									
								
								app/cogs/HelpNotifier.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/cogs/HelpNotifier.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | 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)) | ||||||
							
								
								
									
										112
									
								
								app/cogs/MembershipRestriction.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/cogs/MembershipRestriction.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | 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)) | ||||||
							
								
								
									
										142
									
								
								app/cogs/MembershipVerification.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								app/cogs/MembershipVerification.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | 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)) | ||||||
							
								
								
									
										190
									
								
								app/cogs/PitchMenu.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								app/cogs/PitchMenu.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | |||||||
|  | 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)) | ||||||
							
								
								
									
										5
									
								
								app/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | discord | ||||||
|  | python-dotenv | ||||||
|  | wheel | ||||||
|  | pymongo | ||||||
|  | datetime | ||||||
							
								
								
									
										29
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | 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} | ||||||
		Reference in New Issue
	
	Block a user