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.1KB

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