refactor: Update study module structure.
Prepare `study` module for session and timer systems. Move regular sync to the studybadge loop.
This commit is contained in:
4
bot/modules/study/tracking/__init__.py
Normal file
4
bot/modules/study/tracking/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import data
|
||||
from . import settings
|
||||
from . import time_tracker
|
||||
from . import session_tracker
|
||||
3
bot/modules/study/tracking/data.py
Normal file
3
bot/modules/study/tracking/data.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from data import Table
|
||||
|
||||
untracked_channels = Table('untracked_channels')
|
||||
0
bot/modules/study/tracking/session_tracker.py
Normal file
0
bot/modules/study/tracking/session_tracker.py
Normal file
113
bot/modules/study/tracking/settings.py
Normal file
113
bot/modules/study/tracking/settings.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import settings
|
||||
from settings import GuildSettings
|
||||
from wards import guild_admin
|
||||
|
||||
from .data import untracked_channels
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class untracked_channels(settings.ChannelList, settings.ListData, settings.Setting):
|
||||
category = "Study Tracking"
|
||||
|
||||
attr_name = 'untracked_channels'
|
||||
|
||||
_table_interface = untracked_channels
|
||||
_setting = settings.VoiceChannel
|
||||
|
||||
_id_column = 'guildid'
|
||||
_data_column = 'channelid'
|
||||
|
||||
write_ward = guild_admin
|
||||
display_name = "untracked_channels"
|
||||
desc = "Channels to ignore for study time tracking."
|
||||
|
||||
_force_unique = True
|
||||
|
||||
long_desc = (
|
||||
"Time spent in these voice channels won't add study time or lioncoins to the member."
|
||||
)
|
||||
|
||||
# Flat cache, no need to expire objects
|
||||
_cache = {}
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
if self.value:
|
||||
return "The untracked channels have been updated:\n{}".format(self.formatted)
|
||||
else:
|
||||
return "Study time will now be counted in all channels."
|
||||
|
||||
@classmethod
|
||||
async def launch_task(cls, client):
|
||||
"""
|
||||
Launch initialisation step for the `untracked_channels` setting.
|
||||
|
||||
Pre-fill cache for the guilds with currently active voice channels.
|
||||
"""
|
||||
active_guildids = [
|
||||
guild.id
|
||||
for guild in client.guilds
|
||||
if any(channel.members for channel in guild.voice_channels)
|
||||
]
|
||||
if active_guildids:
|
||||
rows = cls._table_interface.select_where(
|
||||
guildid=active_guildids
|
||||
)
|
||||
cache = defaultdict(list)
|
||||
for row in rows:
|
||||
cache[row['guildid']].append(row['channelid'])
|
||||
cls._cache.update(cache)
|
||||
client.log(
|
||||
"Cached {} untracked channels for {} active guilds.".format(
|
||||
len(rows),
|
||||
len(cache)
|
||||
),
|
||||
context="UNTRACKED_CHANNELS"
|
||||
)
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class hourly_reward(settings.Integer, settings.GuildSetting):
|
||||
category = "Study Tracking"
|
||||
|
||||
attr_name = "hourly_reward"
|
||||
_data_column = "study_hourly_reward"
|
||||
|
||||
display_name = "hourly_reward"
|
||||
desc = "Number of LionCoins given per hour of study."
|
||||
|
||||
_default = 50
|
||||
|
||||
long_desc = (
|
||||
"Each spent in a voice channel will reward this number of LionCoins."
|
||||
)
|
||||
_accepts = "An integer number of LionCoins to reward."
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
return "Members will be rewarded `{}` LionCoins per hour of study.".format(self.formatted)
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class hourly_live_bonus(settings.Integer, settings.GuildSetting):
|
||||
category = "Study Tracking"
|
||||
|
||||
attr_name = "hourly_live_bonus"
|
||||
_data_column = "study_hourly_live_bonus"
|
||||
|
||||
display_name = "hourly_live_bonus"
|
||||
desc = "Number of extra LionCoins given for a full hour of streaming (via go live or video)."
|
||||
|
||||
_default = 10
|
||||
|
||||
long_desc = (
|
||||
"LionCoin bonus earnt for every hour a member streams in a voice channel, including video. "
|
||||
"This is in addition to the standard `hourly_reward`."
|
||||
)
|
||||
_accepts = "An integer number of LionCoins to reward."
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
return "Members will be rewarded an extra `{}` LionCoins per hour if they stream.".format(self.formatted)
|
||||
109
bot/modules/study/tracking/time_tracker.py
Normal file
109
bot/modules/study/tracking/time_tracker.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import itertools
|
||||
import traceback
|
||||
import logging
|
||||
import asyncio
|
||||
from time import time
|
||||
|
||||
from meta import client
|
||||
from core import Lion
|
||||
|
||||
from ..module import module
|
||||
from .settings import untracked_channels, hourly_reward, hourly_live_bonus
|
||||
|
||||
|
||||
last_scan = {} # guildid -> timestamp
|
||||
|
||||
|
||||
def _scan(guild):
|
||||
"""
|
||||
Scan the tracked voice channels and add time and coins to each user.
|
||||
"""
|
||||
# Current timestamp
|
||||
now = time()
|
||||
|
||||
# Get last scan timestamp
|
||||
try:
|
||||
last = last_scan[guild.id]
|
||||
except KeyError:
|
||||
return
|
||||
finally:
|
||||
last_scan[guild.id] = now
|
||||
|
||||
# Calculate time since last scan
|
||||
interval = now - last
|
||||
|
||||
# Discard if it has been more than 20 minutes (discord outage?)
|
||||
if interval > 60 * 20:
|
||||
return
|
||||
|
||||
untracked = untracked_channels.get(guild.id).data
|
||||
guild_hourly_reward = hourly_reward.get(guild.id).data
|
||||
guild_hourly_live_bonus = hourly_live_bonus.get(guild.id).data
|
||||
|
||||
channel_members = (
|
||||
channel.members for channel in guild.voice_channels if channel.id not in untracked
|
||||
)
|
||||
|
||||
members = itertools.chain(*channel_members)
|
||||
# TODO filter out blacklisted users
|
||||
|
||||
blacklist = client.objects['blacklisted_users']
|
||||
guild_blacklist = client.objects['ignored_members'][guild.id]
|
||||
|
||||
for member in members:
|
||||
if member.bot:
|
||||
continue
|
||||
if member.id in blacklist or member.id in guild_blacklist:
|
||||
continue
|
||||
lion = Lion.fetch(guild.id, member.id)
|
||||
|
||||
# Add time
|
||||
lion.addTime(interval, flush=False)
|
||||
|
||||
# Add coins
|
||||
hour_reward = guild_hourly_reward
|
||||
if member.voice.self_stream or member.voice.self_video:
|
||||
hour_reward += guild_hourly_live_bonus
|
||||
|
||||
lion.addCoins(hour_reward * interval / (3600), flush=False)
|
||||
|
||||
|
||||
async def _study_tracker():
|
||||
"""
|
||||
Scanner launch loop.
|
||||
"""
|
||||
while True:
|
||||
while not client.is_ready():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# Launch scanners on each guild
|
||||
for guild in client.guilds:
|
||||
# Short wait to pass control to other asyncio tasks if they need it
|
||||
await asyncio.sleep(0)
|
||||
try:
|
||||
# Scan the guild
|
||||
_scan(guild)
|
||||
except Exception:
|
||||
# Unknown exception. Catch it so the loop doesn't die.
|
||||
client.log(
|
||||
"Error while scanning guild '{}'(gid:{})! "
|
||||
"Exception traceback follows.\n{}".format(
|
||||
guild.name,
|
||||
guild.id,
|
||||
traceback.format_exc()
|
||||
),
|
||||
context="VOICE_ACTIVITY_SCANNER",
|
||||
level=logging.ERROR
|
||||
)
|
||||
|
||||
|
||||
@module.launch_task
|
||||
async def launch_study_tracker(client):
|
||||
# First pre-load the untracked channels
|
||||
await untracked_channels.launch_task(client)
|
||||
asyncio.create_task(_study_tracker())
|
||||
|
||||
|
||||
# TODO: Logout handler, sync
|
||||
Reference in New Issue
Block a user