Experimental Discord bot written in Python
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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. return command
  113. def __make_setter_command(self, cog: Cog) -> Command:
  114. setting: CogSetting = self
  115. async def setter_common(cog: Cog, context: Context, new_value) -> None:
  116. try:
  117. setting.validate_value(new_value)
  118. except ValueError as ve:
  119. await context.message.reply(
  120. f'{CONFIG["failure_emoji"]} {ve}',
  121. mention_author=False)
  122. return
  123. setting_name = setting.name
  124. if context.command.parent:
  125. setting_name = f'{context.command.parent.name}.{setting_name}'
  126. key = f'{cog.__class__.__name__}.{setting.name}'
  127. Storage.set_config_value(context.guild, key, new_value)
  128. await context.message.reply(
  129. f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
  130. mention_author=False)
  131. await cog.on_setting_updated(context.guild, setting)
  132. cog.log(context.guild, f'{context.author.name} set {key} to {new_value}')
  133. async def setter_int(cog, context, new_value: int):
  134. await setter_common(cog, context, new_value)
  135. async def setter_float(cog, context, new_value: float):
  136. await setter_common(cog, context, new_value)
  137. async def setter_str(cog, context, new_value: str):
  138. await setter_common(cog, context, new_value)
  139. async def setter_bool(cog, context, new_value: bool):
  140. await setter_common(cog, context, new_value)
  141. setter: coroutine = None
  142. if setting.datatype == int:
  143. setter = setter_int
  144. elif setting.datatype == float:
  145. setter = setter_float
  146. elif setting.datatype == str:
  147. setter = setter_str
  148. elif setting.datatype == bool:
  149. setter = setter_bool
  150. else:
  151. raise ValueError(f'Datatype {setting.datatype} unsupported')
  152. command = Command(
  153. setter,
  154. name=f'set{setting.name}',
  155. brief=f'Sets {setting.brief}',
  156. description=setting.description,
  157. usage=setting.usage,
  158. checks=[
  159. commands.has_permissions(ban_members=True),
  160. commands.guild_only(),
  161. ])
  162. # Passing `cog` in init gets ignored and set to `None` so set after.
  163. # This ensures the callback is passed the cog as `self` argument.
  164. command.cog = cog
  165. return command
  166. def __make_enable_command(self, cog: Cog) -> Command:
  167. setting: CogSetting = self
  168. async def enabler(cog: Cog, context: Context) -> None:
  169. key = f'{cog.__class__.__name__}.{setting.name}'
  170. Storage.set_config_value(context.guild, key, True)
  171. await context.message.reply(
  172. f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
  173. mention_author=False)
  174. await cog.on_setting_updated(context.guild, setting)
  175. cog.log(context.guild, f'{context.author.name} enabled {cog.__class__.__name__}')
  176. command = Command(
  177. enabler,
  178. name='enable',
  179. brief=f'Enables {setting.brief}',
  180. description=setting.description,
  181. checks=[
  182. commands.has_permissions(ban_members=True),
  183. commands.guild_only(),
  184. ])
  185. command.cog = cog
  186. return command
  187. def __make_disable_command(self, cog: Cog) -> Command:
  188. setting: CogSetting = self
  189. async def disabler(cog: Cog, context: Context) -> None:
  190. key = f'{cog.__class__.__name__}.{setting.name}'
  191. Storage.set_config_value(context.guild, key, False)
  192. await context.message.reply(
  193. f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
  194. mention_author=False)
  195. await cog.on_setting_updated(context.guild, setting)
  196. cog.log(context.guild, f'{context.author.name} disabled {cog.__class__.__name__}')
  197. command = Command(
  198. disabler,
  199. name='disable',
  200. brief=f'Disables {setting.brief}',
  201. description=setting.description,
  202. checks=[
  203. commands.has_permissions(ban_members=True),
  204. commands.guild_only(),
  205. ])
  206. command.cog = cog
  207. return command
  208. @classmethod
  209. def set_up_all(cls, cog: Cog, bot: Bot, settings: list) -> None:
  210. """
  211. Sets up editing commands for a list of CogSettings and adds them to a
  212. cog. If the cog has a command Group, commands will be added to it.
  213. Otherwise they will be added at the top level.
  214. """
  215. group: Group = first_command_group(cog)
  216. for setting in settings:
  217. setting.set_up(cog, bot, group)