545 lines
19 KiB
Python
545 lines
19 KiB
Python
from typing import Optional
|
|
import asyncio
|
|
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.lib import MessageArgs
|
|
from utils.ui import LeoUI, ConfigUI, DashboardSection
|
|
from wards import low_management_iward
|
|
|
|
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):
|
|
setting_id = 'untracked_channels'
|
|
_event = 'guildset_untracked_channels'
|
|
_set_cmd = 'config voice_rewards'
|
|
_write_ward = low_management_iward
|
|
|
|
_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."
|
|
)
|
|
_accepts = _p(
|
|
'guildset:untracked_channels|accepts',
|
|
"Comma separated list of untracked channel name/ids."
|
|
)
|
|
_notset_str = _p(
|
|
'guildset:untracked_channels|notset',
|
|
"Not Set (all voice channels will be tracked.)"
|
|
)
|
|
|
|
_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
|
|
if self.data:
|
|
resp = t(_p(
|
|
'guildset:untracked_channels|set_response|set',
|
|
"Activity in the following channels will now be ignored: {channels}"
|
|
)).format(
|
|
channels=self.formatted
|
|
)
|
|
else:
|
|
resp = t(_p(
|
|
'guildset:untracked_channels|set_response|unset',
|
|
"All voice channels will now be tracked."
|
|
))
|
|
return resp
|
|
|
|
@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 = 'on_guildset_hourly_reward'
|
|
_set_cmd = 'config voice_rewards'
|
|
_write_ward = low_management_iward
|
|
|
|
_display_name = _p('guildset:hourly_reward', "hourly_reward")
|
|
_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."
|
|
)
|
|
_accepts = _p(
|
|
'guildset:hourly_reward|accepts',
|
|
"Number of coins to reward per hour in voice."
|
|
)
|
|
|
|
_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
|
|
)
|
|
|
|
class HourlyReward_Voice(HourlyReward):
|
|
"""
|
|
Voice-mode specialised version of HourlyReward
|
|
"""
|
|
@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 = 'on_guildset_hourly_live_bonus'
|
|
_set_cmd = 'config voice_rewards'
|
|
_write_ward = low_management_iward
|
|
|
|
_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`."
|
|
)
|
|
_accepts = _p(
|
|
'guildset:hourly_live_bonus|accepts',
|
|
"Number of bonus coins to reward per hour when live."
|
|
)
|
|
|
|
_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 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 = 'on_guildset_daily_voice_cap'
|
|
_set_cmd = 'config voice_rewards'
|
|
_write_ward = low_management_iward
|
|
|
|
_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."
|
|
)
|
|
_accepts = _p(
|
|
'guildset:daily_voice_cap|accepts',
|
|
"The maximum number of voice hours to track per day."
|
|
)
|
|
|
|
_default = 16 * 60 * 60
|
|
_default_multiplier = 60 * 60
|
|
|
|
_max = 60 * 60 * 25
|
|
|
|
_model = CoreData.Guild
|
|
_column = CoreData.Guild.daily_study_cap.name
|
|
|
|
@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 VoiceTrackerConfigUIALT(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):
|
|
# TODO: Swap cleanup and close..
|
|
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._message = 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
|
|
|
|
|
|
class VoiceTrackerConfigUI(ConfigUI):
|
|
setting_classes = (
|
|
VoiceTrackerSettings.HourlyReward,
|
|
VoiceTrackerSettings.HourlyLiveBonus,
|
|
VoiceTrackerSettings.DailyVoiceCap,
|
|
VoiceTrackerSettings.UntrackedChannels,
|
|
)
|
|
|
|
def __init__(self, bot: LionBot,
|
|
guildid: int, channelid: int, **kwargs):
|
|
self.settings = bot.get_cog('VoiceTrackerCog').settings
|
|
super().__init__(bot, guildid, channelid, **kwargs)
|
|
|
|
@select(
|
|
cls=ChannelSelect,
|
|
placeholder="UNTRACKED_CHANNELS_PLACEHOLDER",
|
|
channel_types=[
|
|
discord.enums.ChannelType.voice, discord.enums.ChannelType.stage_voice, discord.enums.ChannelType.category
|
|
],
|
|
min_values=0, max_values=25
|
|
)
|
|
async def untracked_channels_menu(self, selection: discord.Interaction, selected):
|
|
await selection.response.defer()
|
|
setting = self.instances[3]
|
|
await setting.interaction_check(setting.parent_id, selection)
|
|
setting.value = selected.values
|
|
await setting.write()
|
|
|
|
async def untracked_channels_menu_refresh(self):
|
|
t = self.bot.translator.t
|
|
self.untracked_channels_menu.placeholder = t(_p(
|
|
'ui:voice_tracker_config|menu:untracked_channels|placeholder',
|
|
"Select Untracked Channels"
|
|
))
|
|
|
|
async def make_message(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)
|
|
|
|
args = MessageArgs(embed=embed)
|
|
return args
|
|
|
|
async def reload(self):
|
|
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
|
|
if lguild.guild_mode.voice is VoiceMode.VOICE:
|
|
hourly_reward = await self.settings.HourlyReward_Voice.get(self.guildid)
|
|
else:
|
|
hourly_reward = await self.settings.HourlyReward_Study.get(self.guildid)
|
|
hourly_live_bonus = lguild.config.get('hourly_live_bonus')
|
|
daily_voice_cap = lguild.config.get('daily_voice_cap')
|
|
untracked_channels = await self.settings.UntrackedChannels.get(self.guildid)
|
|
self.instances = (
|
|
hourly_reward, hourly_live_bonus, daily_voice_cap, untracked_channels
|
|
)
|
|
|
|
async def refresh_components(self):
|
|
await asyncio.gather(
|
|
self.edit_button_refresh(),
|
|
self.close_button_refresh(),
|
|
self.reset_button_refresh(),
|
|
self.untracked_channels_menu_refresh(),
|
|
)
|
|
self._layout = [
|
|
(self.untracked_channels_menu,),
|
|
(self.edit_button, self.reset_button, self.close_button)
|
|
]
|
|
|
|
|
|
class VoiceTrackerDashboard(DashboardSection):
|
|
section_name = _p(
|
|
'dash:voice_tracker|title',
|
|
"Voice Tracker Configuration ({commands[config voice_rewards]})"
|
|
)
|
|
_option_name = _p(
|
|
"dash:voice_tracking|dropdown|placeholder",
|
|
"Voice Activity Panel"
|
|
)
|
|
configui = VoiceTrackerConfigUI
|
|
setting_classes = configui.setting_classes
|