from collections import defaultdict import discord from settings import ModelData, ListData from settings.groups import SettingGroup, ModelConfig, SettingDotDict from settings.setting_types import ( ChannelSetting, IntegerSetting, ChannelListSetting, RoleSetting ) from core.setting_types import CoinSetting from meta import conf from meta.errors import UserInputError from meta.sharding import THIS_SHARD from meta.logger import log_wrap from babel.translator import ctx_translator from . import babel, logger from .data import ScheduleData _p = babel._p class ScheduleConfig(ModelConfig): settings = SettingDotDict() _model_settings = set() model = ScheduleData.ScheduleGuild class ScheduleSettings(SettingGroup): @ScheduleConfig.register_model_setting class SessionLobby(ModelData, ChannelSetting): setting_id = 'session_lobby' _event = 'guildset_session_lobby' _set_cmd = 'configure schedule' _display_name = _p('guildset:session_lobby', "session_lobby") _desc = _p( 'guildset:session_lobby|desc', "Channel to post scheduled session announcement and status to." ) _long_desc = _p( 'guildset:session_lobby|long_desc', "Channel in which to announce scheduled sessions and post their status. " "I must have the `MANAGE_WEBHOOKS` permission in this channel.\n" "**This must be configured in order for the scheduled session system to function.**" ) _accepts = _p( 'guildset:session_lobby|accepts', "Name or id of the session lobby channel." ) _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.lobby_channel.name _allow_object = False @property def update_message(self): t = ctx_translator.get().t if self.data: resp = t(_p( 'guildset:session_lobby|set_response|set', "Scheduled sessions will now be announced in {channel}" )).format(channel=self.formatted) else: resp = t(_p( 'guildset:session_lobby|set_response|unset', "The schedule session lobby has been unset. Shutting down scheduled session system." )) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): t = ctx_translator.get().t if data is None: formatted = t(_p( 'guildset:session_lobby|formatted|unset', "`Not Set` (The scheduled session system is disabled.)" )) else: formatted = t(_p( 'guildset:session_lobby|formatted|set', "<#{channelid}>" )).format(channelid=data) return formatted @ScheduleConfig.register_model_setting class SessionRoom(ModelData, ChannelSetting): setting_id = 'session_room' _set_cmd = 'configure schedule' _display_name = _p('guildset:session_room', "session_room") _desc = _p( 'guildset:session_room|desc', "Special voice channel open to scheduled session members." ) _long_desc = _p( 'guildset:session_room|long_desc', "If set, this voice channel serves as a dedicated room for scheduled session members. " "During (and slightly before) each scheduled session, all members who have booked the session " "will be given permission to join the voice channel (via permission overwrites). " "I require the `MANAGE_CHANNEL`, `MANAGE_PERMISSIONS`, `CONNECT`, and `VIEW_CHANNEL` permissions " "in this channel, and my highest role must be higher than all permission overwrites set in the channel. " "Furthermore, if this is set to a *category* channel, then the permission overwrites will apply " "to all *synced* channels under the category, as usual." ) _accepts = _p( 'guildset:session_room|accepts', "Name or id of the session room voice channel." ) channel_types = [discord.VoiceChannel, discord.CategoryChannel] _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.room_channel.name _allow_object = False @property def update_message(self): t = ctx_translator.get().t if self.data: resp = t(_p( 'guildset:session_room|set_response|set', "Schedule session members will now be given access to {channel}" )).format(channel=self.formatted) else: resp = t(_p( 'guildset:session_room|set_response|unset', "The dedicated schedule session room has been removed." )) return resp class SessionChannels(ListData, ChannelListSetting): setting_id = 'session_channels' _display_name = _p('guildset:session_channels', "session_channels") _desc = _p( 'guildset:session_channels|desc', "Voice channels in which to track activity for scheduled sessions." ) _long_desc = _p( 'guildset:session_channels|long_desc', "Only activity in these channels (and in `session_room` if set) will count towards " "scheduled session attendance. If a category is selected, then all channels " "under the category will also be included. " "Activity tracking also respects the `untracked_voice_channels` setting." ) _accepts = _p( 'guildset:session_channels|accepts', "Comma separated list of session channel names or ids." ) _default = None _table_interface = ScheduleData.schedule_channels _id_column = 'guildid' _data_column = 'channelid' _order_column = 'channelid' _cache = {} @property def update_message(self): t = ctx_translator.get().t if self.data: resp = t(_p( 'guildset:session_channels|set_response|set', "Activity in the following sessions will now count towards scheduled session attendance: {channels}" )).format(channels=self.formatted) else: resp = t(_p( 'guildset:session_channels|set_response|unset', "Activity in all (tracked) voice channels will now count towards session attendance." )) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): t = ctx_translator.get().t if data is None: formatted = t(_p( 'guildset:session_channels|formatted|unset', "All Channels (excluding `untracked_channels`)" )) else: formatted = super()._format_data(parent_id, data, **kwargs) return formatted @classmethod @log_wrap(action='Cache Schedule Channels') async def setup(cls, bot): """ Pre-load schedule channels for every guild on the current shard. This includes guilds which the client cannot see. """ data = bot.db.registries['ScheduleData'] rows = await data.schedule_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} schedule session channels on this shard.") @ScheduleConfig.register_model_setting class ScheduleCost(ModelData, CoinSetting): setting_id = 'schedule_cost' _set_cmd = 'configure schedule' _display_name = _p('guildset:schedule_cost', "schedule_cost") _desc = _p( 'guildset:schedule_cost|desc', "Booking cost for each scheduled session." ) _long_desc = _p( 'guildset:schedule_cost|long_desc', "Members will be charged this many LionCoins for each scheduled session they book." ) _accepts = _p( 'guildset:schedule_cost|accepts', "Price of each session booking (non-negative integer)." ) _default = 100 _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.schedule_cost.name @property def update_message(self) -> str: t = ctx_translator.get().t resp = t(_p( 'guildset:schedule_cost|set_response', "Schedule session bookings will now cost {coin} **{amount}** per timeslot." )).format( coin=conf.emojis.coin, amount=self.value ) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): if data is not None: t = ctx_translator.get().t formatted = t(_p( 'guildset:schedule_cost|formatted', "{coin}**{amount}** per booking." )).format(coin=conf.emojis.coin, amount=data) return formatted @ScheduleConfig.register_model_setting class AttendanceReward(ModelData, CoinSetting): setting_id = 'attendance_reward' _set_cmd = 'configure schedule' _display_name = _p('guildset:attendance_reward', "attendance_reward") _desc = _p( 'guildset:attendance_reward|desc', "Reward for attending a booked scheduled session." ) _long_desc = _p( 'guildset:attendance_reward|long_desc', "When a member successfully attends a scheduled session they booked, " "they will be awarded this many LionCoins. " "Should generally be more than the `schedule_cost` setting." ) _accepts = _p( 'guildset:attendance_reward|accepts', "Number of coins to reward session attendance." ) _default = 200 _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.reward.name @property def update_message(self) -> str: t = ctx_translator.get().t resp = t(_p( 'guildset:attendance_reward|set_response', "Members will be rewarded {coin}**{amount}** when they attend a scheduled session." )).format(coin=conf.emojis.coin, amount=self.value) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): if data is not None: t = ctx_translator.get().t formatted = t(_p( 'guildset:attendance_reward|formatted', "{coin}**{amount}** upon attendance." )).format(coin=conf.emojis.coin, amount=data) return formatted @ScheduleConfig.register_model_setting class AttendanceBonus(ModelData, CoinSetting): setting_id = 'attendance_bonus' _set_cmd = 'configure schedule' _display_name = _p('guildset:attendance_bonus', "group_attendance_bonus") _desc = _p( 'guildset:attendance_bonus|desc', "Bonus reward given when all members attend a scheduled session." ) _long_desc = _p( 'guildset:attendance_bonus|long_desc', "When all members who have booked a session successfully attend the session, " "they will be given this bonus in *addition* to the `attendance_reward`." ) _accepts = _p( 'guildset:attendance_bonus|accepts', "Bonus coins rewarded when everyone attends a session." ) _default = 200 _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.bonus_reward.name @property def update_message(self) -> str: t = ctx_translator.get().t resp = t(_p( 'guildset:attendance_bonus|set_response', "Session members will be rewarded an additional {coin}**{amount}** when everyone attends." )).format(coin=conf.emojis.coin, amount=self.value) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): if data is not None: t = ctx_translator.get().t formatted = t(_p( 'guildset:attendance_bonus|formatted', "{coin}**{amount}** bonus when all booked members attend." )).format(coin=conf.emojis.coin, amount=data) return formatted @ScheduleConfig.register_model_setting class MinAttendance(ModelData, IntegerSetting): setting_id = 'min_attendance' _set_cmd = 'configure schedule' _display_name = _p('guildset:min_attendance', "min_attendance") _desc = _p( 'guildset:min_attendance|desc', "Minimum attendance before reward eligability." ) _long_desc = _p( 'guildset:min_attendance|long_desc', "Scheduled session members will need to attend the session for at least this number of minutes " "before they are marked as having attended (and hence are rewarded)." ) _accepts = _p( 'guildset:min_attendance|accepts', "Number of minutes (1-60) before attendance is counted." ) _default = 10 _min = 1 _max = 60 _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.min_attendance.name @property def update_message(self) -> str: t = ctx_translator.get().t resp = t(_p( 'guildset:min_attendance|set_response', "Members will be rewarded after they have attended booked sessions for at least **`{amount}`** minutes." )).format(amount=self.value) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): if data is not None: t = ctx_translator.get().t formatted = t(_p( 'guildset:min_attendance|formatted', "**`{amount}`** minutes" )).format(amount=data) return formatted @classmethod async def _parse_string(cls, parent_id, string: str, **kwargs): if not string: return None string = string.strip('m ') num = int(string) if string.isdigit() else None try: num = int(string) except Exception: num = None if num is None or not 0 < num < 60: t = ctx_translator.get().t error = t(_p( 'guildset:min_attendance|parse|error', "Minimum attendance must be an integer number of minutes between `1` and `60`." )) raise UserInputError(error) @ScheduleConfig.register_model_setting class BlacklistRole(ModelData, RoleSetting): setting_id = 'schedule_blacklist_role' _set_cmd = 'configure schedule' _event = 'guildset_schedule_blacklist_role' _display_name = _p('guildset:schedule_blacklist_role', "schedule_blacklist_role") _desc = _p( 'guildset:schedule_blacklist_role|desc', "Role which disables scheduled session booking." ) _long_desc = _p( 'guildset:schedule_blacklist_role|long_desc', "Members with this role will not be allowed to book scheduled sessions in this server. " "If the role is manually added, all future scheduled sessions for the user are cancelled. " "This provides a way to stop repeatedly unreliable members from blocking the group bonus for all members. " "Alternatively, consider setting the booking cost (and reward) very high to provide " "a strong disincentive for not attending a session." ) _accepts = _p( 'guildset:schedule_blacklist_role|accepts', "Blacklist role name or id." ) _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.blacklist_role.name _allow_object = False @property def update_message(self): t = ctx_translator.get().t if self.data: resp = t(_p( 'guildset:schedule_blacklist_role|set_response|set', "Members with {role} will be unable to book scheduled sessions." )).format(role=self.formatted) else: resp = t(_p( 'guildset:schedule_blacklist_role|set_response|unset', "The schedule blacklist role has been unset." )) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): t = ctx_translator.get().t if data is not None: formatted = t(_p( 'guildset:schedule_blacklist_role|formatted|set', "{role} members will not be able to book scheduled sessions." )).format(role=f"<&{data}>") else: formatted = t(_p( 'guildset:schedule_blacklist_role|formatted|unset', "Not Set" )) return formatted @ScheduleConfig.register_model_setting class BlacklistAfter(ModelData, IntegerSetting): setting_id = 'schedule_blacklist_after' _set_cmd = 'configure schedule' _display_name = _p('guildset:schedule_blacklist_after', "schedule_blacklist_after") _desc = _p( 'guildset:schedule_blacklist_after|desc', "Number of missed sessions within 24h before blacklisting." ) _long_desc = _p( 'guildset:schedule_blacklist_after|long_desc', "Members who miss more than this number of booked sessions in a single 24 hour period " "will be automatically given the `blacklist_role`. " "Has no effect if the `blacklist_role` is not set or if I do not have sufficient permissions " "to assign the blacklist role." ) _accepts = _p( 'guildset:schedule_blacklist_after|accepts', "A number of missed sessions (1-24) before blacklisting." ) _default = None _min = 1 _max = 24 _model = ScheduleData.ScheduleGuild _column = ScheduleData.ScheduleGuild.blacklist_after.name @property def update_message(self) -> str: t = ctx_translator.get().t if self.data: resp = t(_p( 'guildset:schedule_blacklist_after|set_response|set', "Members will be blacklisted after **`{amount}`** missed sessions within `24h`." )).format(amount=self.data) else: resp = t(_p( 'guildset:schedule_blacklist_after|set_response|unset', "Members will not be automatically blacklisted from booking scheduled sessions." )) return resp @classmethod def _format_data(cls, parent_id, data, **kwargs): t = ctx_translator.get().t if data is not None: formatted = t(_p( 'guildset:schedule_blacklist_after|formatted|set', "Blacklist after **`{amount}`** missed sessions within `24h`." )).format(amount=data) else: formatted = t(_p( 'guildset:schedule_blacklist_after|formatted|unset', "Do not automatically blacklist." )) return formatted @classmethod async def _parse_string(cls, parent_id, string: str, **kwargs): try: return await super()._parse_string(parent_id, string, **kwargs) except UserInputError: t = ctx_translator.get().t error = t(_p( 'guildset:schedule_blacklist_role|parse|error', "Blacklist threshold must be a number between `1` and `24`." )) raise UserInputError(error) from None