diff --git a/TODO.md b/TODO.md index f607b41..641203c 100644 --- a/TODO.md +++ b/TODO.md @@ -18,7 +18,7 @@ - [ ] Re-enable logging - [x] Delete Dev/Test Functions - [x] Error handlers -- [ ] Debug Features +- [x] Debug Features > - [ ] ~~Command Installer/Uninstaller~~ - [x] Help Channel Event Listener > - [x] Add Config key for Help Channel @@ -37,6 +37,9 @@ > - [ ] Channel delete (notifications, logs) - [x] Flag for checking completeness of configuration for a guild. > - [x] Function for checking configs for completeness +- [ ] Synchronise roles on game channel updates +> - [ ] Exception to event listener to prevent circularity + ## Event Listeners ## Review Configs When diff --git a/app/cogs/slashcommands/secondary/game_management.py b/app/cogs/slashcommands/secondary/game_management.py index c3bb4d4..d0131be 100644 --- a/app/cogs/slashcommands/secondary/game_management.py +++ b/app/cogs/slashcommands/secondary/game_management.py @@ -56,10 +56,7 @@ class GameManagement(commands.Cog, name='Game Management'): c = ctx.guild.get_channel(lookup[guildStr][str(game_role.id)]['category']) gm = lookup[guildStr][str(game_role.id)]['gm'] channelsFound = False - for g in list(data[guildStr][time]): - if game_role.id in g.values(): - data[guildStr][time].remove(g) - break + del data[guildStr][time][str(game_role.id)] if c is not None: channelsFound = True for t in ctx.guild.text_channels: @@ -136,8 +133,8 @@ class GameManagement(commands.Cog, name='Game Management'): required=False ), create_option( - name='reserved_spaces', - description='The number of spaces the GM is reserving in the game.', + name='current_players', + description='The number players currently in the game.', option_type=4, required=False ), @@ -164,12 +161,12 @@ class GameManagement(commands.Cog, name='Game Management'): max_players:int=None, game_title:str=None, min_players:int=None, - reserved_spaces: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, reserved_spaces, system, platform]): + if all(x is None for x in [timeslot, gm, max_players, game_title, min_players, current_players, system, platform]): await ctx.send(f'```No parameters have been entered to modify the game.```') return conf = yaml_load(configFile) @@ -177,88 +174,70 @@ class GameManagement(commands.Cog, name='Game Management'): gms = yaml_load(gmFile) lookup = yaml_load(lookupFile) 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] = {} - r = game_role - if timeslot is not None: - time = re.sub(r"\W+",'', timeslot[:9].lower()) - if time not in conf[guildStr]['timeslots']: - await ctx.send(f'```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.```') - return - if time not in data[guildStr]: - data[guildStr][time] = [] - else: - time = lookup[guildStr][str(r.id)]['time'] - g_title = game_title if game_title is not None else lookup[guildStr][str(r.id)]['game_title'] - if str(r.id) not in lookup[guildStr]: + if time not in data[guildStr]: + data[guildStr][time] = {} + # Command Validation Checks + if rStr not in lookup[guildStr]: await ctx.send(f'```This is not a valid game role. Please mention a role that is associated with a game.```') return - if 'roles' not in conf[guildStr]: - conf[guildStr]['roles'] = {} - if 'bot' not in conf[guildStr]['roles']: - await ctx.send(f'```\`Bot` role for guild `{ctx.guild.name}` has not been defined. Cannot configure game.```') - return - if 'timeslots' not in conf[guildStr]: - conf[guildStr]['timeslots'] = {} - if any(x is not None and x < 0 for x in [min_players, max_players, reserved_spaces]): + if timeslot is not None: + if time not in conf[guildStr]['timeslots']: + await ctx.send(f'```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.```') + return + if any(x is not None and x < 0 for x in [min_players, max_players, current_players]): await ctx.send(f'```You cannot enter negative integers for the number of players.```') return - if min_players is not None and max_players is not None: - if min_players > max_players: - await ctx.send(f'```The minimum number of players cannot exceed the maximum number of players.```') - return - if reserved_spaces is not None and max_players is not None: - if reserved_spaces > max_players: - await ctx.send(f'```The number of reserved spaces cannot exceed the maximum number of players.```') - return - if game_title is not None: + if min_players and max_players and min_players > max_players: + await ctx.send(f'```The minimum number of players cannot exceed the maximum number of players.```') + return + if current_players and max_players and current_players > max_players: + await ctx.send(f'```The number of reserved spaces cannot exceed the maximum number of players.```') + 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 game_title and game_title != old_data['game_title']: if game_title in [x['game_title'] for x in lookup[guildStr].values()] and time in [x['time'] for x in lookup[guildStr].values()]: await ctx.send(f'```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.```') return - mc = gm if gm is not None else ctx.guild.get_member(lookup[guildStr][str(r.id)]['gm']) - #### Retrieve Old Values - oldTime = lookup[guildStr][str(r.id)]['time'] - for g in data[guildStr][oldTime]: - if game_role.id in g.values(): - oldDict = g.copy() - break - gDict = { - 'game_title': game_title, - 'gm': mc.id, - 'max_players': max_players, - 'min_players': min_players, - 'current_players': reserved_spaces, - 'system': system, - 'platform': platform, - # 'role': r.id, - # 'category': c.id, - # 'text_channel': t.id, - # 'header_message': o.id - } - d = [k for k in gDict if gDict[k] != oldDict[k] and gDict[k] != None] - result = f'The game {g_title} has been updated.\n' - if time != oldTime: - for g in list(data[guildStr][oldTime]): - if game_role.id in g.values(): - data[guildStr][oldTime].remove(g) - break - data[guildStr][oldTime].pop(str(r.id), None) - result = ''.join([result,f'The time slot for the game has been updated to {conf[guildStr]["timeslots"][time]}.\n']) - if 'gm' in d: - if r not in mc.roles: - await mc.add_roles(r) - if guildStr not in gms: - gms[guildStr] = {} - if str(mc.id) not in gms[guildStr]: - gms[guildStr][str(gm.id)] = [] - gms[guildStr][str(mc.id)].append(r.id) - gms[guildStr][str(lookup[guildStr][str(r.id)]['gm'])].remove(r.id) - if not gms[guildStr][str(lookup[guildStr][str(r.id)]['gm'])]: - gms[guildStr].pop(str(lookup[guildStr][str(r.id)]['gm']), None) - result = ''.join([result,f'The GM has been updated to {mc.display_name}.\n']) - c = await self.client.fetch_channel(lookup[guildStr][str(r.id)]['category']) + 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, + 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) + 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), @@ -274,35 +253,29 @@ class GameManagement(commands.Cog, name='Game Management'): deafen_members=True ) } - print('So far so good.') - if time != oldTime or 'game_title' in d: - await r.edit( - name=f'{time.upper()}: {g_title}', - reason=f'/game modify command issued by `{ctx.author.display_name}`', - mentionable=True, - permissions=discord.Permissions.none(), - ) - result = ''.join([result,f'The names of the role and channel category have been updated to match the new {"game title" if "game_title" in d and time == oldTime else "time slot" if "game_title" not in d and time != oldTime else "game title and time slot"}.\n']) - if c is None: + 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()}: {g_title}', + 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: {g_title}', - topic=f'Default voice channel for the game `{g_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{mc.display_name}`.', - reason=f'/game create command issued by `{ctx.author.display_name}`' + 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: {g_title}', - topic=f'Default text channel for the game `{g_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{mc.display_name}`.', - reason=f'/game create command issued by `{ctx.author.display_name}`' + 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: - print(type(c)) + cExists= True await c.edit( - name=f'{time.upper()}: {g_title}', overwrites=permissions, reason=f'/game modify command issued by `{ctx.author.display_name}`', ) @@ -322,68 +295,71 @@ class GameManagement(commands.Cog, name='Game Management'): reason=f'/game modify command issued by `{ctx.author.display_name}`' ) v = True - if t is None: + if not t: t = await c.create_text_channel( - name=f'text: {g_title}', - topic=f'Default text channel for the game `{g_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{mc.display_name}`.', + 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: - hm = await t.fetch_message(oldDict['header_message']) - if hm is not None: - await hm.delete() - else: - pins = await t.pins - if pins: - hm = discord.utils.find(lambda x: x.text.startswith('```Hello ' and 'Your game channels for ' in x.text and x.author.id == self.client.user.id), pins) - if hm is not None: - await hm.delete() + 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: {g_title}', - topic=f'Default voice channel for the game `{g_title}`` taking place at `{conf[guildStr]["timeslots"][time]}`, with GM `{mc.display_name}`.', + 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}`' ) - result = ''.join([result,f'The game has space for {max_players} players (with {reserved_spaces if reserved_spaces is not None else str(0)} currently occupied).\n']) - sys = system if system is not None else oldDict['system'] - max_p = max_players if max_players is not None else oldDict['max_players'] - min_p = min_players if min_players is not None else oldDict['min_players'] - cur_p = reserved_spaces if reserved_spaces is not None else oldDict['current_players'] - plat = platform if platform is not None else oldDict['platform'] - output = f'```Hello {mc.display_name}! Your game channels for `{g_title}` have been created.\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: {g_title}\n']) - output = ''.join([output,f'GM: {mc.display_name}\n']) + + # 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: {sys}\n'if sys is not None else '']) - output = ''.join([output,f'Max Players: {max_p}\n']) - output = ''.join([output,f'Min Players: {min_p}\n'if min_p is not None else '']) - output = ''.join([output,f'Current Players: {cur_p}\n' if cur_p is not None else '0\n']) - output = ''.join([output,f'Platform: {plat}```' if plat is not None else '```']) - result = ''.join(['```',result,f'```\n{mc.mention} {r.mention} {t.mention}']) + 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 '```']) await ctx.send(result) o = await t.send(output) - await o.pin(reason=f'/game modift command issued by `{ctx.author.display_name}`') - gDict = { - 'game_title': g_title, - 'gm': mc.id, - 'max_players': max_p, - 'min_players': min_p, - 'current_players': cur_p, - 'system': sys, - 'platform': plat, + await o.pin(reason=f'/game create 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 } - if time != oldTime: - data[guildStr][time].append(gDict) - lookup[guildStr][str(r.id)] = { + lookup[guildStr][rStr] = { 'category': c.id, - 'gm': mc.id, + 'gm': gm.id, 'time': time, - 'game_title': g_title + '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) yaml_dump(data,dataFile) yaml_dump(lookup,lookupFile) yaml_dump(gms,gmFile) diff --git a/app/cogs/slashcommands/secondary/game_setup.py b/app/cogs/slashcommands/secondary/game_setup.py index 0a2c974..26e1037 100644 --- a/app/cogs/slashcommands/secondary/game_setup.py +++ b/app/cogs/slashcommands/secondary/game_setup.py @@ -73,8 +73,8 @@ class GameSetup(commands.Cog, name='Game Setup'): required=False ), create_option( - name='reserved_spaces', - description='The number of spaces the GM is reserving in the game.', + name='current_players', + description='The number of players currently in this game.', option_type=4, required=False ), @@ -100,7 +100,7 @@ class GameSetup(commands.Cog, name='Game Setup'): max_players:int, game_title:str, min_players: typing.Optional[int]=None, - reserved_spaces: typing.Optional[int]=None, + current_players: typing.Optional[int]=None, system:typing.Optional[str]= None, platform:typing.Optional[str]=None ): @@ -121,15 +121,13 @@ class GameSetup(commands.Cog, name='Game Setup'): if time not in conf[guildStr]['timeslots']: await ctx.send(f'```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.```') return - if min_players is not None: - if min_players > max_players: - await ctx.send(f'```The minimum number of players cannot exceed the maximum number of players.```') - return - if reserved_spaces is not None: - if reserved_spaces > max_players: - await ctx.send(f'```The number of reserved spaces cannot exceed the maximum number of players.```') - return - if any(x is not None and x < 0 for x in [min_players, max_players, reserved_spaces]): + if min_players and min_players > max_players: + await ctx.send(f'```The minimum number of players cannot exceed the maximum number of players.```') + return + if current_players and current_players > max_players: + await ctx.send(f'```The number of reserved spaces cannot exceed the maximum number of players.```') + return + if any(x is not None and x < 0 for x in [min_players, max_players, current_players]): await ctx.send(f'```You cannot enter negative integers for the number of players.```') return if guildStr not in lookup: @@ -140,10 +138,10 @@ class GameSetup(commands.Cog, name='Game Setup'): if guildStr not in data: data[guildStr] = {} if time not in data[guildStr]: - data[guildStr][time] = [] + data[guildStr][time] = {} rExists, cExists = False, False r = discord.utils.get(ctx.guild.roles, name=f'{time.upper()}: {game_title}') - if r is None: + if not r: r = await ctx.guild.create_role( name=f'{time.upper()}: {game_title}', reason=f'/game create command issued by `{ctx.author.display_name}`', @@ -177,7 +175,7 @@ class GameSetup(commands.Cog, name='Game Setup'): ) } c = discord.utils.get(ctx.guild.categories, name=f'{time.upper()}: {game_title}') - if c is None: + if not c: c = await ctx.guild.create_category( name=f'{time.upper()}: {game_title}', overwrites=permissions, @@ -215,16 +213,16 @@ class GameSetup(commands.Cog, name='Game Setup'): reason=f'/game create command issued by `{ctx.author.display_name}`' ) v = True - if t is None: + 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 create command issued by `{ctx.author.display_name}`' ) else: - pins = await t.pins + pins = await t.pins() if pins: - hm = discord.utils.find(lambda x: x.text.startswith('```Hello ' and 'Your game channels for ' in x.text and x.author.id == self.client.user.id), 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 is not None: await hm.delete() if not v: @@ -233,12 +231,12 @@ class GameSetup(commands.Cog, name='Game Setup'): 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 create command issued by `{ctx.author.display_name}`' ) - result = f'Game `{game_title}` has been created for timeslot `{conf[guildStr]["timeslots"][time]}`` with GM `{gm.display_name}` and space for {max_players} players (with {reserved_spaces if reserved_spaces is not None else str(0)} currently occupied).\n' - if not rExists: + result = f'Game `{game_title}` has been created for timeslot `{conf[guildStr]["timeslots"][time]}`` with GM `{gm.display_name}` and space for {max_players} players (with {current_players if current_players else "0"} currently occupied).\n' + if rExists: result = ''.join([result,f'There was already a role that matched the game, so that role has been reconfigured.\n\nNote: Editing this role will synchronise changes with the game channels, and deleting the role will delete the game and all its data.\n\n']) else: - result = ''.join([result,f'A role for the game has been created.\n']) - if not cExists: + result = ''.join([result,f'A role for the game has been created.\n\nNote: Editing this role will synchronise changes with the game channels, and deleting the role will delete the game and all its data.\n\n']) + if cExists: result = ''.join([result,f'There was already a channel category that matched the game, so it has been reconfigured with the appropriate permissions and text and voice channels.\n']) else: result = ''.join([result,f'A channel category with the appropriate text and voice channels has been created.\n']) @@ -250,17 +248,17 @@ class GameSetup(commands.Cog, name='Game Setup'): 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: {reserved_spaces}\n' if reserved_spaces is not None else '0\n']) + 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 '```']) await ctx.send(result) o = await t.send(output) await o.pin(reason=f'/game create command issued by `{ctx.author.display_name}`') - gDict = { + data[guildStr][time][str(r.id)] = { 'game_title': game_title, 'gm': gm.id, 'max_players': max_players, 'min_players': min_players, - 'current_players': reserved_spaces, + 'current_players': current_players, 'system': system, 'platform': platform, 'role': r.id, @@ -268,12 +266,12 @@ class GameSetup(commands.Cog, name='Game Setup'): 'text_channel': t.id, 'header_message': o.id } - data[guildStr][time].append(gDict) lookup[guildStr][str(r.id)] = { 'category': c.id, 'gm': gm.id, 'time': time, - 'game_title': game_title + 'game_title': game_title, + 'text_channel': t.id } if guildStr not in gms: gms[guildStr] = {} diff --git a/app/data/config.yml b/app/data/config.yml index 29f0ee8..4d49398 100644 --- a/app/data/config.yml +++ b/app/data/config.yml @@ -3,8 +3,9 @@ help: 866645822472454206 mod: 865348933022515220 signup: 866110421592965171 - configured: false - membership: [] + configured: true + membership: + - 866795009121714207 name: Test notifications: help: true diff --git a/app/data/data.yml b/app/data/data.yml index 315aaa4..da1d382 100644 --- a/app/data/data.yml +++ b/app/data/data.yml @@ -1,2 +1,14 @@ '864651943820525609': - sunaft: [] + suneve: + '866788659839238164': + category: 866788661026488352 + current_players: null + game_title: Avatar Legends + gm: 493694762210033664 + header_message: 866825474252210196 + max_players: 6 + min_players: null + platform: null + role: 866788659839238164 + system: null + text_channel: 866788663194812446 diff --git a/app/data/gm.yml b/app/data/gm.yml index 104d380..7a24859 100644 --- a/app/data/gm.yml +++ b/app/data/gm.yml @@ -1 +1,4 @@ -'864651943820525609': {} +'864651943820525609': + '493694762210033664': + - 866788659839238164 + '864649599671205914': [] diff --git a/app/data/lookup.yml b/app/data/lookup.yml index 104d380..0bae420 100644 --- a/app/data/lookup.yml +++ b/app/data/lookup.yml @@ -1 +1,7 @@ -'864651943820525609': {} +'864651943820525609': + '866788659839238164': + category: 866788661026488352 + game_title: Avatar Legends + gm: 493694762210033664 + text_channel: 866788663194812446 + time: suneve