Experimental Discord bot written in Python
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

bangcommandcog.py 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import re
  2. from typing import Optional, TypedDict
  3. from discord import Interaction, Guild, Message, TextChannel
  4. from discord.app_commands import Group
  5. from discord.ext.commands import Cog
  6. from config import CONFIG
  7. from rocketbot.bot import Rocketbot
  8. from rocketbot.cogs.basecog import BaseCog
  9. from rocketbot.cogsetting import CogSetting
  10. from rocketbot.ui.pagedcontent import PAGE_BREAK, update_paged_content, paginate
  11. from rocketbot.utils import MOD_PERMISSIONS, blockquote_markdown, indent_markdown
  12. _CURRENT_DATA_VERSION = 1
  13. class BangCommand(TypedDict):
  14. content: str
  15. mod_only: bool
  16. version: int
  17. class BangCommandCog(BaseCog, name='Bang Commands'):
  18. SETTING_COMMANDS = CogSetting(
  19. name='commands',
  20. datatype=dict[str, BangCommand],
  21. default_value={},
  22. )
  23. def __init__(self, bot: Rocketbot):
  24. super().__init__(
  25. bot,
  26. config_prefix='bangcommand',
  27. short_description='Provides custom informational chat !commands.',
  28. long_description='Bang commands are simple one-word messages starting with an exclamation '
  29. '(bang) that will make the bot respond with simple informational replies. '
  30. 'Useful for posting answers to frequently asked questions, reminding users '
  31. 'of rules, and similar.'
  32. )
  33. def _get_commands(self, guild: Guild) -> dict[str, BangCommand]:
  34. return self.get_guild_setting(guild, BangCommandCog.SETTING_COMMANDS)
  35. def _set_commands(self, guild: Guild, commands: dict[str, BangCommand]) -> None:
  36. self.set_guild_setting(guild, BangCommandCog.SETTING_COMMANDS, commands)
  37. bang = Group(
  38. name='bangcommand',
  39. description='Provides custom informational chat !commands.',
  40. guild_only=True,
  41. default_permissions=MOD_PERMISSIONS,
  42. )
  43. @bang.command()
  44. async def define(self, interaction: Interaction, name: str, definition: str, mod_only: bool = False) -> None:
  45. """
  46. Defines or redefines a bang command.
  47. Parameters
  48. ----------
  49. interaction: Interaction
  50. name: string
  51. name of the command (lowercase a-z, underscores, and hyphens)
  52. definition: string
  53. content of the command
  54. mod_only: bool
  55. whether the command will only be recognized when a mod uses it
  56. """
  57. if not BangCommandCog._is_valid_name(name):
  58. self.log(interaction.guild, f'{interaction.user.name} used command /bangcommand define {name} {definition}')
  59. await interaction.response.send_message(
  60. f'{CONFIG["failure_emoji"]} Invalid command name. Names must consist of lowercase letters, underscores, and hyphens (no spaces).',
  61. ephemeral=True,
  62. )
  63. return
  64. name = BangCommandCog._normalize_name(name)
  65. cmds = self._get_commands(interaction.guild)
  66. cmds[name] = {
  67. 'content': definition,
  68. 'mod_only': mod_only,
  69. 'version': _CURRENT_DATA_VERSION,
  70. }
  71. self._set_commands(interaction.guild, cmds)
  72. await interaction.response.send_message(
  73. f'{CONFIG["success_emoji"]} Command `!{name}` has been defined.\n\n{blockquote_markdown(definition)}',
  74. ephemeral=True,
  75. )
  76. @bang.command()
  77. async def undefine(self, interaction: Interaction, name: str) -> None:
  78. """
  79. Removes a bang command.
  80. Parameters
  81. ----------
  82. interaction: Interaction
  83. name: string
  84. name of the previously defined command
  85. """
  86. name = BangCommandCog._normalize_name(name)
  87. cmds = self._get_commands(interaction.guild)
  88. if name not in cmds:
  89. await interaction.response.send_message(
  90. f'{CONFIG["failure_emoji"]} Command `!{name}` does not exist.',
  91. ephemeral=True,
  92. )
  93. return
  94. del cmds[name]
  95. self._set_commands(interaction.guild, cmds)
  96. await interaction.response.send_message(
  97. f'{CONFIG["success_emoji"]} Command `!{name}` removed.',
  98. ephemeral=True,
  99. )
  100. @bang.command()
  101. async def list(self, interaction: Interaction) -> None:
  102. """
  103. Lists all defined bang commands.
  104. Parameters
  105. ----------
  106. interaction: Interaction
  107. """
  108. cmds = self._get_commands(interaction.guild)
  109. if cmds is None or len(cmds) == 0:
  110. await interaction.response.send_message(
  111. f'{CONFIG["info_emoji"]} No commands defined.',
  112. ephemeral=True,
  113. )
  114. return
  115. text = '## Commands'
  116. for name, cmd in sorted(cmds.items()):
  117. text += PAGE_BREAK + f'\n- `!{name}`'
  118. if cmd['mod_only']:
  119. text += ' - **mod only**'
  120. text += f'\n{indent_markdown(cmd["content"])}'
  121. pages = paginate(text)
  122. await update_paged_content(interaction, None, 0, pages)
  123. @Cog.listener()
  124. async def on_message(self, message: Message) -> None:
  125. if message.guild is None or message.channel is None or not isinstance(message.channel, TextChannel):
  126. return
  127. content = message.content
  128. if content is None or not content.startswith('!') or not BangCommandCog._is_valid_name(content):
  129. return
  130. name = BangCommandCog._normalize_name(content)
  131. cmds = self._get_commands(message.guild)
  132. cmd = cmds.get(name, None)
  133. if cmd is None:
  134. return
  135. if cmd['mod_only'] and not message.author.guild_permissions.ban_members:
  136. return
  137. text = cmd["content"]
  138. # text = f'{text}\n\n-# {message.author.name} used `!{name}`'
  139. await message.channel.send(
  140. text,
  141. )
  142. @staticmethod
  143. def _normalize_name(name: str) -> str:
  144. name = name.lower().strip()
  145. if name.startswith('!'):
  146. name = name[1:]
  147. return name
  148. @staticmethod
  149. def _is_valid_name(name: Optional[str]) -> bool:
  150. if name is None:
  151. return False
  152. return re.match(r'^!?([a-z]+)([_-][a-z]+)*$', name) is not None