""" Configuration settings associated to the statistics module """ from typing import Optional import asyncio import discord from discord.ui.select import select, Select, SelectOption, RoleSelect from discord.ui.button import button, Button, ButtonStyle from discord.ui.text_input import TextInput, TextStyle from settings import ListData, ModelData, InteractiveSetting from settings.setting_types import RoleListSetting, EnumSetting, ListSetting, BoolSetting, TimestampSetting from settings.groups import SettingGroup from meta import conf, LionBot from meta.context import ctx_bot from meta.errors import UserInputError from utils.lib import tabulate, utc_now from utils.ui import ConfigUI, FastModal, error_handler_for, ModalRetryUI, DashboardSection from utils.lib import MessageArgs from core.data import CoreData from core.lion_guild import VoiceMode from babel.translator import ctx_translator from . import babel from .data import StatsData, StatisticType _p = babel._p class StatTypeSetting(EnumSetting): """ ABC setting type mixin describing an available stat type. """ _enum = StatisticType _outputs = { StatisticType.VOICE: _p('settype:stat|output:voice', "`Voice`"), StatisticType.TEXT: _p('settype:stat|output:text', "`Text`"), StatisticType.ANKI: _p('settype:stat|output:anki', "`Anki`"), } _input_formatted = { StatisticType.VOICE: _p('settype:stat|input_format:voice', "Voice"), StatisticType.TEXT: _p('settype:stat|input_format:text', "Text"), StatisticType.ANKI: _p('settype:stat|input_format:anki', "Anki"), } _input_patterns = { StatisticType.VOICE: _p('settype:stat|input_pattern:voice', "voice|study"), StatisticType.TEXT: _p('settype:stat|input_pattern:text', "text|messages"), StatisticType.ANKI: _p('settype:stat|input_pattern:anki', "anki"), } _accepts = _p( 'settype:state|accepts', 'Voice/Text/Anki' ) class StatisticsSettings(SettingGroup): class UserGlobalStats(ModelData, BoolSetting): """ User setting, describing whether to display global statistics or not in servers. Exposed via a button on the `/stats` panel. """ setting_id = 'show_global_stats' _display_name = _p('userset:show_global_stats', "global_stats") _desc = _p( 'userset:show_global_stats|desc', "Whether displayed statistics include all your servers." ) _long_desc = _p( 'userset:show_global_stats|long_desc', "Whether statistics commands display combined stats for all servers or just your current server." ) _model = CoreData.User _column = CoreData.User.show_global_stats.name class SeasonStart(ModelData, TimestampSetting): """ Start of the statistics season, displayed on the leaderboard and used to determine activity ranks Time is assumed to be in set guild timezone (although supports +00 syntax) """ setting_id = 'season_start' _set_cmd = 'configure statistics' _display_name = _p('guildset:season_start', "season_start") _desc = _p( 'guildset:season_start|desc', "Start of the current statistics season." ) _long_desc = _p( 'guildset:season_start|long_desc', "Activity ranks will be determined based on tracked activity since this time, " "and the leaderboard will display activity since this time by default. " "Unset to disable seasons and use all-time statistics instead." ) _accepts = _p( 'guildset:season_start|accepts', "The season start time in the form YYYY-MM-DD HH:MM" ) _notset_str = _p( 'guildset:season_start|notset', "Not Set (Using all-time statistics)" ) _model = CoreData.Guild _column = CoreData.Guild.season_start.name @classmethod async def _timezone_from_id(cls, guildid, **kwargs): bot = ctx_bot.get() lguild = await bot.core.lions.fetch_guild(guildid) return lguild.timezone @classmethod async def _parse_string(cls, parent_id, string, **kwargs): parsed = await super()._parse_string(parent_id, string, **kwargs) if parsed is not None and parsed > utc_now(): t = ctx_translator.get().t raise UserInputError(t(_p( 'guildset:season_start|parse|error:future_time', "Provided season start time {timestamp} is in the future!" )).format(timestamp=f"")) return parsed @property def update_message(self) -> str: t = ctx_translator.get().t bot = ctx_bot.get() value = self.value if value is not None: resp = t(_p( 'guildset:season_start|set_response|set', "The leaderboard season and activity ranks will now count from {timestamp}. " "Member ranks will update when they are next active. Use {rank_cmd} to refresh immediately." )).format( timestamp=self.formatted, rank_cmd=bot.core.mention_cmd('ranks') ) else: resp = t(_p( 'guildset:season_start|set_response|unset', "The leaderboard and activity ranks will now count all-time statistics. " "Member ranks will update when they are next active. Use {rank_cmd} to refresh immediately." )).format(rank_cmd=bot.core.mention_cmd('ranks')) return resp class UnrankedRoles(ListData, RoleListSetting): """ List of roles not displayed on the leaderboard """ setting_id = 'unranked_roles' _display_name = _p('guildset:unranked_roles', "unranked_roles") _desc = _p( 'guildset:unranked_roles|desc', "Roles to exclude from the leaderboards." ) _long_desc = _p( 'guildset:unranked_roles|long_desc', "When set, members with *any* of these roles will not appear on the /leaderboard ranking list." ) _accepts = _p( 'guildset:unranked_roles|accepts', "Comma separated list of unranked role names or ids." ) _default = None _table_interface = StatsData.unranked_roles _id_column = 'guildid' _data_column = 'roleid' _order_column = 'roleid' _cache = {} @property def set_str(self): t = ctx_translator.get().t return t(_p( 'guildset:unranked_roles|set_using', "Role selector below." )) @property def update_message(self) -> str: t = ctx_translator.get().t value = self.value if value is not None: resp = t(_p( 'guildset:unranked_roles|set_response|set', "Members of the following roles will not appear on the leaderboard: {roles}" )).format( roles=self.formatted ) else: resp = t(_p( 'guildset:unranked_roles|set_response|unset', "You have cleared the unranked role list." )) return resp class VisibleStats(ListData, ListSetting, InteractiveSetting): """ Which of the three stats (text, voice/study, anki) to enable in statistics views Default is determined by current guild mode """ setting_id = 'visible_stats' _setting = StatTypeSetting _display_name = _p('guildset:visible_stats', "visible_stats") _desc = _p( 'guildset:visible_stats|desc', "Which statistics will be visible in the statistics commands." ) _long_desc = _p( 'guildset:visible_stats|desc', "Choose which statistics types to display in the leaderboard and statistics commands." ) _accepts = _p( 'guildset:visible_stats|accepts', "Voice, Text, Anki" ) # TODO: Format VOICE as STUDY when possible? _default = [ StatisticType.VOICE, StatisticType.TEXT, ] _table_interface = StatsData.visible_statistics _id_column = 'guildid' _data_column = 'stat_type' _order_column = 'stat_type' _cache = {} @property def set_str(self): t = ctx_translator.get().t return t(_p( 'guildset:visible_stats|set_using', "Option menu below." )) @property def update_message(self) -> str: t = ctx_translator.get().t resp = t(_p( 'guildset:visible_stats|set_response', "Members will be able to view the following statistics types: {types}" )).format(types=self.formatted) return resp class DefaultStat(ModelData, StatTypeSetting): """ Which of the three stats to display by default """ setting_id = 'default_stat' _display_name = _p('guildset:default_stat', "default_stat") _desc = _p( 'guildset:default_stat|desc', "Statistic type to display by default in setting dialogues." ) _long_desc = _p( 'guildset:default_stat|long_desc', "Which statistic type to display by default in setting dialogues." ) class StatisticsConfigUI(ConfigUI): setting_classes = ( StatisticsSettings.SeasonStart, StatisticsSettings.UnrankedRoles, StatisticsSettings.VisibleStats ) def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs): super().__init__(bot, guildid, channelid, **kwargs) self.settings = self.bot.get_cog('StatsCog').settings @select(cls=RoleSelect, placeholder='UNRANKED_ROLE_MENU', min_values=0, max_values=25) async def unranked_roles_menu(self, selection: discord.Interaction, selected): """ Selection menu for the "unranked_roles" setting. """ await selection.response.defer(thinking=True) setting = self.instances[1] setting.value = selected.values await setting.write() # Don't need to refresh due to instance hooks # await self.refresh(thinking=selection) await selection.delete_original_response() async def unranked_roles_menu_refresh(self): t = self.bot.translator.t self.unranked_roles_menu.placeholder = t(_p( 'ui:statistics_config|menu:unranked_roles|placeholder', "Select Unranked Roles" )) @select(placeholder="STAT_TYPE_MENU", min_values=1, max_values=3) async def stat_type_menu(self, selection: discord.Interaction, selected): """ Selection menu for the "visible_stats" setting. """ await selection.response.defer(thinking=True) setting = self.instances[2] data = [StatisticType((value,)) for value in selected.values] setting.data = data await setting.write() await selection.delete_original_response() async def stat_type_menu_refresh(self): t = self.bot.translator.t setting = self.instances[2] value = setting.value lguild = await self.bot.core.lions.fetch_guild(self.guildid) if lguild.guild_mode.voice is VoiceMode.VOICE: voice_label = t(_p( 'ui:statistics_config|menu:visible_stats|item:voice|mode:voice', "Voice Activity" )) else: voice_label = t(_p( 'ui:statistics_config|menu:visible_stats|item:voice|mode:study', "Study Statistics" )) voice_option = SelectOption( label=voice_label, value=StatisticType.VOICE.value[0], default=(StatisticType.VOICE in value) ) text_option = SelectOption( label=t(_p( 'ui:statistics_config|menu:visible_stats|item:text', "Message Activity" )), value=StatisticType.TEXT.value[0], default=(StatisticType.TEXT in value) ) anki_option = SelectOption( label=t(_p( 'ui:statistics_config|menu:visible_stats|item:anki', "Anki Reviews" )), value=StatisticType.ANKI.value[0], default=(StatisticType.ANKI in value) ) self.stat_type_menu.options = [ voice_option, text_option, anki_option ] self.stat_type_menu.placeholder = t(_p( 'ui:statistics_config|menu:visible_stats|placeholder', "Select Visible Statistics" )) async def refresh_components(self): await asyncio.gather( self.edit_button_refresh(), self.close_button_refresh(), self.reset_button_refresh(), self.unranked_roles_menu_refresh(), self.stat_type_menu_refresh(), ) self._layout = [ (self.unranked_roles_menu,), (self.stat_type_menu,), (self.edit_button, self.reset_button, self.close_button) ] async def make_message(self): t = self.bot.translator.t title = t(_p( 'ui:statistics_config|embed|title', "Statistics 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 MessageArgs(embed=embed) async def reload(self): # Re-fetch data for each instance # This should generally hit cache self.instances = [ await setting.get(self.guildid) for setting in self.setting_classes ] class StatisticsDashboard(DashboardSection): section_name = _p( 'dash:stats|title', "Activity Statistics Configuration ({commands[configure statistics]})" ) _option_name = _p( "dash:stats|dropdown|placeholder", "Activity Statistics Panel" ) configui = StatisticsConfigUI setting_classes = StatisticsConfigUI.setting_classes