rewrite (voice sessions): Voice session tracker.

This commit is contained in:
2023-03-08 11:54:56 +02:00
parent 5bf3ecbdfd
commit aa174d8a1d
8 changed files with 1729 additions and 14 deletions

View File

@@ -0,0 +1,433 @@
from typing import Optional
from collections import defaultdict
import discord
from discord.ui.select import select, Select, ChannelSelect
from discord.ui.button import button, Button, ButtonStyle
from settings.groups import SettingGroup
from settings.data import ModelData, ListData
from settings.setting_types import ChannelListSetting, IntegerSetting, DurationSetting
from meta import conf, LionBot
from meta.sharding import THIS_SHARD
from meta.logger import log_wrap
from utils.ui import LeoUI
from core.data import CoreData
from core.lion_guild import VoiceMode
from babel.translator import ctx_translator
from . import babel, logger
from .data import VoiceTrackerData
_p = babel._p
# untracked channels
# hourly_reward
# hourly_live_bonus
# daily_voice_cap
class VoiceTrackerSettings(SettingGroup):
class UntrackedChannels(ListData, ChannelListSetting):
# TODO: Factor out into combined tracking settings?
setting_id = 'untracked_channels'
_event = 'guild_setting_update_untracked_channels'
_display_name = _p('guildset:untracked_channels', "untracked_channels")
_desc = _p(
'guildset:untracked_channels|desc',
"Channels which will be ignored for statistics tracking."
)
_long_desc = _p(
'guildset:untracked_channels|long_desc',
"Activity in these channels will not count towards a member's statistics. "
"If a category is selected, all channels under the category will be untracked."
)
_default = None
_table_interface = VoiceTrackerData.untracked_channels
_id_column = 'guildid'
_data_column = 'channelid'
_order_column = 'channelid'
_cache = {}
@property
def set_str(self):
t = ctx_translator.get().t
return t(_p(
'guildset:untracked_channels|set',
"Channel selector below."
))
@property
def update_message(self):
t = ctx_translator.get().t
return t(_p(
'guildset:untracked_channels|response',
"Activity in the following channels will now be ignored: {channels}"
)).format(
channels=self.formatted
)
@classmethod
@log_wrap(action='Cache Untracked Channels')
async def setup(cls, bot):
"""
Pre-load untracked channels for every guild on the current shard.
"""
data: VoiceTrackerData = bot.db.registries['VoiceTrackerData']
# TODO: Filter by joining on guild_config with last_left = NULL
# Otherwise we are also caching all the guilds we left
rows = await data.untracked_channels.select_where(THIS_SHARD)
new_cache = defaultdict(list)
count = 0
for row in rows:
new_cache[row['guildid']].append(row['channelid'])
count += 1
cls._cache.clear()
cls._cache.update(new_cache)
logger.info(f"Loaded {count} untracked channels on this shard.")
class HourlyReward(ModelData, IntegerSetting):
setting_id = 'hourly_reward'
_event = 'guild_setting_update_hourly_reward'
_display_name = _p('guildset:hourly_reward', "hourly_reward")
_desc = _p(
'guildset:hourly_reward|mode:voice|desc',
"LionCoins given per hour in a voice channel."
)
_default = 50
_min = 0
_max = 2**15
_model = CoreData.Guild
_column = CoreData.Guild.study_hourly_reward.name
@classmethod
def _format_data(cls, parent_id, data, **kwargs):
t = ctx_translator.get().t
if data is not None:
return t(_p(
'guildset:hourly_reward|formatted',
"{coin}**{amount}** per hour."
)).format(
coin=conf.emojis.coin,
amount=data
)
@property
def set_str(self):
# TODO: Dynamic retrieval of command id
return '</configure voice_tracking:1038560947666694144>'
class HourlyReward_Voice(HourlyReward):
"""
Voice-mode specialised version of HourlyReward
"""
_desc = _p(
'guildset:hourly_reward|mode:voice|desc',
"LionCoins given per hour in a voice channel."
)
_long_desc = _p(
'guildset:hourly_reward|mode:voice|long_desc',
"Number of LionCoins to each member per hour that they stay in a tracked voice channel."
)
@property
def set_str(self):
# TODO: Dynamic retrieval of command id
return '</configure voice_tracking:1038560947666694144>'
@property
def update_message(self):
t = ctx_translator.get().t
return t(_p(
'guildset:hourly_reward|mode:voice|response',
"Members will be given {coin}**{amount}** per hour in a voice channel!"
)).format(
coin=conf.emojis.coin,
amount=self.data
)
class HourlyReward_Study(HourlyReward):
"""
Study-mode specialised version of HourlyReward.
"""
_desc = _p(
'guildset:hourly_reward|mode:study|desc',
"LionCoins given per hour of study."
)
_long_desc = _p(
'guildset:hourly_reward|mode:study|long_desc',
"Number of LionCoins given per hour of study, up to the daily hour cap."
)
@property
def update_message(self):
t = ctx_translator.get().t
return t(_p(
'guildset:hourly_reward|mode:study|response',
"Members will be given {coin}**{amount}** per hour that they study!"
)).format(
coin=conf.emojis.coin,
amount=self.data
)
class HourlyLiveBonus(ModelData, IntegerSetting):
"""
Guild setting describing the per-hour LionCoin bonus given to "live" members during tracking.
"""
setting_id = 'hourly_live_bonus'
_event = 'guild_setting_update_hourly_live_bonus'
_display_name = _p('guildset:hourly_live_bonus', "hourly_live_bonus")
_desc = _p(
'guildset:hourly_live_bonus|desc',
"Bonus Lioncoins given per hour when a member streams or video-chats."
)
_long_desc = _p(
'guildset:hourly_live_bonus|long_desc',
"When a member streams or video-chats in a channel they will be given this bonus *additionally* "
"to the `hourly_reward`."
)
_default = 150
_min = 0
_max = 2**15
_model = CoreData.Guild
_column = CoreData.Guild.study_hourly_live_bonus.name
@classmethod
def _format_data(cls, parent_id, data, **kwargs):
t = ctx_translator.get().t
if data is not None:
return t(_p(
'guildset:hourly_live_bonus|formatted',
"{coin}**{amount}** bonus per hour when live."
)).format(
coin=conf.emojis.coin,
amount=data
)
@property
def set_str(self):
# TODO: Dynamic retrieval of command id
return '</configure voice_tracking:1038560947666694144>'
@property
def update_message(self):
t = ctx_translator.get().t
return t(_p(
'guildset:hourly_live_bonus|response',
"Live members will now *additionally* be given {coin}**{amount}** per hour."
)).format(
coin=conf.emojis.coin,
amount=self.data
)
class DailyVoiceCap(ModelData, DurationSetting):
setting_id = 'daily_voice_cap'
_event = 'guild_setting_update_daily_voice_cap'
_display_name = _p('guildset:daily_voice_cap', "daily_voice_cap")
_desc = _p(
'guildset:daily_voice_cap|desc',
"Maximum number of hours per day to count for each member."
)
_long_desc = _p(
'guildset:daily_voice_cap|long_desc',
"Time spend in voice channels over this amount will not be tracked towards the member's statistics. "
"Tracking will resume at the start of the next day. "
"The start of the day is determined by the configured guild timezone."
)
_default = 16 * 60 * 60
_default_multiplier = 60 * 60
_max = 60 * 60 * 25
_model = CoreData.Guild
_column = CoreData.Guild.daily_study_cap.name
@property
def set_str(self):
# TODO: Dynamic retrieval of command id
return '</configure voice_tracking:1038560947666694144>'
@property
def update_message(self):
t = ctx_translator.get().t
return t(_p(
'guildset:daily_voice_cap|response',
"Members will be tracked for at most {duration} per day. "
"(**NOTE:** This will not affect members currently in voice channels.)"
)).format(
duration=self.formatted
)
class VoiceTrackerConfigUI(LeoUI):
# TODO: Bulk edit
# TODO: Cohesive exit
# TODO: Back to main configuration panel
_listening = {}
def __init__(self, bot: LionBot, settings: VoiceTrackerSettings, guildid: int, channelid: int, **kwargs):
super().__init__(**kwargs)
self.bot = bot
self.settings = settings
self.guildid = guildid
self.channelid = channelid
self._original: Optional[discord.Interaction] = None
self._message: Optional[discord.Message] = None
self.hourly_reward: Optional[VoiceTrackerSettings.HourlyReward] = None
self.hourly_live_bonus: Optional[VoiceTrackerSettings.HourlyLiveBonus] = None
self.daily_voice_cap: Optional[VoiceTrackerSettings.DailyVoiceCap] = None
self.untracked_channels: Optional[VoiceTrackerSettings.UntrackedChannels] = None
self.embed: Optional[discord.Embed] = None
@property
def instances(self):
return (self.hourly_reward, self.hourly_live_bonus, self.daily_voice_cap, self.untracked_channels)
async def cleanup(self):
self._listening.pop(self.channelid, None)
for instance in self.instances:
instance.deregister_callback(self.id)
try:
if self._original is not None:
await self._original.delete_original_response()
self._original = None
if self._message is not None:
await self._message.delete()
self._message = None
except discord.HTTPException:
# Interaction is likely expired or invalid, or some form of comms issue
pass
@button(label='CLOSE')
async def close_button(self, interaction: discord.Interaction, pressed):
await interaction.response.defer()
await self.close()
async def refresh_close_button(self):
t = self.bot.translator.t
self.close_button.label = t(_p('ui:voice_tracker_config|button:close|label', "Close"))
@button(label='RESET')
async def reset_button(self, interaction: discord.Interaction, pressed):
await interaction.response.defer()
for instance in self.instances:
instance.data = None
await instance.write()
await self.reload()
async def refresh_reset_button(self):
t = self.bot.translator.t
self.reset_button.label = t(_p('ui:voice_tracker_config|button:reset|label', "Reset"))
@select(cls=ChannelSelect, placeholder='UNTRACKED_CHANNEL_MENU', min_values=0, max_values=25)
async def untracked_channels_menu(self, interaction: discord.Interaction, selected):
await interaction.response.defer()
self.untracked_channels.value = selected.values
await self.untracked_channels.write()
await self.reload()
async def refresh_untracked_channels_menu(self):
t = self.bot.translator.t
self.untracked_channels_menu.placeholder = t(_p(
'ui:voice_tracker_config|menu:untracked_channels|placeholder',
"Set Untracked Channels"
))
async def run(self, interaction: discord.Interaction):
if old := self._listening.get(self.channelid, None):
await old.close()
await self.refresh()
if interaction.response.is_done():
# Use followup to respond
self._mesage = await interaction.followup.send(embed=self.embed, view=self)
else:
# Use interaction response to respond
self._original = interaction
await interaction.response.send_message(embed=self.embed, view=self)
for instance in self.instances:
instance.register_callback(self.id)(self.reload)
self._listening[self.channelid] = self
async def refresh(self):
# TODO: Check if listening works for subclasses
await self.refresh_close_button()
await self.refresh_reset_button()
await self.refresh_untracked_channels_menu()
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
if lguild.guild_mode.voice is VoiceMode.VOICE:
self.hourly_reward = await self.settings.HourlyReward_Voice.get(self.guildid)
else:
self.hourly_reward = await self.settings.HourlyReward_Study.get(self.guildid)
self.hourly_live_bonus = lguild.config.get('hourly_live_bonus')
self.daily_voice_cap = lguild.config.get('daily_voice_cap')
self.untracked_channels = await self.settings.UntrackedChannels.get(self.guildid)
self._layout = [
(self.untracked_channels_menu,),
(self.reset_button, self.close_button)
]
self.embed = await self.make_embed()
async def redraw(self):
try:
if self._message:
await self._message.edit(embed=self.embed, view=self)
elif self._original:
await self._original.edit_original_response(embed=self.embed, view=self)
except discord.HTTPException:
await self.close()
async def reload(self, *args, **kwargs):
await self.refresh()
await self.redraw()
async def make_embed(self):
t = self.bot.translator.t
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
mode = lguild.guild_mode
if mode.voice is VoiceMode.VOICE:
title = t(_p(
'ui:voice_tracker_config|mode:voice|embed|title',
"Voice Tracker Configuration Panel"
))
else:
title = t(_p(
'ui:voice_tracker_config|mode:study|embed|title',
"Study Tracker Configuration Panel"
))
embed = discord.Embed(
colour=discord.Colour.orange(),
title=title
)
for setting in self.instances:
embed.add_field(**setting.embed_field, inline=False)
return embed