Experimental Discord bot written in Python
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

cogsetting.py 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. """
  2. A guild configuration setting available for editing via bot commands.
  3. """
  4. from typing import Any, Callable, Coroutine, Optional, Type
  5. from discord.ext import commands
  6. from discord.ext.commands import Bot, Command, Context, Group, Cog
  7. from config import CONFIG
  8. from rocketbot.storage import Storage
  9. from rocketbot.utils import first_command_group
  10. def _fix_command(command: Command) -> None:
  11. """
  12. HACK: Fixes bug in discord.py 2.3.2 where it's requiring the user to
  13. supply the context argument. This removes that argument from the list.
  14. """
  15. params = command.params
  16. del params['context']
  17. command.params = params
  18. class CogSetting:
  19. """
  20. Describes a configuration setting for a guild that can be edited by the
  21. mods of those guilds. BaseCog can generate "get" and "set" commands (or
  22. "enable" and "disable" commands for boolean values) automatically, reducing
  23. the boilerplate of generating commands manually. Offers simple validation rules.
  24. """
  25. def __init__(self,
  26. name: str,
  27. datatype: Optional[Type],
  28. brief: Optional[str] = None,
  29. description: Optional[str] = None,
  30. usage: Optional[str] = None,
  31. min_value: Optional[Any] = None,
  32. max_value: Optional[Any] = None,
  33. enum_values: Optional[set[Any]] = None):
  34. """
  35. Params:
  36. - name Setting identifier. Must follow variable naming
  37. conventions.
  38. - datatype Datatype of the setting. E.g. int, float, str
  39. - brief Description of the setting, starting with lower case.
  40. Will be inserted into phrases like "Sets <brief>" and
  41. "Gets <brief".
  42. - description Long-form description. Min, max, and enum values will be
  43. appended to the end, so does not need to include these.
  44. - usage Description of the value argument in a set command, e.g.
  45. "<maxcount:int>"
  46. - min_value Smallest allowable value. Must be of the same datatype as
  47. the value. None for no minimum.
  48. - max_value Largest allowable value. None for no maximum.
  49. - enum_values Set of allowed values. None if unconstrained.
  50. """
  51. self.name: str = name
  52. self.datatype: Type = datatype
  53. self.brief: Optional[str] = brief
  54. self.description: str = description or '' # Can't be None
  55. self.usage: Optional[str] = usage
  56. self.min_value: Optional[Any] = min_value
  57. self.max_value: Optional[Any] = max_value
  58. self.enum_values: Optional[set[Any]] = enum_values
  59. if self.enum_values or self.min_value is not None or self.max_value is not None:
  60. self.description += '\n'
  61. if self.enum_values:
  62. allowed_values = '`' + ('`, `'.join(enum_values)) + '`'
  63. self.description += f'\nAllowed values: {allowed_values}'
  64. if self.min_value is not None:
  65. self.description += f'\nMin value: {self.min_value}'
  66. if self.max_value is not None:
  67. self.description += f'\nMax value: {self.max_value}'
  68. if self.usage is None:
  69. self.usage = f'<{self.name}>'
  70. def validate_value(self, new_value) -> None:
  71. """
  72. Checks if a value is legal for this setting. Raises a ValueError if not.
  73. """
  74. if self.min_value is not None and new_value < self.min_value:
  75. raise ValueError(f'`{self.name}` must be >= {self.min_value}')
  76. if self.max_value is not None and new_value > self.max_value:
  77. raise ValueError(f'`{self.name}` must be <= {self.max_value}')
  78. if self.enum_values is not None and new_value not in self.enum_values:
  79. allowed_values = '`' + ('`, `'.join(self.enum_values)) + '`'
  80. raise ValueError(f'`{self.name}` must be one of {allowed_values}')
  81. def set_up(self, cog: Cog, bot: Bot, group: Group) -> None:
  82. """
  83. Sets up getter and setter commands for this setting. This should
  84. usually only be called by BaseCog.
  85. """
  86. if self.name in ('enabled', 'is_enabled'):
  87. (group or bot).add_command(self.__make_enable_command(cog))
  88. (group or bot).add_command(self.__make_disable_command(cog))
  89. else:
  90. (group or bot).add_command(self.__make_getter_command(cog))
  91. (group or bot).add_command(self.__make_setter_command(cog))
  92. def __make_getter_command(self, cog: Cog) -> Command:
  93. setting: CogSetting = self
  94. async def getter(cog0: Cog, context: Context) -> None:
  95. setting_name = setting.name
  96. if isinstance(context.command.parent, Group):
  97. setting_name = f'{context.command.parent.name}.{setting_name}'
  98. key = f'{cog0.__class__.__name__}.{setting.name}'
  99. value = Storage.get_config_value(context.guild, key)
  100. if value is None:
  101. value = cog0.get_cog_default(setting.name)
  102. await context.message.reply(
  103. f'{CONFIG["info_emoji"]} `{setting_name}` is using default of `{value}`',
  104. mention_author=False)
  105. else:
  106. await context.message.reply(
  107. f'{CONFIG["info_emoji"]} `{setting_name}` is set to `{value}`',
  108. mention_author=False)
  109. command = Command(
  110. getter,
  111. name=f'get{setting.name}',
  112. brief=f'Shows {setting.brief}',
  113. description=setting.description,
  114. checks=[
  115. commands.has_permissions(ban_members=True),
  116. commands.guild_only(),
  117. ])
  118. command.cog = cog
  119. _fix_command(command)
  120. return command
  121. def __make_setter_command(self, cog: Cog) -> Command:
  122. setting: CogSetting = self
  123. async def setter_common(cog0: Cog, context: Context, new_value) -> None:
  124. try:
  125. setting.validate_value(new_value)
  126. except ValueError as ve:
  127. await context.message.reply(
  128. f'{CONFIG["failure_emoji"]} {ve}',
  129. mention_author=False)
  130. return
  131. setting_name = setting.name
  132. if isinstance(context.command.parent, Group):
  133. setting_name = f'{context.command.parent.name}.{setting_name}'
  134. key = f'{cog0.__class__.__name__}.{setting.name}'
  135. Storage.set_config_value(context.guild, key, new_value)
  136. await context.message.reply(
  137. f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
  138. mention_author=False)
  139. await cog0.on_setting_updated(context.guild, setting)
  140. cog0.log(context.guild, f'{context.author.name} set {key} to {new_value}')
  141. async def setter_int(cog1, context, new_value: int):
  142. await setter_common(cog1, context, new_value)
  143. async def setter_float(cog2, context, new_value: float):
  144. await setter_common(cog2, context, new_value)
  145. async def setter_str(cog3, context, new_value: str):
  146. await setter_common(cog3, context, new_value)
  147. async def setter_bool(cog4, context, new_value: bool):
  148. await setter_common(cog4, context, new_value)
  149. setter: Callable[[Cog, Context, Any], Coroutine]
  150. if setting.datatype == int:
  151. setter = setter_int
  152. elif setting.datatype == float:
  153. setter = setter_float
  154. elif setting.datatype == str:
  155. setter = setter_str
  156. elif setting.datatype == bool:
  157. setter = setter_bool
  158. else:
  159. raise ValueError(f'Datatype {setting.datatype} unsupported')
  160. command = Command(
  161. setter,
  162. name=f'set{setting.name}',
  163. brief=f'Sets {setting.brief}',
  164. description=setting.description,
  165. usage=setting.usage,
  166. checks=[
  167. commands.has_permissions(ban_members=True),
  168. commands.guild_only(),
  169. ])
  170. # HACK: Passing `cog` in init gets ignored and set to `None` so set after.
  171. # This ensures the callback is passed the cog as `self` argument.
  172. command.cog = cog
  173. _fix_command(command)
  174. return command
  175. def __make_enable_command(self, cog: Cog) -> Command:
  176. setting: CogSetting = self
  177. async def enabler(cog0: Cog, context: Context) -> None:
  178. key = f'{cog0.__class__.__name__}.{setting.name}'
  179. Storage.set_config_value(context.guild, key, True)
  180. await context.message.reply(
  181. f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
  182. mention_author=False)
  183. await cog0.on_setting_updated(context.guild, setting)
  184. cog0.log(context.guild, f'{context.author.name} enabled {cog0.__class__.__name__}')
  185. command = Command(
  186. enabler,
  187. name='enable',
  188. brief=f'Enables {setting.brief}',
  189. description=setting.description,
  190. checks=[
  191. commands.has_permissions(ban_members=True),
  192. commands.guild_only(),
  193. ])
  194. command.cog = cog
  195. _fix_command(command)
  196. return command
  197. def __make_disable_command(self, cog: Cog) -> Command:
  198. setting: CogSetting = self
  199. async def disabler(cog0: Cog, context: Context) -> None:
  200. key = f'{cog0.__class__.__name__}.{setting.name}'
  201. Storage.set_config_value(context.guild, key, False)
  202. await context.message.reply(
  203. f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
  204. mention_author=False)
  205. await cog0.on_setting_updated(context.guild, setting)
  206. cog0.log(context.guild, f'{context.author.name} disabled {cog0.__class__.__name__}')
  207. command = Command(
  208. disabler,
  209. name='disable',
  210. brief=f'Disables {setting.brief}',
  211. description=setting.description,
  212. checks=[
  213. commands.has_permissions(ban_members=True),
  214. commands.guild_only(),
  215. ])
  216. command.cog = cog
  217. _fix_command(command)
  218. return command
  219. @classmethod
  220. def set_up_all(cls, cog: Cog, bot: Bot, settings: list) -> None:
  221. """
  222. Sets up editing commands for a list of CogSettings and adds them to a
  223. cog. If the cog has a command Group, commands will be added to it.
  224. Otherwise they will be added at the top level.
  225. """
  226. group: Group = first_command_group(cog)
  227. for setting in settings:
  228. setting.set_up(cog, bot, group)