531 lines
20 KiB
Python
531 lines
20 KiB
Python
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
|