From aebd5375c728704ac9627bfe0e3135d90a73cded Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 8 Jul 2023 12:46:49 +0300 Subject: [PATCH] fix (schedule): Setting tweaks and more bugfixes. Add the schedule dashboard to the config dashboard. Move the schedule cost to the second page. Allow categories to be selected for the session room. Fix an issue where blacklist role would be checked with no guild data. Fix typos in session unloading. Delay session notification. More logging. --- src/modules/config/dashboard.py | 4 ++- src/modules/schedule/cog.py | 35 +++++++++++++++------------ src/modules/schedule/core/session.py | 21 +++++++++++++++- src/modules/schedule/core/timeslot.py | 10 +++----- src/modules/schedule/settings.py | 6 +++-- src/modules/schedule/ui/settingui.py | 4 +-- 6 files changed, 52 insertions(+), 28 deletions(-) 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):