Experimental Discord bot written in Python
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

cogsetting.py 9.8KB

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