From 1586354b39c648d8f501698c60c7fa9f88cac65c Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 14 Oct 2023 01:08:12 +0300 Subject: [PATCH] feat(voice): Event logging for voice sessions. --- src/tracking/voice/cog.py | 22 +++++- src/tracking/voice/data.py | 6 ++ src/tracking/voice/session.py | 134 ++++++++++++++++++++++++++-------- 3 files changed, 130 insertions(+), 32 deletions(-) diff --git a/src/tracking/voice/cog.py b/src/tracking/voice/cog.py index 93d706fe..2b1645d8 100644 --- a/src/tracking/voice/cog.py +++ b/src/tracking/voice/cog.py @@ -505,10 +505,27 @@ class VoiceTrackerCog(LionCog): logger.debug( f"Scheduling voice session for member `{member.name}' " f"in guild '{member.guild.name}' " - f"in channel '{achannel}' . " + f"in channel '{achannel}' . " f"Session will start at {start}, expire at {expiry}, and confirm in {delay}." ) await session.schedule_start(delay, start, expiry, astate, hourly_rate) + + t = self.bot.translator.t + lguild = await self.bot.core.lions.fetch_guild(member.guild.id) + lguild.log_event( + t(_p( + 'eventlog|event:voice_session_start|title', + "Member Joined Tracked Voice Channel" + )), + t(_p( + 'eventlog|event:voice_session_start|desc', + "{member} joined {channel}." + )).format( + member=member.mention, channel=achannel.mention, + ), + start=discord.utils.format_dt(start, 'F'), + expiry=discord.utils.format_dt(expiry, 'R'), + ) elif session.activity: # If the channelid did not change, the live state must have # Recalculate the economy rate, and update the session @@ -584,7 +601,8 @@ class VoiceTrackerCog(LionCog): start_time = now delay = 20 - expiry = start_time + dt.timedelta(seconds=cap) + remaining = cap - studied_today + expiry = start_time + dt.timedelta(seconds=remaining) if expiry > tomorrow: expiry = tomorrow + dt.timedelta(seconds=cap) diff --git a/src/tracking/voice/data.py b/src/tracking/voice/data.py index 86c5e500..3b835231 100644 --- a/src/tracking/voice/data.py +++ b/src/tracking/voice/data.py @@ -7,6 +7,7 @@ from data import RowModel, Registry, Table from data.columns import Integer, String, Timestamp, Bool from core.data import CoreData +from utils.lib import utc_now class VoiceTrackerData(Registry): @@ -113,6 +114,11 @@ class VoiceTrackerData(Registry): live_video = Bool() hourly_coins = Integer() + @property + def _total_coins_earned(self): + since = (utc_now() - self.last_update).total_seconds() / 3600 + return self.coins_earned + since * self.hourly_coins + @classmethod @log_wrap(action='close_voice_session') async def close_study_session_at(cls, guildid: int, userid: int, _at: dt.datetime) -> int: diff --git a/src/tracking/voice/session.py b/src/tracking/voice/session.py index 37de1cdc..37b9e10b 100644 --- a/src/tracking/voice/session.py +++ b/src/tracking/voice/session.py @@ -12,7 +12,9 @@ from meta import LionBot from data import WeakCache from .data import VoiceTrackerData -from . import logger +from . import logger, babel + +_p = babel._p class TrackedVoiceState: @@ -243,20 +245,6 @@ class VoiceSession: delay = (expire_time - utc_now()).total_seconds() self.expiry_task = asyncio.create_task(self._expire_after(delay)) - async def _expire_after(self, delay: int): - """ - Expire a session which has exceeded the daily voice cap. - """ - # TODO: Logging, and guild logging, and user notification (?) - await asyncio.sleep(delay) - logger.info( - f"Expiring voice session for member in guild " - f"and channel ." - ) - # TODO: Would be better not to close the session and wipe the state - # Instead start a new PENDING session. - await self.close() - async def update(self, new_state: Optional[TrackedVoiceState] = None, new_rate: Optional[int] = None): """ Update the session state with the provided voice state or hourly rate. @@ -282,26 +270,95 @@ class VoiceSession: rate=self.hourly_rate ) + async def _expire_after(self, delay: int): + """ + Expire a session which has exceeded the daily voice cap. + """ + # TODO: Logging, and guild logging, and user notification (?) + await asyncio.sleep(delay) + logger.info( + f"Expiring voice session for member in guild " + f"and channel ." + ) + async with self.lock: + await self._close() + + if self.activity: + t = self.bot.translator.t + lguild = await self.bot.core.lions.fetch_guild(self.guildid) + if self.activity is SessionState.ONGOING and self.data is not None: + lguild.log_event( + t(_p( + 'eventlog|event:voice_session_expired|title', + "Member Voice Session Expired" + )), + t(_p( + 'eventlog|event:voice_session_expired|desc', + "{member}'s voice session in {channel} expired " + "because they reached the daily voice cap." + )).format( + member=f"<@{self.userid}>", + channel=f"<#{self.state.channelid}>", + ), + start=discord.utils.format_dt(self.data.start_time), + coins_earned=int(self.data._total_coins_earned), + ) + + if self.start_task is not None: + self.start_task.cancel() + self.start_task = None + + self.data = None + + cog = self.bot.get_cog('VoiceTrackerCog') + delay, start, expiry = await cog._session_boundaries_for(self.guildid, self.userid) + hourly_rate = await cog._calculate_rate(self.guildid, self.userid, self.state) + + self.hourly_rate = hourly_rate + self._start_time = start + + self.start_task = asyncio.create_task(self._start_after(delay, start)) + self.schedule_expiry(expiry) + async def close(self): """ Close the session, or cancel the pending session. Idempotent. """ async with self.lock: - if self.activity is SessionState.ONGOING: - # End the ongoing session - now = utc_now() - await self.data.close_study_session_at(self.guildid, self.userid, now) - - # TODO: Something a bit saner/safer.. dispatch the finished session instead? - self.bot.dispatch('voice_session_end', self.data, now) - - # Rank update - # TODO: Change to broadcasted event? - rank_cog = self.bot.get_cog('RankCog') - if rank_cog is not None: - asyncio.create_task(rank_cog.on_voice_session_complete( - (self.guildid, self.userid, int((utc_now() - self.data.start_time).total_seconds()), 0) - )) + await self._close() + if self.activity: + t = self.bot.translator.t + lguild = await self.bot.core.lions.fetch_guild(self.guildid) + if self.activity is SessionState.ONGOING and self.data is not None: + lguild.log_event( + t(_p( + 'eventlog|event:voice_session_closed|title', + "Member Voice Session Ended" + )), + t(_p( + 'eventlog|event:voice_session_closed|desc', + "{member} completed their voice session in {channel}." + )).format( + member=f"<@{self.userid}>", + channel=f"<#{self.state.channelid}>", + ), + start=discord.utils.format_dt(self.data.start_time), + coins_earned=int(self.data._total_coins_earned), + ) + else: + lguild.log_event( + t(_p( + 'eventlog|event:voice_session_cancelled|title', + "Member Voice Session Cancelled" + )), + t(_p( + 'eventlog|event:voice_session_cancelled|desc', + "{member} left {channel} before their voice session started." + )).format( + member=f"<@{self.userid}>", + channel=f"<#{self.state.channelid}>", + ), + ) if self.start_task is not None: self.start_task.cancel() @@ -319,3 +376,20 @@ class VoiceSession: # Always release strong reference to session (to allow garbage collection) self._active_sessions_[self.guildid].pop(self.userid) + + async def _close(self): + if self.activity is SessionState.ONGOING: + # End the ongoing session + now = utc_now() + await self.data.close_study_session_at(self.guildid, self.userid, now) + + # TODO: Something a bit saner/safer.. dispatch the finished session instead? + self.bot.dispatch('voice_session_end', self.data, now) + + # Rank update + # TODO: Change to broadcasted event? + rank_cog = self.bot.get_cog('RankCog') + if rank_cog is not None: + asyncio.create_task(rank_cog.on_voice_session_complete( + (self.guildid, self.userid, int((utc_now() - self.data.start_time).total_seconds()), 0) + ))