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(
f"Scheduling voice session for member `{member.name}' <uid:{member.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}."
)
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)

View File

@@ -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:

View File

@@ -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 <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):
"""
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 <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):
"""
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)
))