Experimental Discord bot written in Python
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

cogsetting.py 8.9KB

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