Changed file structure.

Moved code to main bot and cog files.
This commit is contained in:
Vivek Santayana 2021-07-15 23:13:01 +01:00
parent b0b417a8d2
commit 1fa5029212
23 changed files with 139 additions and 1454 deletions

5
.gitignore vendored
View File

@ -143,8 +143,9 @@ cython_debug/
# Local Dev Env Configs
Scripts/
pyvenv.cfg
app/dev_cogs/template.py.tmp
app/dev_cogs/events.py.tmp
app/cogs/template.py.tmp
app/cogs/events.py.tmp
app/old_code/
# ---> VisualStudioCode
.vscode/*

View File

@ -1,17 +1,18 @@
# To Do
## Bot Architecture
[] Simplify directory tree
[X] Simplify directory tree
[X] Split event listeners into independent cogs.
## Bot Functionality
[] Delete Commands Function
[] Register Commands Function
[] 'Delete Commands' Function
[] 'Register Commands' Function
[] Infer Permissions from Config
[X] Dynamic Command Prefixes
[] Infer Games from Server Structure
[] Re-enable logging
[X] Delete Dev/Test Functions
[] Error handlers
[] Debug Features
## Event Listeners
@ -22,6 +23,7 @@
[X] Mod Channel Deleted
## Commands
[] Configure Bot function and sub commands
[] Migrate existing bot commands
## Misc

View File

@ -1,460 +1,136 @@
# Import Dependencies
import os
import configparser
import discord
from discord.ext import commands, tasks
import os, sys # OS Locations
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import yaml # Parser for yaml files for config settings.
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
configFile = './config.ini'
dataDir = './data'
## Define YAML functions
# Set Intents
intents = discord.Intents.all()
intents.typing = True
intents.presences = True
intents.members = True
caches = discord.MemberCacheFlags.all()
def yaml_load(filepath:str):
### Loads a YAML file
with open(filepath, 'r') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
return data
# Set Prefix
p = '¬'
def yaml_dump(data:dict, filepath:str):
### Dumps a YAML file
with open(filepath, 'w') as file:
yaml.dump(data, file)
# Define Global State Dictionary
state = {}
# Locate or create config file
configFile = './data/config.yml'
# Clients
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)
if not os.path.exists(configFile):
yaml_dump({},configFile)
# Define Game Times Dictionary
gameTimes = {
'wed': 'WED',
'sunaft': 'SUN AFT',
'suneve': 'SUN EVE',
'oneshot': 'ONE SHOT'
}
# Locate or create data file
dataFile = './data/data.yml'
# Reference Time Codes
def gameTime(arg):
return gameTimes.get(arg, 'OTHER')
if not os.path.exists(dataFile):
yaml_dump({},dataFile)
# List Time Codes
def timeSlotList():
l = []
for t in gameTimes:
l.append(gameTimes[t])
l.append('OTHER')
return l
# Locate Cogs Directory
cogsDir = 'cogs'
# 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]
# --> Temporary disable logging because of verboseness.
# ## Logging configuration imported boilerplate from Discord Py Docs
# logger = logging.getLogger('discord')
# logger.setLevel(logging.DEBUG)
# handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
# handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
# logger.addHandler(handler)
# 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.')
#### Dynamic Prefixes
def getPrefix(client, message):
conf = yaml_load(configFile)
return conf[str(message.guild.id)]['prefix']
# 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)
# Define Clients
client = commands.Bot(
intents=discord.Intents.all(),
command_prefix=getPrefix
)
slash = SlashCommand(
client,
sync_commands = True,
sync_on_cog_reload = True
)
# sync_on_reload is an important parameter that will become relevant when having to reload cogs on changing bot configs.
# 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
# Define Config keys
configKeys = ['adminroles', 'committeerole', 'botrole', 'modchannel', 'name', 'owner', 'prefix']
# 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
def setConfig(guild:discord.Guild):
#### Check if the bot is missing any config entries for the guilds it is in, and if it is then add it in.
#### N.B.: The way the commands work, the bot will have to list specific guilds in which it will synchronise the commands when it is defining them. So it needs to give a list of all the guilds it is part of when the bot loads, which it draws from the config files.
#### Because the bot connects to Discord after it loads, it will not be able to introspect and see what guilds it is part of before the commands are first loaded, and it will only add new guilds to the config files after it has already connected.
#### The Bot will first need to set up all of its configurations, and then begin loading all other commands once it is ready.
conf = yaml_load(configFile)
if str(guild.id) not in conf:
conf[str(guild.id)] = {}
if 'name' not in conf[str(guild.id)] or conf[str(guild.id)]['name'] != guild.name:
conf[str(guild.id)]['name'] = guild.name
if 'owner' not in conf[str(guild.id)] or conf[str(guild.id)]['owner'] != guild.owner_id:
conf[str(guild.id)]['owner'] = guild.owner_id
if 'adminroles' not in conf[str(guild.id)] or (type(conf[str(guild.id)]['adminroles']) is not list or len(conf[str(guild.id)]['adminroles']) == 0 or None in conf[str(guild.id)]['adminroles']):
conf[str(guild.id)]['adminroles'] = []
for role in guild.roles:
if not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator:
conf[str(guild.id)]['adminroles'].append(role.id)
if 'prefix' not in conf[str(guild.id)]:
conf[str(guild.id)]['prefix'] = '-'
if 'modchannel' not in conf[str(guild.id)]:
if guild.system_channel is None:
p = len(guild.channels)
c = None
for t in guild.text_channels:
if t.position < p:
p = t.position
conf[str(guild.id)]['modchannel'] = t.id
else:
conf[str(guild.id)]['modchannel'] = guild.system_channel.id
yaml_dump(conf, configFile)
# 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
def clearConfig(guildKey:str):
#### Delete Configs for Guilds that the Bot is no longer in
conf = yaml_load(configFile)
if discord.utils.find(lambda g: str(g.id) == guildKey, client.guilds) is None:
del conf[guildKey]
yaml_dump(conf, configFile)
# 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
def loadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.load_extension('.'.join(path))
# 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
def unloadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.unload_extension('.'.join(path))
# 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
def loadCogs(cogClass:str = 'all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == 'all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
loadCog(f'./{cogsDir}/{category}/{cogfile}')
@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)
def unloadCogs(cogClass:str = 'all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == 'all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
unloadCog(f'./{cogsDir}/{category}/{cogfile}')
# On Ready
@client.event
async def on_ready():
loadCogs('dev')
loadCogs('events')
loadCogs('botcommands')
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)
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'))
client.run(os.getenv('TEST_3_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

@ -2,7 +2,7 @@ import yaml # YAML parser for Bot config files
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
### Cog for handling the non-Slash prefix for native Bot commands.

View File

@ -7,10 +7,10 @@ from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
from dev import loadCog, unloadCog
from bot import loadCog, unloadCog
##### Debug Cog
class DevCog(commands.Cog):
##### Dev Cog
class Dev(commands.Cog):
def __init__(self, client):
self.client = client
@ -28,4 +28,4 @@ class DevCog(commands.Cog):
await ctx.reply(f'Debug commands disabled.')
def setup(client):
client.add_cog(DevCog(client))
client.add_cog(Dev(client))

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
#### Actions for the Bot to take on connecting to Discord.
class on_connect(commands.Cog):

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever a channel in a guild is deleted
class on_guild_channel_delete(commands.Cog):

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, setConfig, yaml_load, yaml_dump
from bot import configFile, setConfig, yaml_load, yaml_dump
#### Actions for the bot to take when the Bot joins a guild.

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import clearConfig, configFile, yaml_load, yaml_dump
from bot import clearConfig, configFile, yaml_load, yaml_dump
#### Actions for the bot to take when removed from a guild.

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever there is a new role created.

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever there is a new role deleted.

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever there is a new role deleted.

View File

@ -6,7 +6,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import configFile, yaml_load, yaml_dump
from bot import configFile, yaml_load, yaml_dump
##### Actions for the bot to take whenever the guild info or ownership are updated.
class on_guild_update(commands.Cog):

View File

@ -7,7 +7,7 @@ from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash C
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from dev import clearConfig, configFile, setConfig, yaml_dump, yaml_load
from bot import clearConfig, configFile, setConfig, yaml_dump, yaml_load
#### Actions for the Bot to take once it is ready to interact with commands.
class on_ready(commands.Cog):

View File

@ -7,7 +7,7 @@ from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
from dev import clearConfig, configFile, loadCog, loadCogs, setConfig, unloadCog, unloadCogs, yaml_dump, yaml_load
from bot import clearConfig, configFile, loadCog, loadCogs, setConfig, unloadCog, unloadCogs, yaml_dump, yaml_load
##### Debug Cog
class Debug(commands.Cog):

View File

@ -1,136 +0,0 @@
import os, sys # OS Locations
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import yaml # Parser for yaml files for config settings.
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
## Define YAML functions
def yaml_load(filepath:str):
### Loads a YAML file
with open(filepath, 'r') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
return data
def yaml_dump(data:dict, filepath:str):
### Dumps a YAML file
with open(filepath, 'w') as file:
yaml.dump(data, file)
# Locate or create config file
configFile = './data/config.yml'
if not os.path.exists(configFile):
yaml_dump({},configFile)
# Locate or create data file
dataFile = './data/data.yml'
if not os.path.exists(dataFile):
yaml_dump({},dataFile)
# Locate Cogs Directory
cogsDir = 'dev_cogs'
# --> Temporary disable logging because of verboseness.
# ## Logging configuration imported boilerplate from Discord Py Docs
# logger = logging.getLogger('discord')
# logger.setLevel(logging.DEBUG)
# handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
# handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
# logger.addHandler(handler)
#### Dynamic Prefixes
def getPrefix(client, message):
conf = yaml_load(configFile)
return conf[str(message.guild.id)]['prefix']
# Define Clients
client = commands.Bot(
intents=discord.Intents.all(),
command_prefix=getPrefix
)
slash = SlashCommand(
client,
sync_commands = True,
sync_on_cog_reload = True
)
# sync_on_reload is an important parameter that will become relevant when having to reload cogs on changing bot configs.
# Define Config keys
configKeys = ['adminroles', 'committeerole', 'botrole', 'modchannel', 'name', 'owner', 'prefix']
def setConfig(guild:discord.Guild):
#### Check if the bot is missing any config entries for the guilds it is in, and if it is then add it in.
#### N.B.: The way the commands work, the bot will have to list specific guilds in which it will synchronise the commands when it is defining them. So it needs to give a list of all the guilds it is part of when the bot loads, which it draws from the config files.
#### Because the bot connects to Discord after it loads, it will not be able to introspect and see what guilds it is part of before the commands are first loaded, and it will only add new guilds to the config files after it has already connected.
#### The Bot will first need to set up all of its configurations, and then begin loading all other commands once it is ready.
conf = yaml_load(configFile)
if str(guild.id) not in conf:
conf[str(guild.id)] = {}
if 'name' not in conf[str(guild.id)] or conf[str(guild.id)]['name'] != guild.name:
conf[str(guild.id)]['name'] = guild.name
if 'owner' not in conf[str(guild.id)] or conf[str(guild.id)]['owner'] != guild.owner_id:
conf[str(guild.id)]['owner'] = guild.owner_id
if 'adminroles' not in conf[str(guild.id)] or (type(conf[str(guild.id)]['adminroles']) is not list or len(conf[str(guild.id)]['adminroles']) == 0 or None in conf[str(guild.id)]['adminroles']):
conf[str(guild.id)]['adminroles'] = []
for role in guild.roles:
if not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator:
conf[str(guild.id)]['adminroles'].append(role.id)
if 'prefix' not in conf[str(guild.id)]:
conf[str(guild.id)]['prefix'] = '-'
if 'modchannel' not in conf[str(guild.id)]:
if guild.system_channel is None:
p = len(guild.channels)
c = None
for t in guild.text_channels:
if t.position < p:
p = t.position
conf[str(guild.id)]['modchannel'] = t.id
else:
conf[str(guild.id)]['modchannel'] = guild.system_channel.id
yaml_dump(conf, configFile)
def clearConfig(guildKey:str):
#### Delete Configs for Guilds that the Bot is no longer in
conf = yaml_load(configFile)
if discord.utils.find(lambda g: str(g.id) == guildKey, client.guilds) is None:
del conf[guildKey]
yaml_dump(conf, configFile)
def loadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.load_extension('.'.join(path))
def unloadCog(filepath:str):
path = os.path.normpath(filepath).split(os.path.sep)
if path[-1].endswith('.py'):
path[-1] = path[-1][:-3]
client.unload_extension('.'.join(path))
def loadCogs(cogClass:str = 'all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == 'all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
loadCog(f'./{cogsDir}/{category}/{cogfile}')
def unloadCogs(cogClass:str = 'all'):
for category in os.listdir(f'./{cogsDir}'):
if cogClass == 'all' or cogClass == category:
for cogfile in os.listdir(f'./{cogsDir}/{category}'):
if cogfile.endswith('.py'):
unloadCog(f'./{cogsDir}/{category}/{cogfile}')
loadCogs('dev')
loadCogs('events')
loadCogs('botcommands')
client.run(os.getenv('TEST_3_TOKEN'))

View File

@ -1,189 +0,0 @@
import os # OS Locations
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import configparser # Config ini parser for Bot config files
import json # Json Library to manage json Data files
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
# Locate configuration file
configFile = './data/config.ini'
# Create empty list of current guilds, to be populated Bot load
guild_ids = []
# Config keys
configKeys = ['botrole', 'committeerole', 'modchannel', 'prefix']
# Bot Prefix Function for Regular String Prefix (non-Slash commands)
def getPrefix(client,message):
conf = configparser.ConfigParser()
conf.read(configFile)
if message.guild.id in conf.sections():
if 'prefix' in conf[message.guild.id]:
return conf[message.guild.id]['prefix']
pass
# Define bot client and initialise Slash Command client
client = commands.Bot(command_prefix= '¬', intents=discord.Intents.all()) # Regular client set up, but I did not know you could pass a function as a prefix!
slash = SlashCommand(client, sync_commands=True) # Enable Slash Command Functionality
# Starting Bot Initialisation
@client.event
async def on_guild_join(guild):
pass
# @slash.slash(
# name='configure',
# description='Configuration command to set up the various parameters for the bot on the server.'
# guild_ids=guild_ids,
# options = [
# create_option(
# name='parameter1',
# description='Please select the first parameter you would like to configure.',
# type=3,
# required=True
# )
# ]
# )
# On Ready
@client.event
async def on_ready():
print('Bot Ready')
for guild in client.guilds:
guild_ids.append(guild.id)
channel = discord.utils.get(guild.text_channels,position=1)
print(channel)
print(guild_ids)
conf = configparser.ConfigParser()
conf.read(configFile)
undef = []
if guild.id in conf.sections(): # Check if there are configs for the guild, and check if configs are complete
for i in configKeys:
if i not in conf[guild.id]:
undef.append(i)
if len(undef) == 0: # If none of the key values are undefined, ignore it.
channel = discord.utils.get(guild.text_channels,id=conf[guild.id]['modchannel'])
output = f'`{client.user.display_name}` has already been configured for the guild `{guild.name}`. \n'
output = ''.join([output, f'The `botrole` for the guild `{guild.name}` is {discord.utils.get(guild.roles,id=conf[guild.id]["botrole"]).mention}\n'])
output = ''.join([output, f'The `committeerole` for the guild `{guild.name}` is {discord.utils.get(guild.roles,id=conf[guild.id]["committeerole"]).mention}\n'])
output = ''.join([output, f'The `modchannel` for the guild `{guild.name}` is {channel.mention}\n'])
output = ''.join([output, f'The `prefix` for the guild `{guild.name}` is `{conf[guild.id]["prefix"]}`'])
await channel.send(output)
break
if len(undef) == 0:
undef = configKeys.copy()
output = f'`{client.user.display_name}` has not been configured for the guild `{guild.name}`. Please define:\n'
for u in undef:
output = ''.join([output, f'`{u}`\n'])
output = ''.join([output, f'using the `/configure` command.'])
await channel.send(output)
@slash.slash(
name='hello',
description='Hello World command',
guild_ids=guild_ids,
options = [
create_option(
name='option',
description='choose your word',
required=True,
option_type=3
)
]
)
async def _hello(ctx:SlashContext, option:str):
await ctx.send(option)
@slash.slash(
name='mypfp',
description='Displays profile picture',
guild_ids=guild_ids
)
async def pfp(ctx):
embed = discord.Embed(
title=f'Avatar of {ctx.author.display_name}',
color=discord.Color.teal()
).set_image(url=ctx.author.avatar_url)
await ctx.send(embed=embed)
class Cogs(commands.Cog):
def __init__(self, client):
self.client = client
@commands.command(description='Sends Pong')
async def ping(self, ctx):
await ctx.send('pong')
@cog_ext.cog_slash(name='Ping', description='Sends Pong')
async def ping(self, ctx):
await ctx.send('pong')
@client.command(name='foo')
async def foo(ctx):
f = await utils.manage_commands.get_all_commands(client.user.id,os.getenv('TEST_3_TOKEN'),guild_id=ctx.guild.id)
print(f)
def setup(bot):
client.add_cog(Cogs(client))
###### Configuration Cog
# class Configuration(commands.Cog):
# def __init__(self, client):
# self.client = client
# @cog_ext.cog_slash(
# # base='botrole',
# # subcommand_group='configure',
# name='configure',
# description='Parameter to define the role that is assigned to the dice bots in this guild so they can access in-game text channels.',
# # base_description='Command to configure the various guild parameters.',
# # subcommand_group_description='These are configuration commands to set up the various guild parameters.',
# guild_ids=guild_ids
# # options=[
# # create_option(
# # name='botrole',
# # description='The role that the dice bots are assigned in order to access the text channels.'
# # type=8,
# # required=True
# # )
# # ]
# )
# async def _configure(self, ctx:SlashContext, option):
# await ctx.send(f'The `botrole` for the guild `{ctx.guild.name}` has been set to `{option.mention}`.')
# def setup(client):
# client.add_cog(Configuration(client))
client.run(os.getenv('TEST_3_TOKEN'))
# guilds = [
# 'guild1',
# 'guild2',
# 'guild3'
# ]
# configFile = './config.ini'
# config = configparser.RawConfigParser()
# for g in guilds:
# config.add_section(g)
# config.set(g,'botrole',f'<Bot Role> for {g}')
# config.set(g,'committeerole',f'<Committee Role> for {g}')
# with open(configFile,'w') as c:
# config.write(c)
# parser = configparser.ConfigParser()
# parser.read(configFile)
# print(parser.sections())
# print(parser['guild1']['botrole'])