diff --git a/bot/modules/study/tracking/session_tracker.py b/bot/modules/study/tracking/session_tracker.py index e44a2bc5..fb9b9341 100644 --- a/bot/modules/study/tracking/session_tracker.py +++ b/bot/modules/study/tracking/session_tracker.py @@ -1,14 +1,23 @@ import asyncio import discord +import logging +import traceback from collections import defaultdict from utils.lib import utc_now +from data import tables +from core import Lion + from ..module import module -from .data import current_sessions +from .data import current_sessions, SessionChannelType from .settings import untracked_channels, hourly_reward, hourly_live_bonus, max_daily_study class Session: + """ + A `Session` is a guild member that is currently studying (i.e. that is in a tracked voice channel). + This class acts as an opaque interface to the corresponding `sessions` data row. + """ # TODO: Slots sessions = defaultdict(dict) @@ -37,13 +46,20 @@ class Session: if userid in cls.sessions[guildid]: raise ValueError("A session for this member already exists!") # TODO: Handle daily study cap - # TODO: Calculate channel type - # TODO: Ensure lion + + # TODO: More reliable channel type determination + if state.channel.id in tables.rented.row_cache: + channel_type = SessionChannelType.RENTED + elif state.channel.id in tables.accountability_rooms.row_cache: + channel_type = SessionChannelType.ACCOUNTABILITY + else: + channel_type = SessionChannelType.STANDARD + current_sessions.create_row( guildid=guildid, userid=userid, channelid=state.channel.id, - channel_type=None, + channel_type=channel_type, start_time=now, live_start=now if (state.self_video or state.self_stream) else None, stream_start=now if state.self_stream else None, @@ -101,6 +117,7 @@ async def session_voice_tracker(client, member, before, after): Voice update event dispatcher for study session tracking. """ guild = member.guild + Lion.fetch(guild.id, member.id) session = Session.get(guild.id, member.id) if before.channel == after.channel: @@ -112,6 +129,7 @@ async def session_voice_tracker(client, member, before, after): # Member changed channel # End the current session and start a new one, if applicable # TODO: Max daily study session tasks + # TODO: Error if before is None but we have a current session if session: # End the current session session.finish() @@ -135,8 +153,108 @@ async def _init_session_tracker(client): update them depending on the current voice states, and attach the voice event handler. """ + # Ensure the client caches are ready and guilds are chunked await client.wait_until_ready() + + # Pre-cache the untracked channels await untracked_channels.launch_task(client) + + # Log init start and define logging counters + client.log( + "Loading ongoing study sessions.", + context="SESSION_INIT", + level=logging.DEBUG + ) + resumed = 0 + ended = 0 + + # Grab all ongoing sessions from data + rows = current_sessions.fetch_rows_where() + + # Iterate through, resume or end as needed + for row in rows: + if (guild := client.get_guild(row.guildid)) is not None and row.channelid is not None: + try: + # Load the Session + session = Session(row.guildid, row.userid) + + # Find the channel and member voice state + voice = None + if channel := guild.get_channel(row.channelid): + voice = next((member.voice for member in channel.members if member.id == row.userid), None) + + # Resume or end as required + if voice and voice.channel: + client.log( + "Resuming ongoing session: {}".format(row), + context="SESSION_INIT", + level=logging.DEBUG + ) + Session.sessions[row.guildid][row.userid] = session + session.save_live_status(voice) + resumed += 1 + else: + client.log( + "Ending already completed session: {}".format(row), + context="SESSION_INIT", + level=logging.DEBUG + ) + session.finish() + ended += 1 + except Exception: + # Fatal error + client.log( + "Fatal error occurred initialising session: {}\n{}".format(row, traceback.format_exc()), + context="SESSION_INIT", + level=logging.CRITICAL + ) + module.ready = False + return + + # Log resumed sessions + client.log( + "Resumed {} ongoing study sessions, and ended {}.".format(resumed, ended), + context="SESSION_INIT", + level=logging.INFO + ) + + # Now iterate through members of all tracked voice channels + # Start sessions if they don't already exist + tracked_channels = [ + channel + for guild in client.guilds + for channel in guild.voice_channels + if channel.members and channel.id not in untracked_channels.get(guild.id).data + ] + new_members = [ + member + for channel in tracked_channels + for member in channel.members + if not Session.get(member.guild.id, member.id) + ] + for member in new_members: + client.log( + "Starting new session for '{}' (uid: {}) in '{}' (cid: {}) of '{}' (gid: {})".format( + member.name, + member.id, + member.voice.channel.name, + member.voice.channel.id, + member.guild.name, + member.guild.id + ), + context="SESSION_INIT", + level=logging.DEBUG + ) + Session.start(member, member.voice) + + # Log newly started sessions + client.log( + "Started {} new study sessions from current voice channel members.".format(len(new_members)), + context="SESSION_INIT", + level=logging.INFO + ) + + # Now that we are in a valid initial state, attach the session event handler client.add_after_event("voice_state_update", session_voice_tracker)