feat(voice): Event logging for voice sessions.
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user