geas-bot/app/cogs/slashcommands/secondary/game_management.py
2021-07-24 13:15:01 +01:00

560 lines
22 KiB
Python

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 # 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
import re
from bot import configFile, yaml_load, yaml_dump, cogsDir, unloadCog, dataFile, lookupFile, gmFile, categoriesFile
class GameManagement(commands.Cog, name='Game Management'):
def __init__(self, client):
self.client = client
lookup = yaml_load(lookupFile)
guild_ids= [ int(x) for x in list(lookup)]
### Move delete, Modify, and Reset commands to a separate secondary cog to enable when games exist?
@cog_ext.cog_subcommand(
base='game',
# subcommand_group='',
name='delete',
description='Deletes a game role and accompanying category, text, voice channels, and data.',
# base_description='Commands for setting up and removing games on the Guild.',
# base_default_permission=False,
# base_permissions=permissions,
# subcommand_group_description='Adds a time slot available to the channel for games.',
guild_ids=guild_ids,
options=[
create_option(
name='game_role',
description='The role representing the game you want to interact with.',
option_type=8,
required=True
)
]
)
async def _game_delete(self, ctx:SlashContext, game_role:discord.Role):
await ctx.channel.trigger_typing()
conf = yaml_load(configFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
lookup = yaml_load(lookupFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
rStr = str(game_role.id)
if 'bot' not in conf[guildStr]['roles']:
await ctx.send(f'```Error: `Bot` role for guild `{ctx.guild.name}` has not been defined. Cannot configure game.```',hidden=True)
return
if rStr not in lookup[guildStr]:
await ctx.send(f'```Error: This is not a valid game role. Please mention a role that is associated with a game.```', hidden=True)
return
if ctx.channel.category.id != lookup[guildStr][rStr]['category']:
await ctx.send(f'```Error: You must issue this command in a text channel associated with the game you are trying to delete.```', hidden=True)
return
game_title = lookup[guildStr][rStr]['game_title']
time = lookup[guildStr][rStr]['time']
c = ctx.guild.get_channel(lookup[guildStr][rStr]['category'])
gm = lookup[guildStr][rStr]['gm']
channelsFound = False
del data[guildStr][time][rStr]
if c is not None:
channelsFound = True
for t in ctx.guild.text_channels:
if t.category == c:
await t.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`')
for v in ctx.guild.voice_channels:
if v.category == c:
await v.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`')
del categories[guildStr][str(c.id)]
await c.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`')
lookup[guildStr].pop(rStr, None)
gm_m = await ctx.guild.fetch_member(gm)
output = f'The game `{game_title}` for timeslot `{conf[guildStr]["timeslots"][time]}` and with GM `{gm_m.display_name}` has been deleted.'
if channelsFound:
output = ''.join([output,' All associated text, voice, and category channels have been deleted.'])
else:
output = ''.join([output,' No associated text, voice, or category channels were found. Please delete them manually if they still persist.'])
if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels)
await c.send(
content = f'```{output}```'
)
await game_role.delete(reason=f'/game delete command issued by `{ctx.author.display_name}`')
gms[guildStr][str(gm)].remove(game_role.id)
if not gms[guildStr][str(gm)]: del gms[guildStr][str(gm)]
if not data[guildStr][time]: del data[guildStr][time]
yaml_dump(lookup, lookupFile)
yaml_dump(data, dataFile)
yaml_dump(gms, gmFile)
yaml_dump(categories, categoriesFile)
if not any([x for x in yaml_load(lookupFile).values()]):
unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
if self.client.get_cog('Player Commands') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
if self.client.get_cog('Pitch Command') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
await self.client.slash.sync_all_commands()
@cog_ext.cog_subcommand(
base='game',
# subcommand_group='',
name='modify',
description='Edit the information of an existing game channel.',
# base_description='Commands for setting up and removing games on the Guild.',
# base_default_permission=False,
# base_permissions=permissions,
# subcommand_group_description='Adds a time slot available to the channel for games.',
guild_ids=guild_ids,
options=[
create_option(
name='game_role',
description='The role of the game you are trying to edit.',
option_type=8,
required=True,
),
create_option(
name='timeslot',
description='The new timeslot, if you are changing timeslots.',
option_type=3,
required=False
),
create_option(
name='gm',
description='The new GM, if the GM is changing.',
option_type=6,
required=False
),
create_option(
name='max_players',
description='The maximum number of players the game can take.',
option_type=4,
required=False
),
create_option(
name='game_title',
description='The new title if the title is changing.',
option_type=3,
required=False
),
create_option(
name='min_players',
description='The minimum number of players the gane can take.',
option_type=4,
required=False
),
create_option(
name='current_players',
description='The number players currently in the game.',
option_type=4,
required=False
),
create_option(
name='system',
description='What system the game is using.',
option_type=3,
required=False
),
create_option(
name='platform',
description='What platform the game will be running on.',
option_type=3,
required=False
)
]
)
async def _game_modify(
self,
ctx:SlashContext,
game_role:discord.Role,
timeslot:str=None,
gm:discord.User=None,
max_players:int=None,
game_title:str=None,
min_players:int=None,
current_players:int=None,
system:str=None,
platform:str=None
):
await ctx.channel.trigger_typing()
if all(x is None for x in [timeslot, gm, max_players, game_title, min_players, current_players, system, platform]):
await ctx.send(f'```Error: No parameters have been entered to modify the game.```',hidden=True)
return
conf = yaml_load(configFile)
data = yaml_load(dataFile)
gms = yaml_load(gmFile)
lookup = yaml_load(lookupFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
r = game_role
rStr = str(r.id)
old_time = lookup[guildStr][rStr]['time']
time = re.sub(r"\W+",'', timeslot[:9].lower()) if timeslot else old_time
if guildStr not in lookup:
lookup[guildStr] = {}
if guildStr not in data:
data[guildStr] = {}
if time not in data[guildStr]:
data[guildStr][time] = {}
# Command Validation Checks
if rStr not in lookup[guildStr]:
await ctx.send(f'```Error: This is not a valid game role. Please mention a role that is associated with a game.```',hidden=True)
return
if timeslot is not None:
if time not in conf[guildStr]['timeslots']:
await ctx.send(f'```Error: Time code `{timeslot}` is not recognised. Please enter a valid time code to register the game. use `/config timeslots list` to get a list of valid time codes.```',hidden=True)
return
if any(x is not None and x < 0 for x in [min_players, max_players, current_players]):
await ctx.send(f'```Error: You cannot enter negative integers for the number of players.```',hidden=True)
return
if min_players and max_players and min_players > max_players:
await ctx.send(f'```Error: The minimum number of players cannot exceed the maximum number of players.```',hidden=True)
return
if current_players and max_players and current_players > max_players:
await ctx.send(f'```Error: The number of reserved spaces cannot exceed the maximum number of players.```',hidden=True)
return
# Infer Old Data
old_data = data[guildStr][old_time][rStr].copy()
result = f'The game {old_data["game_title"]} has been updated.\n'
## Change to the Title and/or Time Slot:
if time != old_time:
result = ''.join([result,f"The game's time slot has changed from {conf[guildStr]['timeslots'][time]} to {conf[guildStr]['timeslots'][time]}.\n"])
del data[guildStr][old_time][rStr]
if not data[guildStr][old_time]:
del data[guildStr][old_time]
if game_title and game_title != old_data['game_title']:
if game_title in [x['game_title'] for x in lookup[str(ctx.guild.id)].values()] and time in [x['time'] for x in lookup[str(ctx.guild.id)].values()]:
await ctx.send(f'```Error: The target game `{game_title}` has already been created for the time slot `{conf[guildStr]["timeslots"][time]}`. Please avoud duplicates, or use the `modify` sub-command to edit the existing game.```',hidden=True)
return
result = ''.join([result,f"The game's title has been updated to {game_title}\n"])
game_title = old_data['game_title'] if not game_title else game_title
if time != old_time or game_title != old_data['game_title']:
result = ''.join([result,f"The names of the game role and channel categories have been changed to match the updated {'timeslot' if time != old_time and game_title == old_data['game_title'] else 'game title' if game_title != old_data['game_title'] and time == old_time else 'time slot and game title'}.\n"])
# Update Role
await r.edit(
mentionable=True,
name=f'{time.upper()}: {game_title}',
permissions=discord.Permissions.none(),
reason=f'/game modify command issued by `{ctx.author.display_name}`',
colour=discord.Colour.green()
)
# Update GM
if gm and gm.id != old_data['gm']:
result = ''.join([result,f"The GM has been updated to {gm.display_name}.\n"])
gms[guildStr][str(old_data['gm'])].remove(r.id)
if not gms[guildStr][str(old_data['gm'])]:
del gms[guildStr][str(old_data['gm'])]
gm = await ctx.guild.fetch_member(old_data['gm']) if not gm else gm
if r not in gm.roles:
await gm.add_roles(r)
# Update Category
cExists = False
permissions = {
ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False),
r: discord.PermissionOverwrite(read_messages=True),
ctx.guild.get_role(conf[guildStr]['roles']['bot']): discord.PermissionOverwrite(read_messages=True),
gm: 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
)
}
c_id = lookup[guildStr][rStr]['category']
c = discord.utils.get(ctx.guild.categories, id=c_id)
t_id = lookup[guildStr][rStr]['text_channel']
t = discord.utils.get(ctx.guild.text_channels, id=t_id)
if not c:
c = await ctx.guild.create_category(
name=f'{time.upper()}: {game_title}',
overwrites=permissions,
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
await c.create_voice_channel(
name=f'voice: {game_title}',
topic=f'Default voice channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.',
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
t = await c.create_text_channel(
name=f'text: {game_title}',
topic=f'Default text channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.',
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
else:
cExists= True
await c.edit(
name=f'{time.upper()}: {game_title}',
overwrites=permissions,
reason=f'/game modify command issued by `{ctx.author.display_name}`',
)
tPos = len(ctx.guild.channels)
t = None
v = False
for tc in c.text_channels:
if tc.position <= tPos:
t, tPos = tc, tc.position
await t.edit(
sync_permissions=True,
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
for vc in c.voice_channels:
await vc.edit(
sync_permissions=True,
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
v = True
if not t:
t = await c.create_text_channel(
name=f'text: {game_title}',
topic=f'Default text channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.',
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
else:
pins = await t.pins()
if pins:
hm = discord.utils.find(lambda x: x.content.startswith('```Hello ') and 'Your game channels for ' in x.content and x.author.id == self.client.user.id, pins)
if hm:
await hm.delete()
if not v:
await c.create_voice_channel(
name=f'voice: {game_title}',
topic=f'Default voice channel for the game `{game_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{gm.display_name}`.',
reason=f'/game modify command issued by `{ctx.author.display_name}`'
)
# Determine remaining variables
max_players = old_data['max_players'] if not max_players else max_players
min_players = old_data['min_players'] if not min_players else min_players
current_players = old_data['current_players'] if not current_players else current_players
platform = old_data['platform'] if not platform else platform
system = old_data['system'] if not system else system
result = ''.join(['```',result,f"The game has been configured with space for {max_players} players (with {current_players if current_players else '0'} currently occupied).",'```'])
output = f'```Hello {gm.display_name}! Your game channels for `{game_title}` has been updated.\nYou can ping your players or edit the game settings by interacting with the game role through the Bot commands. Have fun!```\n'
output = ''.join([output,f'```Game Title: {game_title}\n'])
output = ''.join([output,f'GM: {gm.display_name}\n'])
output = ''.join([output,f'Time: {conf[guildStr]["timeslots"][time]}\n'])
output = ''.join([output,f'System: {system}\n'if system is not None else ''])
output = ''.join([output,f'Max Players: {max_players}\n'])
output = ''.join([output,f'Min Players: {min_players}\n'if min_players is not None else ''])
output = ''.join([output,f'Current Players: {current_players if current_players else "0"}\n'])
output = ''.join([output,f'Platform: {platform}```' if platform is not None else '```'])
output = ''.join([output,f'\n\n{gm.mention} | {r.mention}'])
await ctx.send(result,hidden=True)
o = await t.send(output)
await o.pin(reason=f'/game modify command issued by `{ctx.author.display_name}`')
data[guildStr][time][rStr] = {
'game_title': game_title,
'gm': gm.id,
'max_players': max_players,
'min_players': min_players,
'current_players': current_players,
'system': system,
'platform': platform,
'role': r.id,
'category': c.id,
'text_channel': t.id,
'header_message': o.id
}
lookup[guildStr][rStr] = {
'category': c.id,
'gm': gm.id,
'time': time,
'game_title': game_title,
'text_channel': t.id
}
if guildStr not in gms: gms[guildStr] = {}
if str(gm.id) not in gms[guildStr]: gms[guildStr][str(gm.id)] = []
gms[guildStr][str(gm.id)].append(r.id)
if guildStr not in categories: categories[guildStr] = {}
categories[guildStr][str(c.id)] = r.id
yaml_dump(data,dataFile)
yaml_dump(lookup,lookupFile)
yaml_dump(gms,gmFile)
yaml_dump(categories, categoriesFile)
@cog_ext.cog_subcommand(
base='game',
# subcommand_group='',
name='purge',
description='Delete all games in a given timeslot.',
# base_description='Commands for setting up and removing games on the Guild.',
# base_default_permission=False,
# base_permissions=permissions,
# subcommand_group_description='Adds a time slot available to the channel for games.',
guild_ids=guild_ids,
)
async def _game_purge(
self,
ctx:SlashContext
):
await ctx.channel.trigger_typing()
conf = yaml_load(configFile)
data = yaml_load(dataFile)
lookup = yaml_load(lookupFile)
gms = yaml_load(gmFile)
categories = yaml_load(categoriesFile)
guildStr = str(ctx.guild.id)
async def purgeGames(ctx:SlashContext, timeslot:str):
for g in list(data[guildStr][timeslot].values()):
c = discord.utils.find(lambda x: x.id == g['category'], ctx.guild.categories)
r = discord.utils.find(lambda x: x.id == g['role'], ctx.guild.roles)
for x in c.channels:
await x.delete(reason=f'/game purge command issued by `{ctx.author.display_name}`')
del categories[guildStr][str(c.id)]
await c.delete(reason=f'/game purge command issued by `{ctx.author.display_name}`')
await r.delete(reason=f'/game purge command issued by `{ctx.author.display_name}`')
gms[guildStr][str(g['gm'])].remove(r.id)
if not gms[guildStr][str(g['gm'])]:
del gms[guildStr][str(g['gm'])]
del lookup[guildStr][str(r.id)]
del data[guildStr][timeslot]
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]
optionsList.insert(0, create_select_option(label='All Timeslots', value='--all', description='--all'))
try:
m = await ctx.send(
content='```Select which time slot for which you would like to purge all games.```',
delete_after=5,
components=[
create_actionrow(
create_select(
placeholder='Time Slot',
options= optionsList,
min_values=1,
max_values=1
)
)
]
)
while True:
select_ctx = await wait_for_component(self.client,messages=m, timeout=5)
if select_ctx.author != ctx.author:
await select_ctx.send(f'```Invalid response: you are not the person who issued the command.```', hidden=True)
else:
break
await m.delete()
[timeslot] = select_ctx.selected_options
except asyncio.TimeoutError:
await ctx.send(f'```Error: Command timed out.```', hidden=True)
return
if timeslot == '--all':
m = await ctx.send(
content=f'```You are attempting to purge games for all time slots. This will delete every game currently running for guild {ctx.guild.name}. Are you sure?```',
delete_after=5,
components=[
create_actionrow(
create_button(
style=ButtonStyle.green,
label='Yes',
emoji='👍',
custom_id='purge_yes',
),
create_button(
style=ButtonStyle.red,
label='No',
emoji='👎',
custom_id='purge_no',
)
)
]
)
while True:
button_ctx = await wait_for_component(self.client, messages=m, timeout=5)
if button_ctx.author != ctx.author:
await button_ctx.send(f'```Invalid response: you are not the person who issued the command.```', hidden=True)
else:
break
await m.delete()
if button_ctx.custom_id == 'purge_no':
await ctx.send(f'```The action `/game purge --all` has been aborted.```',hidden=True)
return
await ctx.channel.trigger_typing()
ctx_id = ctx.channel.id
for t in list(data[guildStr]): await purgeGames(ctx=ctx, timeslot=t)
if discord.utils.find(lambda x: x.id == ctx_id, ctx.guild.text_channels) is not None:
await ctx.send(
content = '```All games for all time slots have been purged.```',
hidden=True
)
else:
if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels)
await c.send(
content = '```All games for all time slots have been purged.```'
)
else:
m = await ctx.send(
content=f'```You are attempting to purge games for `{conf[guildStr]["timeslots"][timeslot]}` for guild {ctx.guild.name}. Are you sure?```',
delete_after=5,
components=[
create_actionrow(
create_button(
style=ButtonStyle.green,
label='Yes',
emoji='👍',
custom_id='purge_yes',
),
create_button(
style=ButtonStyle.red,
label='No',
emoji='👎',
custom_id='purge_no',
)
)
]
)
while True:
button_ctx = await wait_for_component(self.client, messages=m, timeout=5)
if button_ctx.author != ctx.author:
await button_ctx.send(f'```Invalid response: you are not the person who issued the command.```', hidden=True)
else:
break
await m.delete()
if button_ctx.custom_id == 'purge_no':
await ctx.send(f'```The action `/game purge {timeslot}` has been aborted.```',hidden=True)
return
await ctx.channel.trigger_typing()
ctx_id = ctx.channel.id
await purgeGames(ctx=ctx, timeslot=timeslot)
if discord.utils.find(lambda x: x.id == ctx_id, ctx.guild.text_channels) is not None:
await ctx.send(
content = f'```All games for time slot `{conf[guildStr]["timeslots"][timeslot]}` have been purged.```',
hidden=True
)
else:
if 'mod' in conf[guildStr]['channels'] and 'committee' in conf[guildStr]['roles']:
c = discord.utils.find(lambda x: x.id == conf[guildStr]['channels']['mod'], ctx.guild.channels)
await c.send(
content = f'```All games for time slot `{conf[guildStr]["timeslots"][timeslot]}` have been purged.```'
)
yaml_dump(gms,gmFile)
yaml_dump(lookup,lookupFile)
yaml_dump(data,dataFile)
yaml_dump(categories,categoriesFile)
if not any([x for x in yaml_load(lookupFile).values()]):
unloadCog(f'./{cogsDir}/slashcommands/secondary/game_management.py')
if self.client.get_cog('Player Commands') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/player_commands.py')
if self.client.get_cog('Pitch Command') is not None: unloadCog(f'./{cogsDir}/slashcommands/secondary/pitch.py')
await self.client.slash.sync_all_commands()
def setup(client):
client.add_cog(GameManagement(client))