Debugged membership sign-ups and pitch menu.

Ready for more rigorous testing.
This commit is contained in:
Vivek Santayana 2021-07-24 17:58:23 +01:00
parent 173aeb2a3c
commit e30e89e7e3
13 changed files with 356 additions and 245 deletions

View File

@ -31,6 +31,7 @@ DATA=(Path to data file. The bot defaults to './data/data.yml' if not provided.)
LOOKUP=(Path to the game role lookup file. The bot defaults to './data/lookup.yml' if not provided.)
GM=(Path to the GM lookup file. The bot defaults to './data/gm.yml' if not provided.)
CATEGORIES=(Path to the channel category lookup file. The bot defaults to './data/categories.yml' if not provided.)
PITCHES=(Path to the pitches data file. The bot defaults to './data/pitches.yml' if not provided.)
BOT_VERSION=(verson string)
BOT_MAINTAINER_ID=(Discord user ID of the person maintaining the bot to enable debug features.)
```
@ -74,7 +75,9 @@ in order for to authenticate as the correct bot.
| | | |-- on_guild_role_update.py
| | | |-- on_guild_update.py
| | | |-- on_message.py
| | | `-- on_ready.py
| | | |-- on_ready.py
| | | `-- secondary
| | | `-- pitch_listener.py
| | |-- membership
| | | |-- membership_verification.py
| | | `-- restriction_listener.py
@ -194,8 +197,10 @@ There is currently no way of having an exception for the Bot's edits.
To reconcile this, the bot would need to work such that the command process that modified games only acted upon the roles, which would then trigger the event listeners to synchronise these changes with the categories, and subsequently the data.
Having the bot edit the data in the main command process would mean that there would be conflicts with the simuntaneous execution of parallel threads.
This works for individual commands, but it breaks down when trying to use the `purge` command because of conflicts causedb by simultaneous changes to the data files.
Programming around this will need a further layer of complexity, involving flags checking for R/W operations and a time-out.
### Membership sign up performance issues
The way the membership signup prompt works is that it creates a new instance of the process executing for each member who submits a verification request, and the command runs until the verification is complete (either by verifying it or rejecting it).
This means that there is a risk that several active instances of the command will run simultaneously if a lot of members submit membership confirmation at once.
This should probably also be changed to being a global event listener, with the requisite inforation being passed to the function in the event listener via the custom values of the buttons and drop-down menu options.
I have set the member verification prompt to use a global listener to avoid a situation where it creates several backlogged processes when multiple people post sign-ups at the same time.
This should also mean that the sign-up prompts should persist over reboots.

View File

@ -27,34 +27,34 @@ def yaml_dump(data:dict, filepath:str):
# Locate or create config file. Read from environment variables to locate file, and if missing or not valid then use default location.
configFile = os.getenv('CONFIG') if ((os.getenv('CONFIG').endswith('.yml') or os.getenv('CONFIG').endswith('.yaml')) and not os.getenv('CONFIG').endswith('config_blueprint.yml')) else './data/config.yml'
if not os.path.exists(configFile):
yaml_dump({},configFile)
if not os.path.exists(configFile): yaml_dump({},configFile)
# Locate or create data file. Same as above.
dataFile = os.getenv('DATA') if ((os.getenv('DATA').endswith('.yml') or os.getenv('DATA').endswith('.yaml')) and not os.getenv('DATA').endswith('data_blueprint.yml')) else './data/data.yml'
if not os.path.exists(dataFile):
yaml_dump({},dataFile)
if not os.path.exists(dataFile): yaml_dump({},dataFile)
# Locate or create lookup file. Same as above.
lookupFile = os.getenv('LOOKUP') if os.getenv('LOOKUP').endswith('.yml') or os.getenv('LOOKUP').endswith('.yaml') else './data/lookup.yml'
if not os.path.exists(lookupFile):
yaml_dump({},lookupFile)
if not os.path.exists(lookupFile): yaml_dump({},lookupFile)
# Locate or create GM lookup file. Same as above.
gmFile = os.getenv('GM') if os.getenv('GM').endswith('.yml') or os.getenv('GM').endswith('.yaml') else './data/gm.yml'
if not os.path.exists(gmFile):
yaml_dump({},gmFile)
if not os.path.exists(gmFile): yaml_dump({},gmFile)
# Locate or create Categories lookup file. Same as above.
categoriesFile = os.getenv('CATEGORIES') if os.getenv('CATEGORIES').endswith('.yml') or os.getenv('CATEGORIES').endswith('.yaml') else './data/categories.yml'
if not os.path.exists(categoriesFile):
yaml_dump({},categoriesFile)
if not os.path.exists(categoriesFile): yaml_dump({},categoriesFile)
l = [dataFile, lookupFile, gmFile, categoriesFile]
# Locate or create Pitches data file. Same as above.
pitchesFile = os.getenv('PITCHES') if os.getenv('PITCHES').endswith('.yml') or os.getenv('PITCHES').endswith('.yaml') else './data/pitches.yml'
if not os.path.exists(pitchesFile): yaml_dump({},pitchesFile)
l = [dataFile, lookupFile, gmFile, categoriesFile, configFile, pitchesFile]
if len(set(l)) != len(l): raise Exception('Config Error: there is a clash between two file names.')
# Locate Cogs Directory
@ -267,6 +267,7 @@ def reloadCogs(cogClass:str = '--all'):
loadCogs('controlcommands')
loadCogs('events')
loadCogs('membership')
loadCogs('botcommands')
loadCogs('slashcommands')
if yaml_load(configFile):
@ -279,6 +280,8 @@ if yaml_load(configFile):
loadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
loadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
loadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
if yaml_load(pitchesFile):
loadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
if any([len(yaml_load(configFile)[x]['membership']) > 0 for x in yaml_load(configFile)]):
loadCog(f'./{cogsDir}/slashcommands/secondary/edit_membership.py')

View File

@ -1,36 +0,0 @@
import yaml # YAML parser for Bot config files
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
# logger and handler
from bot import configFile, yaml_load, yaml_dump
#### Error Handler Event Listener for Slash Command Errors
class on_slash_command_error(commands.Cog, name='On Command Error'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_slash_command_error(self, ctx:SlashContext, error):
if isinstance(error, Exception):
await ctx.send(
content='```Invalid Command: {error}```',
tts=True,
hidden=True,
delete_after=10,
)
# if isinstance(error, commands.CommandNotFound):
# print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} entered an invalid command <{ctx.message.clean_content}> in the guild {ctx.guild.name}.')
# await ctx.reply(f'```Error: This is not a valid command.```')
# elif isinstance(error, commands.CheckFailure):
# print(f'Error: User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} is not authorised to issue the command <{ctx.command.name}> in the guild {ctx.guild.name}.')
# await ctx.reply(f'```Error: You are not authorised to issue this command.```')
# else:
# print(f'User {ctx.author.name}#{ctx.author.discriminator} / {ctx.author.display_name} received error: "{error}" when attempting to issue command <{ctx.command.name}> in the guild {ctx.guild.name}.')
# await ctx.reply(f'```Error: {error}```')
def setup(client):
client.add_cog(on_slash_command_error(client))

View File

@ -0,0 +1,163 @@
import os # OS Locations
import yaml # YAML parser for Bot config files
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils, ComponentContext # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option, create_permission # Slash Command features
from discord_slash.model import SlashCommandPermissionType, ButtonStyle
from discord_slash.client import SlashCommand
from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component, create_button, create_actionrow
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile, pitchesFile, configFile, dataFile, lookupFile, unloadCog
#### Pitch Command
class PitchListener(commands.Cog, name='Pitch Listener'):
def __init__(self, client):
self.client = client
@commands.Cog.listener(name='on_component')
async def _pitch_listener(self, ctx:ComponentContext):
conf = yaml_load(configFile)
data = yaml_load(dataFile)
lookup = yaml_load(lookupFile)
pitches = yaml_load(pitchesFile)
guildStr = str(ctx.guild.id)
if not pitches.get(guildStr, {}): return # If no pitches for current guild, ignore.
[timeslot] = [*pitches[guildStr]]
if ctx.origin_message.id not in pitches[guildStr][timeslot]['messages'] + [pitches[guildStr][timeslot]['control']]: return # If the context id is not in the pitch menu, ignore
newcomer = returning_player = None
if 'newcomer' in conf[guildStr]['roles']: newcomer = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['newcomer'], ctx.guild.roles)
if 'returning_player' in conf[guildStr]['roles']: returning_player = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['returning_player'], ctx.guild.roles)
control = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['control'])
header_message = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['header_message'])
if ctx.origin_message.id == control.id:
if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[str(ctx.guild.id)]['roles']['admin']]) or ctx.author == ctx.guild.owner):
await ctx.send(f'```Error: You are not authorised to do this. The control panel may only be issued by an administrator.```',hidden=True)
else:
if ctx.custom_id == 'allow_returning':
await ctx.channel.set_permissions(reason=f'/pitch control switch triggered by {ctx.author.display_name}', target=returning_player, read_messages=True)
await ctx.send(f'```Returning Players have now been allowed access to the pitch menu.```', hidden=True)
if ctx.custom_id == 'allow_newcomers':
await ctx.channel.set_permissions(reason=f'/pitch control switch triggered by {ctx.author.display_name}', target=newcomer, read_messages=True)
await ctx.send(f'```Newcomers have now been allowed access to the pitch menu.```', hidden=True)
if ctx.custom_id == 'allow_all':
await ctx.channel.set_permissions(reason=f'/pitch control switch triggered by {ctx.author.display_name}', target=ctx.guild.default_role, read_messages= True, send_messages=False)
await ctx.send(f'```All members have now been allowed access to the pitch menu.```', hidden=True)
if ctx.custom_id == 'close_pitches':
await ctx.send(f'```Please wait: closing pitches.```', hidden=True)
await header_message.delete()
for message in pitches[guildStr][timeslot]['messages']:
m = await ctx.channel.fetch_message(message)
await m.delete()
await control.delete()
await ctx.channel.edit(reason=f'/pitch command issued by {ctx.author.display_name}', overwrites={})
await ctx.channel.send('```Pitch menu cleared. Pitches have now concluded.```')
del pitches[guildStr][timeslot]
if not pitches[guildStr]: del pitches[guildStr]
yaml_dump(pitches,pitchesFile)
if not pitches and self.client.get_cog('Pitch Listener') is not None:
unloadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
#### Deactivate global pitch listener
else:
index = int(ctx.custom_id.split('_',1)[1])
if ctx.custom_id.startswith('join_'):
if set([x.id for x in ctx.author.roles]) & set(pitches[guildStr][timeslot]['roles'].values()):
for r in list(set([x.id for x in ctx.author.roles]) & set(pitches[guildStr][timeslot]['roles'].values())):
role = ctx.guild.get_role(r)
if role.id != pitches[guildStr][timeslot]['roles'][index]:
await ctx.author.remove_roles(role,reason=f'/pitch interaction by {ctx.author.display_name}')
i = pitches[guildStr][timeslot]['indices'][role.id]
element = pitches[guildStr][timeslot]['entries'][i]
gm = await self.client.fetch_user(element['gm'])
data[guildStr][timeslot][str(role.id)]['current_players'] -= 1
element['current_players'] -= 1
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
m = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['messages'][i])
await m.edit(content=o)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{ctx.author.display_name} has left the game.```')
role = ctx.guild.get_role(pitches[guildStr][timeslot]['roles'][index])
if role in ctx.author.roles:
await ctx.send(f'```Error: You are already in the game `{lookup[guildStr][str(role.id)]["game_title"]}`.```', hidden=True)
else:
await ctx.author.add_roles(role,reason=f'/pitch interaction by {ctx.author.display_name}')
element = pitches[guildStr][timeslot]['entries'][index]
data[guildStr][timeslot][str(role.id)]['current_players'] += 1
element['current_players'] += 1
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
m = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['messages'][index])
await m.edit(content=o)
await ctx.send(f'You have joined the game `{lookup[guildStr][str(role.id)]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{ctx.author.display_name} has joined the game.```')
elif ctx.custom_id.startswith('leave_'):
role = ctx.guild.get_role(pitches[guildStr][timeslot]['roles'][index])
if role not in ctx.author.roles:
await ctx.send(f'```Error: You are not in the game `{lookup[guildStr][str(role.id)]["game_title"]}`.```', hidden=True)
else:
await ctx.author.remove_roles(role,reason=f'/pitch interaction by {ctx.author.display_name}')
data[guildStr][timeslot][str(role.id)]['current_players'] -= 1
element = pitches[guildStr][timeslot]['entries'][index]
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
me = await ctx.channel.fetch_message(pitches[guildStr][timeslot]['messages'][index])
await me.edit(content=o)
await ctx.send(f'You have left the game `{lookup[guildStr][str(role.id)]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{ctx.author.display_name} has left the game.```')
yaml_dump(data, dataFile)
yaml_dump(pitches, pitchesFile)
def setup(client):
client.add_cog(PitchListener(client))

View File

@ -3,8 +3,9 @@ import yaml # YAML parser for Bot config files
import asyncio # Discord Py Dependency
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component, create_button, create_actionrow, create_choice, create_option
from discord_slash import SlashCommand, SlashContext, cog_ext, utils, ComponentContext # Slash Command Library
from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component, create_button, create_actionrow
from discord_slash.utils.manage_commands import create_choice, create_option
from discord_slash.model import ButtonStyle
import logging
# logger and handler
@ -16,16 +17,17 @@ class MemberVerification(commands.Cog, name='Member Verification Cog'):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_message(self, message):
@commands.Cog.listener(name='on_message')
async def _submission_listener(self, message):
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(message.guild.id)
lookup = yaml_load(lookupFile)
if conf[guildStr]['channels'].get('signup', None) is not None: return
if conf[guildStr]['channels'].get('signup', None) is None: return
if message.author.bot: return
if message.channel.id != conf[guildStr]['channels']['signup']: return
if not message.attachments:
await message.author.send(f'```Error: The message you posted in the `{message.channel.name}` channel of the guild `{message.guild.name}` was invalid. Your post must contain a screensot of your proof of purchase for membership.```')
if not (message.attachments):
await message.channel.send(f'```Error: The message you posted in the `{message.channel.name}` channel of the guild `{message.guild.name}` was invalid. Your post must contain a screensot of your proof of purchase for membership.```')
await message.delete()
return
membership = [discord.utils.get(message.guild.roles, id=x) for x in conf[guildStr]['membership']]
@ -35,9 +37,9 @@ class MemberVerification(commands.Cog, name='Member Verification Cog'):
admin_buttons.append(create_button(style=ButtonStyle.grey, label='Alert', emoji='⚠️', custom_id=f'alert_{message.id}'))
admin_buttons.append(create_button(style=ButtonStyle.red, label='Deny', emoji='✖️', custom_id=f'deny_{message.id}'))
admin_buttons.append(create_button(style=ButtonStyle.green, label='Done', emoji='▶️', custom_id=f'done_{message.id}'))
o = f'```For Administrators: Please verify the membership request submitted.```\n'
o = f'```For Administrators: Please verify the membership request submitted by `{message.author.display_name}`.```'
admins = '|'.join([discord.utils.get(message.guild.roles, id=x).mention for x in conf[guildStr]['roles']['admin']])
o = '\n'.join((o,admins))
o = ''.join((admins,o))
m = await message.reply(
content= o,
components=[
@ -55,55 +57,70 @@ class MemberVerification(commands.Cog, name='Member Verification Cog'):
)
]
)
while True:
interaction_ctx = await wait_for_component(self.client, messages=m)
if not (set(interaction_ctx.author.roles) & set([interaction_ctx.guild.get_role(x) for x in conf[str(interaction_ctx.guild.id)]['roles']['admin']]) or interaction_ctx.author == interaction_ctx.guild.owner):
await interaction_ctx.send(f'```Error: You are not authorised to assign memberships for guild `{interaction_ctx.guild.name}`. Only administrators may assign memberships using this interface.```', hidden=True)
else:
submission = await interaction_ctx.channel.fetch_message(int(interaction_ctx.custom_id.split('_',1)[1]))
if interaction_ctx.custom_id.startswith('done_'):
await interaction_ctx.send(f'```Membership verification complete.```', hidden=True)
break
elif interaction_ctx.custom_id.startswith('deny_'):
await interaction_ctx.send(f'```Membership verification denied.```', hidden=True)
embed = discord.Embed(
title = submission.author.name,
description = f'[Jup to Message]({submission.jump_url})',
colour = discord.Colour.red(),
)
await submission.author.send(f'```Your membership for guild `{submission.guild.name}` could not be verified. Please make sure your name and the kind of membership that you have bought are visible in the screenshot you upload. Please contact a Committee member if you have any difficulties.```')
if conf[guildStr]['channels'].get('mod', None) is not None:
await submission.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```Verifying the membership of {submission.author.display_name} failed.```\n{admins}', embed=embed)
break
elif interaction_ctx.custom_id.startswith('alert_'):
await interaction_ctx.send(f'```Membership verification alert raised.```', hidden=True)
embed = discord.Embed(
title = submission.author.name,
description = f'[Jup to Message]({submission.jump_url})',
colour = discord.Colour.orange()
)
await submission.author.send(f'```Your membership for guild `{submission.guild.name}` needs to be reviewed by a Committee member.```')
if conf[guildStr]['channels'].get('mod', None) is not None:
await submission.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```There is a problem verifying the membership of {submission.author.display_name}.\nCould someone verify this person\'s membership manually via the EUSA portal?.```\n{admins}', embed=embed)
elif interaction_ctx.custom_id.startswith('student_'):
await interaction_ctx.send(f'````Student` role granted.```', hidden=True)
student_role = submission.guild.get_role(conf[guildStr]['roles']['student'])
await submission.author.add_roles(student_role, reason=f'Membership Verification: Student role assigned by `{interaction_ctx.author.display_name}`.')
await submission.author.send(f'```You have additionally been assigned the role `Student` in the guild `{submission.guild.name}`.```')
elif interaction_ctx.custom_id.startswith('membership_'):
[selected_membership] = interaction_ctx.selected_options
selected_role = interaction_ctx.guild.get_role(int(selected_membership))
if selected_role not in submission.author.roles:
await interaction_ctx.send(f'```Membership `{selected_role.name}` added to member `{submission.author.display_name}`.```', hidden=True)
await submission.author.add_roles(selected_role, reason=f'Membership Verification: Membership verified by `{interaction_ctx.author.display_name}`.')
await submission.author.send(f'```Your membership for guild `{submission.guild.name}` has been verified and you have been assigned the role `{selected_role.name}`.```')
else:
await interaction_ctx.send(f'```Membership `{selected_role.name}` removed from member `{submission.author.display_name}`.```', hidden=True)
await submission.author.remove_roles(selected_role, reason=f'Membership Verification: Membership removed by `{interaction_ctx.author.display_name}`.')
await submission.author.send(f'```Your role `{selected_role.name}` has been removed in the guild `{submission.guild.name}`.```')
if conf[guildStr]['notifications'].get('signup', False):
embed = discord.Embed(
title = f'Member Verification Request',
description = f'User: {message.author.name}\n\n[Jup to Message]({m.jump_url})',
colour = discord.Colour.blue(),
)
if conf[guildStr]['channels'].get('mod', None) is not None:
await message.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```New membership verification request.```\n{admins}', embed=embed)
@commands.Cog.listener(name='on_component')
async def _verification_response(self, ctx:ComponentContext):
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
admins = '|'.join([discord.utils.get(ctx.guild.roles, id=x).mention for x in conf[guildStr]['roles']['admin']])
lookup = yaml_load(lookupFile)
if ctx.channel.id != conf[guildStr]['channels']['signup']: return
if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[str(ctx.guild.id)]['roles']['admin']]) or ctx.author == ctx.guild.owner):
await ctx.send(f'```Error: You are not authorised to assign memberships for guild `{ctx.guild.name}`. Only administrators may assign memberships using this interface.```', hidden=True)
else:
submission = await ctx.channel.fetch_message(int(ctx.custom_id.split('_',1)[1]))
if ctx.custom_id.startswith('done_'):
await ctx.send(f'```Membership verification complete.```', hidden=True)
await ctx.origin_message.delete()
elif ctx.custom_id.startswith('deny_'):
await ctx.send(f'```Membership verification denied.```', hidden=True)
embed = discord.Embed(
title = submission.author.name,
description = f'[Jup to Message]({submission.jump_url})',
colour = discord.Colour.red(),
)
await submission.channel.send(f'```Your membership for guild `{submission.guild.name}` could not be verified. Please make sure your name and the kind of membership that you have bought are visible in the screenshot you upload. Please contact a Committee member if you have any difficulties.```')
if conf[guildStr]['channels'].get('mod', None) is not None:
await submission.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```Verifying the membership of {submission.author.display_name} failed.```\n{admins}', embed=embed)
await ctx.origin_message.delete()
elif ctx.custom_id.startswith('alert_'):
await ctx.send(f'```Membership verification alert raised.```', hidden=True)
embed = discord.Embed(
title = submission.author.name,
description = f'[Jup to Message]({submission.jump_url})',
colour = discord.Colour.orange()
)
await submission.channel.send(f'```Your membership for guild `{submission.guild.name}` needs to be reviewed by a Committee member.```')
if conf[guildStr]['channels'].get('mod', None) is not None:
await submission.guild.get_channel(conf[guildStr]['channels']['mod']).send(f'```There is a problem verifying the membership of {submission.author.display_name}.\nCould someone verify this person\'s membership manually via the EUSA portal?.```\n{admins}', embed=embed)
elif ctx.custom_id.startswith('student_'):
await ctx.send(f'````Student` role granted.```', hidden=True)
student_role = submission.guild.get_role(conf[guildStr]['roles']['student'])
await submission.author.add_roles(student_role, reason=f'Membership Verification: Student role assigned by `{ctx.author.display_name}`.')
await submission.channel.send(f'```You have additionally been assigned the role `Student` in the guild `{submission.guild.name}`.```')
elif ctx.custom_id.startswith('membership_'):
[selected_membership] = ctx.selected_options
selected_role = ctx.guild.get_role(int(selected_membership))
if selected_role not in submission.author.roles:
await ctx.send(f'```Membership `{selected_role.name}` added to member `{submission.author.display_name}`.```', hidden=True)
await submission.author.add_roles(selected_role, reason=f'Membership Verification: Membership verified by `{ctx.author.display_name}`.')
await submission.channel.send(f'```Your membership for guild `{submission.guild.name}` has been verified and you have been assigned the role `{selected_role.name}`.```')
else:
pass
await m.delete()
await ctx.send(f'```Membership `{selected_role.name}` removed from member `{submission.author.display_name}`.```', hidden=True)
await submission.author.remove_roles(selected_role, reason=f'Membership Verification: Membership removed by `{ctx.author.display_name}`.')
await submission.channel.send(f'```Your role `{selected_role.name}` has been removed in the guild `{submission.guild.name}`.```')
else:
pass
def setup(client):
client.add_cog(MemberVerification(client))

View File

@ -15,15 +15,15 @@ class RestrictionListener(commands.Cog, name='Membership Restriction Listener'):
self.client = client
# Block non-verified user from posting messages.
@commands.Cog.listener()
async def on_message(self,message):
@commands.Cog.listener(name='on_message')
async def _restriction_listener(self,message):
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(message.guild.id)
lookup = yaml_load(lookupFile)
if conf[guildStr].get('restrict',False): return
if not conf[guildStr].get('restrict',False): return
if message.author.bot: return
if str(message.channel.category) not in categories[guildStr]: return
if str(message.channel.category) in categories[guildStr]: return
if (set(message.author.roles) & set([message.guild.get_role(x) for x in conf[guildStr]['roles']['admin']]) or message.author == message.guild.owner): return
if set(message.author.roles) & set([message.guild.get_role(x) for x in conf[guildStr]['membership']]): return
if message.channel.overwrites_for(message.author).manage_channels: return
@ -33,18 +33,21 @@ class RestrictionListener(commands.Cog, name='Membership Restriction Listener'):
await message.delete()
# Reinstate on verification
@commands.Cog.listener()
async def on_member_update(self, before, after):
@commands.Cog.listener(name='on_member_update')
async def _reinstate_listener(self, before, after):
if before.roles == after.roles: return
if len(set(after.roles) - set(before.roles)) != 1: return
[d] = list(set(after.roles) - set(before.roles))
conf = yaml_load(configFile)
categories = yaml_load(categoriesFile)
guildStr = str(after.guild.id)
if d.id not in conf[guildStr]['membership']: return
lookup = yaml_load(lookupFile)
if not set(after.author.roles) & set([after.guild.get_role(x) for x in conf[guildStr]['membership']]): return
for game in list(set(after.author.roles) & set([after.guild.get_role(int(x)) for x in lookup[guildStr]])):
c = discord.utils.get(lambda x: x.id == lookup[guildStr][str(game.id)]['category'])
if not set(after.roles) & set([after.guild.get_role(x) for x in conf[guildStr]['membership']]): return
for game in list(set(after.roles) & set([after.guild.get_role(int(x)) for x in lookup[guildStr]])):
c = discord.utils.get(after.guild.categories, id=lookup[guildStr][str(game.id)]['category'])
if c is not None:
if c.overwrites_for(after).send_messages is False: await c.set_permissions(after, overwrite = False, reason= f'Membership Restriction: {after.display_name} has been verified and reinstated.')
if c.overwrites_for(after).send_messages is False: await c.set_permissions(after, overwrite = None, reason= f'Membership Restriction: {after.display_name} has been verified and reinstated.')
def setup(client):
client.add_cog(RestrictionListener(client))

View File

@ -9,7 +9,7 @@ from discord_slash.model import SlashCommandPermissionType, ButtonStyle
from discord_slash.client import SlashCommand
from discord_slash.utils.manage_components import create_select, create_select_option, create_actionrow, wait_for_component, create_button, create_actionrow
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile, pitchesFile, loadCog
#### Pitch Command
@ -42,8 +42,12 @@ class Pitch(commands.Cog, name='Pitch Command'):
lookup = yaml_load(lookupFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
pitches = {}
guildStr = str(ctx.guild.id)
pitches = yaml_load(pitchesFile)
if guildStr not in pitches: pitches[guildStr] = {}
if pitches[guildStr]:
await ctx.send(f'```Error: pitches are already running for the guild `{ctx.guild.name}`. Please close the existing pitches first before issuing this command.```')
return
if 'timeslots' not in conf[guildStr]: conf[guildStr]['timeslots'] = {}
tsDict = {k: conf[guildStr]['timeslots'][k] for k in data[guildStr] if data[guildStr][k]}
optionsList = [create_select_option(label=tsDict[x], value=x, description=x) for x in tsDict]
@ -83,7 +87,6 @@ class Pitch(commands.Cog, name='Pitch Command'):
ctx.guild.default_role: p
}
)
if guildStr not in pitches: pitches[guildStr] = {}
if timeslot not in pitches[guildStr]: pitches[guildStr][timeslot] = {}
pitches[guildStr][timeslot]['indices'] = {}
pitches[guildStr][timeslot]['entries'] = [x for x in data[guildStr][timeslot].values()]
@ -91,6 +94,7 @@ class Pitch(commands.Cog, name='Pitch Command'):
header_message = await ctx.channel.send(
content=f'**Game listing for {conf[guildStr]["timeslots"][timeslot]}**\n_ _```The following are the games that are being pitched. Please select which game you would like to join by clicking on the `Join` button below.```'
)
pitches[guildStr][timeslot]['header_message'] = header_message.id
pitches[guildStr][timeslot]['messages'] = []
pitches[guildStr][timeslot]['roles'] = {}
for index, element in enumerate(pitches[guildStr][timeslot]['entries']):
@ -122,9 +126,9 @@ class Pitch(commands.Cog, name='Pitch Command'):
)
]
)
pitches[guildStr][timeslot]['messages'].append(m)
pitches[guildStr][timeslot]['messages'].append(m.id)
r = discord.utils.find(lambda x: x.id == element['role'],ctx.guild.roles)
pitches[guildStr][timeslot]['roles'][index] = r
pitches[guildStr][timeslot]['roles'][index] = r.id
pitches[guildStr][timeslot]['indices'][r.id] = index
newcomer = returning_player = None
if 'newcomer' in conf[guildStr]['roles']: newcomer = discord.utils.find(lambda x: x.id == conf[guildStr]['roles']['newcomer'], ctx.guild.roles)
@ -172,123 +176,11 @@ class Pitch(commands.Cog, name='Pitch Command'):
)
]
)
while True:
button_ctx = await wait_for_component(
self.client,
messages=pitches[guildStr][timeslot]['messages'] + [control]
)
if button_ctx.origin_message.id == control.id:
if not (set(ctx.author.roles) & set([ctx.guild.get_role(x) for x in conf[str(ctx.guild.id)]['roles']['admin']]) or ctx.author == ctx.guild.owner):
await button_ctx.send(f'```Error: You are not authorised to do this. The control panel may only be issued by an administrator.```',hidden=True)
else:
if button_ctx.custom_id == 'allow_returning':
await ctx.channel.set_permissions(reason=f'/pitch command issued by {ctx.author.display_name}', target=returning_player, read_messages=True)
await button_ctx.send(f'```Returning Players have now been allowed access to the pitch menu.```', hidden=True)
if button_ctx.custom_id == 'allow_newcomers':
await ctx.channel.set_permissions(reason=f'/pitch command issued by {ctx.author.display_name}', target=newcomer, read_messages=True)
await button_ctx.send(f'```Newcomers have now been allowed access to the pitch menu.```', hidden=True)
if button_ctx.custom_id == 'allow_all':
await ctx.channel.set_permissions(reason=f'/pitch command issued by {ctx.author.display_name}', target=ctx.guild.default_role, read_messages= True, send_messages=False)
await button_ctx.send(f'```All members have now been allowed access to the pitch menu.```', hidden=True)
if button_ctx.custom_id == 'close_pitches': break
else:
index = int(button_ctx.custom_id.split('_',1)[1])
if button_ctx.custom_id.startswith('join_'):
if set(button_ctx.author.roles) & set(pitches[guildStr][timeslot]['roles'].values()):
print('Check 0')
for role in list(set(button_ctx.author.roles) & set(pitches[guildStr][timeslot]['roles'].values())):
if role != pitches[guildStr][timeslot]['roles'][index]:
print('check 1')
await button_ctx.author.remove_roles(role,reason=f'/pitch interaction by {button_ctx.author.display_name}')
data[guildStr][timeslot][str(role.id)]['current_players'] -= 1
i = pitches[guildStr][timeslot]['indices'][role.id]
element = pitches[guildStr][timeslot]['entries'][i]
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
await pitches[guildStr][timeslot]['messages'][i].edit(content=o)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{button_ctx.author.display_name} has left the game.```')
role = pitches[guildStr][timeslot]['roles'][index]
if role in button_ctx.author.roles:
await button_ctx.send(f'```Error: You are already in the game `{lookup[guildStr][str(role.id)]["game_title"]}`.```', hidden=True)
else:
await button_ctx.author.add_roles(role,reason=f'/pitch interaction by {button_ctx.author.display_name}')
data[guildStr][timeslot][str(role.id)]['current_players'] += 1
element = pitches[guildStr][timeslot]['entries'][index]
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
await pitches[guildStr][timeslot]['messages'][index].edit(content=o)
await button_ctx.send(f'You have joined the game `{lookup[guildStr][str(role.id)]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{button_ctx.author.display_name} has joined the game.```')
elif button_ctx.custom_id.startswith('leave_'):
role = pitches[guildStr][timeslot]['roles'][index]
if role not in button_ctx.author.roles:
await button_ctx.send(f'```Error: You are not in the game `{lookup[guildStr][str(role.id)]["game_title"]}`.```', hidden=True)
else:
await button_ctx.author.remove_roles(role,reason=f'/pitch interaction by {button_ctx.author.display_name}')
data[guildStr][timeslot][str(role.id)]['current_players'] -= 1
element = pitches[guildStr][timeslot]['entries'][index]
gm = await self.client.fetch_user(element['gm'])
o = f'_ _\n***{element["game_title"]}*** (GM: {gm.mention})\n```\n'
if element['system'] is not None: o = ''.join([o,f'System: {element["system"]}\n'])
if element['min_players'] is not None: o = ''.join([o,f'Minimum Players: {str(element["min_players"])} '])
if element['max_players'] is not None: o = ''.join([o,f'Maximum Players: {str(element["max_players"])}\n'])
if element['platform'] is not None: o = ''.join([o,f'Platform: {element["platform"]}\n'])
o = ''.join([o,f'```'])
spaces_remaining = element["max_players"] - element["current_players"]
o = ''.join([o,f'~~Spaces Remaining: {str(0)}~~'])if spaces_remaining <= 0 else ''.join([o,f'Spaces Remaining: {str(spaces_remaining)}'])
await pitches[guildStr][timeslot]['messages'][index].edit(content=o)
await button_ctx.send(f'You have left the game `{lookup[guildStr][str(role.id)]["game_title"]}`.',hidden=True)
tc = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['text_channel'],ctx.guild.text_channels)
if tc is None:
c = discord.utils.find(lambda x: x.id == lookup[guildStr][str(role.id)]['category'],ctx.guild.categories)
if c is not None:
tPos = len(ctx.guild.channels)
for t in c.text_channels:
if t.position <= tPos:
tc = t
tPos = t.position
if tc is not None:
await tc.send(f'```{button_ctx.author.display_name} has left the game.```')
yaml_dump(data, dataFile)
await header_message.delete()
for message in pitches[guildStr][timeslot]['messages']: await message.delete()
await control.delete()
await ctx.channel.edit(reason=f'/pitch command issued by {ctx.author.display_name}', overwrites={})
await button_ctx.channel.send('```Pitch menu cleared. Pitches have now concluded.```')
pitches[guildStr][timeslot]['control'] = control.id
yaml_dump(pitches,pitchesFile)
if self.client.get_cog('Pitch Listener') is None:
loadCog(f'./{cogsDir}/events/secondary/pitch_listener.py')
#### Activate global pitch listener
def setup(client):
client.add_cog(Pitch(client))

View File

@ -1 +1,4 @@
'864651943820525609': {}
'864651943820525609':
'868506573700468787': 868506572421234718
'868506638720577586': 868506637252583494
'868506722187227166': 868506720375300137

View File

@ -2,7 +2,7 @@
channels:
help: 866645822472454206
mod: 865348933022515220
signup: 866110421592965171
signup: 868523157680693278
configured: true
membership:
- 866795009121714207
@ -12,6 +12,7 @@
signup: true
owner: 493694762210033664
prefix: '-'
restrict: false
roles:
admin:
- 866642278529368095

View File

@ -1 +1,38 @@
'864651943820525609': {}
'864651943820525609':
avatar:
'868506572421234718':
category: 868506573700468787
current_players: 1
game_title: Kyoshi
gm: 864649599671205914
header_message: 868506578934976543
max_players: 5
min_players: null
platform: null
role: 868506572421234718
system: null
text_channel: 868506576695230534
'868506637252583494':
category: 868506638720577586
current_players: 0
game_title: Roku
gm: 864649599671205914
header_message: 868506642545795142
max_players: 5
min_players: null
platform: null
role: 868506637252583494
system: null
text_channel: 868506640347971655
'868506720375300137':
category: 868506722187227166
current_players: 0
game_title: Aang
gm: 864649599671205914
header_message: 868506726029197312
max_players: 5
min_players: null
platform: null
role: 868506720375300137
system: null
text_channel: 868506724045303878

View File

@ -1 +1,5 @@
'864651943820525609': {}
'864651943820525609':
'864649599671205914':
- 868506572421234718
- 868506637252583494
- 868506720375300137

View File

@ -1 +1,19 @@
'864651943820525609': {}
'864651943820525609':
'868506572421234718':
category: 868506573700468787
game_title: Kyoshi
gm: 864649599671205914
text_channel: 868506576695230534
time: avatar
'868506637252583494':
category: 868506638720577586
game_title: Roku
gm: 864649599671205914
text_channel: 868506640347971655
time: avatar
'868506720375300137':
category: 868506722187227166
game_title: Aang
gm: 864649599671205914
text_channel: 868506724045303878
time: avatar

1
app/data/pitches.yml Normal file
View File

@ -0,0 +1 @@
{}