feat(voice): Event logging for voice sessions.

This commit is contained in:
2023-10-14 01:08:12 +03:00
parent 7b6290b73e
commit 1586354b39
3 changed files with 130 additions and 32 deletions

View File

@@ -505,10 +505,27 @@ class VoiceTrackerCog(LionCog):
logger.debug( logger.debug(
f"Scheduling voice session for member `{member.name}' <uid:{member.id}> " f"Scheduling voice session for member `{member.name}' <uid:{member.id}> "
f"in guild '{member.guild.name}' <gid: member.guild.id> " f"in guild '{member.guild.name}' <gid: member.guild.id> "
f"in channel '{achannel}' <cid: {after.channel.id}>. " f"in channel '{achannel}' <cid: {achannel.id}>. "
f"Session will start at {start}, expire at {expiry}, and confirm in {delay}." f"Session will start at {start}, expire at {expiry}, and confirm in {delay}."
) )
await session.schedule_start(delay, start, expiry, astate, hourly_rate) 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: elif session.activity:
# If the channelid did not change, the live state must have # If the channelid did not change, the live state must have
# Recalculate the economy rate, and update the session # Recalculate the economy rate, and update the session
@@ -584,7 +601,8 @@ class VoiceTrackerCog(LionCog):
start_time = now start_time = now
delay = 20 delay = 20
expiry = start_time + dt.timedelta(seconds=cap) remaining = cap - studied_today
expiry = start_time + dt.timedelta(seconds=remaining)
if expiry > tomorrow: if expiry > tomorrow:
expiry = tomorrow + dt.timedelta(seconds=cap) expiry = tomorrow + dt.timedelta(seconds=cap)

View File

@@ -7,6 +7,7 @@ from data import RowModel, Registry, Table
from data.columns import Integer, String, Timestamp, Bool from data.columns import Integer, String, Timestamp, Bool
from core.data import CoreData from core.data import CoreData
from utils.lib import utc_now
class VoiceTrackerData(Registry): class VoiceTrackerData(Registry):
@@ -113,6 +114,11 @@ class VoiceTrackerData(Registry):
live_video = Bool() live_video = Bool()
hourly_coins = Integer() 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 @classmethod
@log_wrap(action='close_voice_session') @log_wrap(action='close_voice_session')
async def close_study_session_at(cls, guildid: int, userid: int, _at: dt.datetime) -> int: async def close_study_session_at(cls, guildid: int, userid: int, _at: dt.datetime) -> int:

View File

@@ -12,7 +12,9 @@ from meta import LionBot
from data import WeakCache from data import WeakCache
from .data import VoiceTrackerData from .data import VoiceTrackerData
from . import logger from . import logger, babel
_p = babel._p
class TrackedVoiceState: class TrackedVoiceState:
@@ -243,20 +245,6 @@ class VoiceSession:
delay = (expire_time - utc_now()).total_seconds() delay = (expire_time - utc_now()).total_seconds()
self.expiry_task = asyncio.create_task(self._expire_after(delay)) 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 <uid:{self.userid}> in guild <gid:{self.guildid}> "
f"and channel <cid:{self.state.channelid}>."
)
# 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): 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. Update the session state with the provided voice state or hourly rate.
@@ -282,26 +270,95 @@ class VoiceSession:
rate=self.hourly_rate 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 <uid:{self.userid}> in guild <gid:{self.guildid}> "
f"and channel <cid:{self.state.channelid}>."
)
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): async def close(self):
""" """
Close the session, or cancel the pending session. Idempotent. Close the session, or cancel the pending session. Idempotent.
""" """
async with self.lock: async with self.lock:
if self.activity is SessionState.ONGOING: await self._close()
# End the ongoing session if self.activity:
now = utc_now() t = self.bot.translator.t
await self.data.close_study_session_at(self.guildid, self.userid, now) lguild = await self.bot.core.lions.fetch_guild(self.guildid)
if self.activity is SessionState.ONGOING and self.data is not None:
# TODO: Something a bit saner/safer.. dispatch the finished session instead? lguild.log_event(
self.bot.dispatch('voice_session_end', self.data, now) t(_p(
'eventlog|event:voice_session_closed|title',
# Rank update "Member Voice Session Ended"
# TODO: Change to broadcasted event? )),
rank_cog = self.bot.get_cog('RankCog') t(_p(
if rank_cog is not None: 'eventlog|event:voice_session_closed|desc',
asyncio.create_task(rank_cog.on_voice_session_complete( "{member} completed their voice session in {channel}."
(self.guildid, self.userid, int((utc_now() - self.data.start_time).total_seconds()), 0) )).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: if self.start_task is not None:
self.start_task.cancel() self.start_task.cancel()
@@ -319,3 +376,20 @@ class VoiceSession:
# Always release strong reference to session (to allow garbage collection) # Always release strong reference to session (to allow garbage collection)
self._active_sessions_[self.guildid].pop(self.userid) 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)
))