From e2a2e7be8afb1b50843ce57fc6edeb57c1b36cae Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 1 Oct 2023 02:22:09 +0300 Subject: [PATCH] fix(schedule): Remove members after session. --- src/modules/schedule/core/session.py | 38 +++++++++++--- src/modules/schedule/core/timeslot.py | 74 ++++++++++++++++++++++++++- src/modules/schedule/lib.py | 27 +++++++++- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/modules/schedule/core/session.py b/src/modules/schedule/core/session.py index 20096e1b..537b7840 100644 --- a/src/modules/schedule/core/session.py +++ b/src/modules/schedule/core/session.py @@ -12,7 +12,7 @@ from utils.lib import MessageArgs from .. import babel, logger from ..data import ScheduleData as Data -from ..lib import slotid_to_utc +from ..lib import slotid_to_utc, vacuum_channel from ..settings import ScheduleSettings as Settings from ..settings import ScheduleConfig from ..ui.sessionui import SessionUI @@ -289,12 +289,16 @@ class ScheduledSession: Remove overwrites for non-members. """ async with self.lock: - if not (members := list(self.members.values())): - return if not (guild := self.guild): return if not (room := self.room_channel): + # Nothing to do + self.prepared = True + self.opened = True return + members = list(self.members.values()) + + t = self.bot.translator.t if room.permissions_for(guild.me) >= my_room_permissions: # Replace the member overwrites @@ -314,17 +318,36 @@ class ScheduledSession: if mobj: overwrites[mobj] = discord.PermissionOverwrite(connect=True, view_channel=True) try: - await room.edit(overwrites=overwrites) + await room.edit( + overwrites=overwrites, + reason=t(_p( + 'session|open|update_perms|audit_reason', + "Opening configured scheduled session room." + )) + ) except discord.HTTPException: logger.exception( f"Unhandled discord exception received while opening schedule session room {self!r}" ) else: logger.debug( - f"Opened schedule session room for session {self!r}" + f"Opened schedule session room for session {self!r} with overwrites {overwrites}" + ) + + # Cleanup members who should not be in the channel(s) + if room.type is discord.enums.ChannelType.category: + channels = room.voice_channels + else: + channels = [room] + for channel in channels: + await vacuum_channel( + channel, + reason=t(_p( + 'session|open|clean_room|audit_reason', + "Removing extra member from scheduled session room." + )) ) else: - t = self.bot.translator.t await self.send( t(_p( 'session|open|error:room_permissions', @@ -344,6 +367,8 @@ class ScheduledSession: await asyncio.sleep(ping_wait) except asyncio.CancelledError: return + + # Ghost ping alert for missing members missing = [mid for mid, m in self.members.items() if m.total_clock == 0 and m.clock_start is None] if missing: ping = ''.join(f"<@{mid}>" for mid in missing) @@ -361,6 +386,7 @@ class ScheduledSession: # In case we somehow left the guild in the meantime return + # DM alert for _still_ missing members missing = [mid for mid, m in self.members.items() if m.total_clock == 0 and m.clock_start is None] for mid in missing: member = self.guild.get_member(mid) diff --git a/src/modules/schedule/core/timeslot.py b/src/modules/schedule/core/timeslot.py index 200c3e1b..75a32f5c 100644 --- a/src/modules/schedule/core/timeslot.py +++ b/src/modules/schedule/core/timeslot.py @@ -19,7 +19,7 @@ from modules.economy.data import EconomyData, TransactionType from .. import babel, logger from ..data import ScheduleData as Data -from ..lib import slotid_to_utc, batchrun_per_second, limit_concurrency +from ..lib import slotid_to_utc, batchrun_per_second, limit_concurrency, vacuum_channel from ..settings import ScheduleSettings from .session import ScheduledSession @@ -439,6 +439,75 @@ class TimeSlot: f"Closed {len(sessions)} for scheduled session timeslot: {self!r}" ) + @log_wrap(action='Tidy session rooms') + async def tidy_rooms(self, sessions: list[ScheduledSession]): + """ + 'Tidy Up' after sessions have been closed. + + This cleans up permissions for sessions which do not have another running session, + and vacuums the channel. + + Somewhat temporary measure, + workaround for the design flaw that channel permissions are only updated during open, + and hence are never cleared unless there is a next session. + Limitations include not clearing after a manual close. + """ + t = self.bot.translator.t + + for session in sessions: + if not session.guild: + # Can no longer access the session guild, nothing to clean up + logger.debug(f"Not tidying {session!r} because guild gone.") + continue + if not (room := session.room_channel): + # Session did not have a room to clean up + logger.debug(f"Not tidying {session!r} because room channel gone.") + continue + if not session.opened or session.cancelled: + # Not an active session, don't try to tidy up + logger.debug(f"Not tidying {session!r} because cancelled or not opened.") + continue + if (active := self.cog.get_active_session(session.guild.id)) is not None: + # Rely on the active session to set permissions and vacuum channel + logger.debug(f"Not tidying {session!r} because guild has active session {active!r}.") + continue + logger.debug(f"Tidying {session!r}.") + + me = session.guild.me + if room.permissions_for(me).manage_roles: + overwrites = { + target: overwrite for target, overwrite in room.overwrites.items() + if not isinstance(target, discord.Member) + } + try: + await room.edit( + overwrites=overwrites, + reason=t(_p( + "session|closing|audit_reason", + "Removing previous scheduled session member permissions." + )) + ) + except discord.HTTPException: + logger.warning( + f"Unexpected exception occurred while tidying after sessions {session!r}", + exc_info=True + ) + else: + logger.debug(f"Updated room permissions while tidying {session!r}.") + if room.type is discord.enums.ChannelType.category: + channels = room.voice_channels + else: + channels = [room] + for channel in channels: + await vacuum_channel( + channel, + reason=t(_p( + "session|closing|disconnecting|audit_reason", + "Disconnecting previous scheduled session members." + )) + ) + logger.debug(f"Finished tidying {session!r}.") + def launch(self) -> asyncio.Task: self.run_task = asyncio.create_task(self.run(), name=f"TimeSlot {self.slotid}") return self.run_task @@ -475,6 +544,9 @@ class TimeSlot: logger.info(f"Active timeslot closing. {self!r}") await self.close(list(self.sessions.values()), consequences=True) logger.info(f"Active timeslot closed. {self!r}") + await asyncio.sleep(30) + await self.tidy_rooms(list(self.sessions.values())) + logger.info(f"Previous active timeslot tidied up. {self!r}") except asyncio.CancelledError: logger.info( f"Deactivating active time slot: {self!r}" diff --git a/src/modules/schedule/lib.py b/src/modules/schedule/lib.py index 3951aef3..27595961 100644 --- a/src/modules/schedule/lib.py +++ b/src/modules/schedule/lib.py @@ -1,9 +1,13 @@ import asyncio import itertools import datetime as dt +from typing import Optional -from . import logger, babel +import discord + +from meta.logger import log_wrap from utils.ratelimits import Bucket +from . import logger, babel _p, _np = babel._p, babel._np @@ -88,3 +92,24 @@ def format_until(t, distance): 'ui:schedule|format_until|now', "right now!" )) + + +@log_wrap(action='Vacuum Channel') +async def vacuum_channel(channel: discord.VoiceChannel, reason: Optional[str] = None): + """ + Launch disconnect tasks for each voice channel member who does not have permission to connect. + """ + me = channel.guild.me + if not channel.permissions_for(me).move_members: + # Nothing we can do + return + + to_remove = [member for member in channel.members if not channel.permissions_for(member).connect] + for member in to_remove: + # Disconnect member from voice + # Extra check here since members may come and go while we are trying to remove + if member in channel.members: + try: + await member.edit(voice_channel=None, reason=reason) + except discord.HTTPException: + pass