| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- """
- A guild configuration setting available for editing via bot commands.
- """
- from typing import Any, Callable, Coroutine, Optional, Type
-
- from discord.ext import commands
- from discord.ext.commands import Bot, Command, Context, Group, Cog
-
- from config import CONFIG
- from rocketbot.storage import Storage
- from rocketbot.utils import first_command_group
-
- def _fix_command(command: Command) -> None:
- """
- HACK: Fixes bug in discord.py 2.3.2 where it's requiring the user to
- supply the context argument. This removes that argument from the list.
- """
- params = command.params
- del params['context']
- command.params = params
-
- class CogSetting:
- """
- Describes a configuration setting for a guild that can be edited by the
- mods of those guilds. BaseCog can generate "get" and "set" commands (or
- "enable" and "disable" commands for boolean values) automatically, reducing
- the boilerplate of generating commands manually. Offers simple validation rules.
- """
- def __init__(self,
- name: str,
- datatype: Optional[Type],
- brief: Optional[str] = None,
- description: Optional[str] = None,
- usage: Optional[str] = None,
- min_value: Optional[Any] = None,
- max_value: Optional[Any] = None,
- enum_values: Optional[set[Any]] = None):
- """
- Params:
- - name Setting identifier. Must follow variable naming
- conventions.
- - datatype Datatype of the setting. E.g. int, float, str
- - brief Description of the setting, starting with lower case.
- Will be inserted into phrases like "Sets <brief>" and
- "Gets <brief".
- - description Long-form description. Min, max, and enum values will be
- appended to the end, so does not need to include these.
- - usage Description of the value argument in a set command, e.g.
- "<maxcount:int>"
- - min_value Smallest allowable value. Must be of the same datatype as
- the value. None for no minimum.
- - max_value Largest allowable value. None for no maximum.
- - enum_values Set of allowed values. None if unconstrained.
- """
- self.name: str = name
- self.datatype: Type = datatype
- self.brief: Optional[str] = brief
- self.description: str = description or '' # Can't be None
- self.usage: Optional[str] = usage
- self.min_value: Optional[Any] = min_value
- self.max_value: Optional[Any] = max_value
- self.enum_values: Optional[set[Any]] = enum_values
- if self.enum_values or self.min_value is not None or self.max_value is not None:
- self.description += '\n'
- if self.enum_values:
- allowed_values = '`' + ('`, `'.join(enum_values)) + '`'
- self.description += f'\nAllowed values: {allowed_values}'
- if self.min_value is not None:
- self.description += f'\nMin value: {self.min_value}'
- if self.max_value is not None:
- self.description += f'\nMax value: {self.max_value}'
- if self.usage is None:
- self.usage = f'<{self.name}>'
-
- def validate_value(self, new_value) -> None:
- """
- Checks if a value is legal for this setting. Raises a ValueError if not.
- """
- if self.min_value is not None and new_value < self.min_value:
- raise ValueError(f'`{self.name}` must be >= {self.min_value}')
- if self.max_value is not None and new_value > self.max_value:
- raise ValueError(f'`{self.name}` must be <= {self.max_value}')
- if self.enum_values is not None and new_value not in self.enum_values:
- allowed_values = '`' + ('`, `'.join(self.enum_values)) + '`'
- raise ValueError(f'`{self.name}` must be one of {allowed_values}')
-
- def set_up(self, cog: Cog, bot: Bot, group: Group) -> None:
- """
- Sets up getter and setter commands for this setting. This should
- usually only be called by BaseCog.
- """
- if self.name in ('enabled', 'is_enabled'):
- (group or bot).add_command(self.__make_enable_command(cog))
- (group or bot).add_command(self.__make_disable_command(cog))
- else:
- (group or bot).add_command(self.__make_getter_command(cog))
- (group or bot).add_command(self.__make_setter_command(cog))
-
- def __make_getter_command(self, cog: Cog) -> Command:
- setting: CogSetting = self
- async def getter(cog0: Cog, context: Context) -> None:
- setting_name = setting.name
- if isinstance(context.command.parent, Group):
- setting_name = f'{context.command.parent.name}.{setting_name}'
- key = f'{cog0.__class__.__name__}.{setting.name}'
- value = Storage.get_config_value(context.guild, key)
- if value is None:
- value = cog0.get_cog_default(setting.name)
- await context.message.reply(
- f'{CONFIG["info_emoji"]} `{setting_name}` is using default of `{value}`',
- mention_author=False)
- else:
- await context.message.reply(
- f'{CONFIG["info_emoji"]} `{setting_name}` is set to `{value}`',
- mention_author=False)
- command = Command(
- getter,
- name=f'get{setting.name}',
- brief=f'Shows {setting.brief}',
- description=setting.description,
- checks=[
- commands.has_permissions(ban_members=True),
- commands.guild_only(),
- ])
- command.cog = cog
- _fix_command(command)
- return command
-
- def __make_setter_command(self, cog: Cog) -> Command:
- setting: CogSetting = self
- async def setter_common(cog0: Cog, context: Context, new_value) -> None:
- try:
- setting.validate_value(new_value)
- except ValueError as ve:
- await context.message.reply(
- f'{CONFIG["failure_emoji"]} {ve}',
- mention_author=False)
- return
- setting_name = setting.name
- if isinstance(context.command.parent, Group):
- setting_name = f'{context.command.parent.name}.{setting_name}'
- key = f'{cog0.__class__.__name__}.{setting.name}'
- Storage.set_config_value(context.guild, key, new_value)
- await context.message.reply(
- f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
- mention_author=False)
- await cog0.on_setting_updated(context.guild, setting)
- cog0.log(context.guild, f'{context.author.name} set {key} to {new_value}')
-
- async def setter_int(cog1, context, new_value: int):
- await setter_common(cog1, context, new_value)
- async def setter_float(cog2, context, new_value: float):
- await setter_common(cog2, context, new_value)
- async def setter_str(cog3, context, new_value: str):
- await setter_common(cog3, context, new_value)
- async def setter_bool(cog4, context, new_value: bool):
- await setter_common(cog4, context, new_value)
-
- setter: Callable[[Cog, Context, Any], Coroutine]
- if setting.datatype == int:
- setter = setter_int
- elif setting.datatype == float:
- setter = setter_float
- elif setting.datatype == str:
- setter = setter_str
- elif setting.datatype == bool:
- setter = setter_bool
- else:
- raise ValueError(f'Datatype {setting.datatype} unsupported')
-
- command = Command(
- setter,
- name=f'set{setting.name}',
- brief=f'Sets {setting.brief}',
- description=setting.description,
- usage=setting.usage,
- checks=[
- commands.has_permissions(ban_members=True),
- commands.guild_only(),
- ])
- # HACK: Passing `cog` in init gets ignored and set to `None` so set after.
- # This ensures the callback is passed the cog as `self` argument.
- command.cog = cog
- _fix_command(command)
- return command
-
- def __make_enable_command(self, cog: Cog) -> Command:
- setting: CogSetting = self
- async def enabler(cog0: Cog, context: Context) -> None:
- key = f'{cog0.__class__.__name__}.{setting.name}'
- Storage.set_config_value(context.guild, key, True)
- await context.message.reply(
- f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
- mention_author=False)
- await cog0.on_setting_updated(context.guild, setting)
- cog0.log(context.guild, f'{context.author.name} enabled {cog0.__class__.__name__}')
-
- command = Command(
- enabler,
- name='enable',
- brief=f'Enables {setting.brief}',
- description=setting.description,
- checks=[
- commands.has_permissions(ban_members=True),
- commands.guild_only(),
- ])
- command.cog = cog
- _fix_command(command)
- return command
-
- def __make_disable_command(self, cog: Cog) -> Command:
- setting: CogSetting = self
- async def disabler(cog0: Cog, context: Context) -> None:
- key = f'{cog0.__class__.__name__}.{setting.name}'
- Storage.set_config_value(context.guild, key, False)
- await context.message.reply(
- f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
- mention_author=False)
- await cog0.on_setting_updated(context.guild, setting)
- cog0.log(context.guild, f'{context.author.name} disabled {cog0.__class__.__name__}')
-
- command = Command(
- disabler,
- name='disable',
- brief=f'Disables {setting.brief}',
- description=setting.description,
- checks=[
- commands.has_permissions(ban_members=True),
- commands.guild_only(),
- ])
- command.cog = cog
- _fix_command(command)
- return command
-
- @classmethod
- def set_up_all(cls, cog: Cog, bot: Bot, settings: list) -> None:
- """
- Sets up editing commands for a list of CogSettings and adds them to a
- cog. If the cog has a command Group, commands will be added to it.
- Otherwise they will be added at the top level.
- """
- group: Group = first_command_group(cog)
- for setting in settings:
- setting.set_up(cog, bot, group)
|