diff --git a/src/modules/config/dashboard.py b/src/modules/config/dashboard.py index cb62c35e..e6cb70cc 100644 --- a/src/modules/config/dashboard.py +++ b/src/modules/config/dashboard.py @@ -16,6 +16,7 @@ from modules.ranks.ui.config import RankDashboard from modules.pomodoro.settingui import TimerDashboard from modules.rooms.settingui import RoomDashboard from babel.settingui import LocaleDashboard +from modules.schedule.ui.settingui import ScheduleDashboard # from modules.statistics.settings import StatisticsConfigUI from . import babel, logger @@ -31,7 +32,8 @@ class GuildDashboard(BasePager): pages = [ (LocaleDashboard, EconomyDashboard, TasklistDashboard), (VoiceTrackerDashboard, TextTrackerDashboard, ), - (RankDashboard, TimerDashboard, RoomDashboard, ) + (RankDashboard, TimerDashboard, RoomDashboard, ), + (ScheduleDashboard,), ] def __init__(self, bot: LionBot, guild: discord.Guild, callerid: int, channelid: int, **kwargs): diff --git a/src/modules/schedule/cog.py b/src/modules/schedule/cog.py index f4196c10..e030b76a 100644 --- a/src/modules/schedule/cog.py +++ b/src/modules/schedule/cog.py @@ -87,7 +87,7 @@ class ScheduleCog(LionCog): slot.run_task.cancel() for session in slot.sessions.values(): if session._updater and not session._updater.done(): - session._update.cancel() + session._updater.cancel() if session._status_task and not session._status_task.done(): session._status_task.cancel() @@ -217,21 +217,23 @@ class ScheduleCog(LionCog): for bookingid in bookingids: await self._cancel_booking_active(*bookingid) - # Now delete from data - records = await self.data.ScheduleSessionMember.table.delete_where( - MULTIVALUE_IN( - ('slotid', 'guildid', 'userid'), - *bookingids + conn = await self.bot.db.get_connection() + async with conn.transaction(): + # Now delete from data + records = await self.data.ScheduleSessionMember.table.delete_where( + MULTIVALUE_IN( + ('slotid', 'guildid', 'userid'), + *bookingids + ) ) - ) - # Refund cancelled bookings - if refund: - maybe_tids = (record['book_transactionid'] for record in records) - tids = [tid for tid in maybe_tids if tid is not None] - if tids: - economy = self.bot.get_cog('Economy') - await economy.data.Transaction.refund_transactions(*tids) + # Refund cancelled bookings + if refund: + maybe_tids = (record['book_transactionid'] for record in records) + tids = [tid for tid in maybe_tids if tid is not None] + if tids: + economy = self.bot.get_cog('Economy') + await economy.data.Transaction.refund_transactions(*tids) finally: for lock in locks: lock.release() @@ -240,6 +242,7 @@ class ScheduleCog(LionCog): ) return records + @log_wrap(action='Cancel Active Booking') async def _cancel_booking_active(self, slotid, guildid, userid): """ Booking cancel worker for active slots. @@ -259,7 +262,7 @@ class ScheduleCog(LionCog): return async with session.lock: # Update message if it has already been sent - session.update_message_soon(resend=False) + session.update_status_soon(resend=False) room = session.room_channel member = session.guild.get_member(userid) if room else None if room and member and session.prepared: @@ -564,7 +567,7 @@ class ScheduleCog(LionCog): if new_roles: # This should be in cache in the vast majority of cases guild_data = await self.data.ScheduleGuild.fetch(guild.id) - if (roleid := guild_data.blacklist_role) is not None and roleid in new_roles: + if guild_data and (roleid := guild_data.blacklist_role) is not None and roleid in new_roles: # Clear member schedule await self.clear_member_schedule(guild.id, after.id) diff --git a/src/modules/schedule/core/session.py b/src/modules/schedule/core/session.py index 5d095891..86ecd382 100644 --- a/src/modules/schedule/core/session.py +++ b/src/modules/schedule/core/session.py @@ -5,6 +5,7 @@ import asyncio import discord from meta import LionBot +from meta.logger import log_wrap from utils.lib import utc_now from utils.lib import MessageArgs @@ -79,6 +80,7 @@ class ScheduledSession: self._last_update = None self._updater = None self._status_task = None + self._notify_task = None def __repr__(self): return ' '.join(( @@ -193,6 +195,7 @@ class ScheduledSession: if hook: return hook.as_webhook(client=self.bot) + @log_wrap(action='Lobby Send') async def send(self, *args, wait=True, **kwargs): lobby_hook = await self.get_lobby_hook() if lobby_hook: @@ -209,6 +212,7 @@ class ScheduledSession: exc_info=True ) + @log_wrap(action='Session Prepare') async def prepare(self, **kwargs): """ Execute prepare stage for this guild. @@ -218,6 +222,7 @@ class ScheduledSession: await self.update_status(**kwargs) self.prepared = True + @log_wrap(action='Prepare Room') async def prepare_room(self): """ Add overwrites allowing current members to connect. @@ -258,6 +263,7 @@ class ScheduledSession: )).format(room=room.mention) ) + @log_wrap(action='Open Room') async def open_room(self): """ Remove overwrites for non-members. @@ -302,10 +308,15 @@ class ScheduledSession: self.prepared = True self.opened = True - async def notify(self): + @log_wrap(action='Notify') + async def _notify(self, wait=60): """ Ghost ping members who have not yet attended. """ + try: + await asyncio.sleep(wait) + except asyncio.CancelledError: + return 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) @@ -313,6 +324,12 @@ class ScheduledSession: if message is not None: asyncio.create_task(message.delete()) + def notify(self): + """ + Trigger notify after one minute. + """ + self._notify_task = asyncio.create_task(self._notify()) + async def current_status(self) -> MessageArgs: """ Lobby status message args. @@ -474,6 +491,7 @@ class ScheduledSession: args = MessageArgs(embed=embed, view=view) return args + @log_wrap(action='Update Status') async def _update_status(self, save=True, resend=True): """ Send or update the lobby message. @@ -530,6 +548,7 @@ class ScheduledSession: self._status_task.cancel() await self._update_status(**kwargs) + @log_wrap(action='Status Loop') async def update_loop(self): """ Keep the lobby message up to date with a message per minute. diff --git a/src/modules/schedule/core/timeslot.py b/src/modules/schedule/core/timeslot.py index f2849c34..cc65f377 100644 --- a/src/modules/schedule/core/timeslot.py +++ b/src/modules/schedule/core/timeslot.py @@ -297,11 +297,10 @@ class TimeSlot: for session in sessions if session.lobby_channel is not None ] - notify_tasks = [ - asyncio.create_task(session.notify()) - for session in fresh - if session.lobby_channel is not None and session.data.opened_at is None - ] + # Trigger notify tasks + for session in fresh: + if session.lobby_channel is not None: + session.notify() # Start lobby update loops for session in sessions: @@ -317,7 +316,6 @@ class TimeSlot: async for task in limit_concurrency(voice_coros, 5): await task await asyncio.gather(*message_tasks) - await asyncio.gather(*notify_tasks) # Write opened if fresh: diff --git a/src/modules/schedule/settings.py b/src/modules/schedule/settings.py index b0d8357b..bc996005 100644 --- a/src/modules/schedule/settings.py +++ b/src/modules/schedule/settings.py @@ -98,13 +98,15 @@ class ScheduleSettings(SettingGroup): "During (and slightly before) each scheduled session, all members who have booked the session " "will be given permission to join the voice channel (via permission overwrites). " "I require the `MANAGE_CHANNEL`, `MANAGE_PERMISSIONS`, `CONNECT`, and `VIEW_CHANNEL` permissions " - "in this channel, and my highest role must be higher than all permission overwrites set in the channel." + "in this channel, and my highest role must be higher than all permission overwrites set in the channel. " + "Furthermore, if this is set to a *category* channel, then the permission overwrites will apply " + "to all *synced* channels under the category, as usual." ) _accepts = _p( 'guildset:session_room|accepts', "Name or id of the session room voice channel." ) - channel_types = [discord.VoiceChannel] + channel_types = [discord.VoiceChannel, discord.CategoryChannel] _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.room_channel.name diff --git a/src/modules/schedule/ui/settingui.py b/src/modules/schedule/ui/settingui.py index f0799799..a5ea69b6 100644 --- a/src/modules/schedule/ui/settingui.py +++ b/src/modules/schedule/ui/settingui.py @@ -22,8 +22,8 @@ class ScheduleSettingUI(ConfigUI): ScheduleSettings.SessionLobby, ScheduleSettings.SessionRoom, ScheduleSettings.SessionChannels, - ScheduleSettings.ScheduleCost, ), ( + ScheduleSettings.ScheduleCost, ScheduleSettings.AttendanceReward, ScheduleSettings.AttendanceBonus, ScheduleSettings.MinAttendance, @@ -89,7 +89,7 @@ class ScheduleSettingUI(ConfigUI): )) # Room channel selector - @select(cls=ChannelSelect, channel_types=[discord.ChannelType.voice], + @select(cls=ChannelSelect, channel_types=[discord.ChannelType.category, discord.ChannelType.voice], min_values=0, max_values=1, placeholder='ROOM_PLACEHOLDER') async def room_menu(self, selection: discord.Interaction, selected: ChannelSelect):