| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- import re
- from typing import Optional, TypedDict
-
- from discord import Interaction, Guild, Message, TextChannel
- from discord.app_commands import Group
- from discord.ext.commands import Cog
-
- from config import CONFIG
- from rocketbot.bot import Rocketbot
- from rocketbot.cogs.basecog import BaseCog
- from rocketbot.cogsetting import CogSetting
- from rocketbot.ui.pagedcontent import PAGE_BREAK, update_paged_content, paginate
- from rocketbot.utils import MOD_PERMISSIONS, blockquote_markdown, indent_markdown
-
- _CURRENT_DATA_VERSION = 1
-
- class BangCommand(TypedDict):
- content: str
- mod_only: bool
- version: int
-
- class BangCommandCog(BaseCog, name='Bang Commands'):
- SETTING_COMMANDS = CogSetting(
- name='commands',
- datatype=dict[str, BangCommand],
- default_value={},
- )
-
- def __init__(self, bot: Rocketbot):
- super().__init__(
- bot,
- config_prefix='bangcommand',
- short_description='Provides custom informational chat !commands.',
- long_description='Bang commands are simple one-word messages starting with an exclamation '
- '(bang) that will make the bot respond with simple informational replies. '
- 'Useful for posting answers to frequently asked questions, reminding users '
- 'of rules, and similar.'
- )
-
- def _get_commands(self, guild: Guild) -> dict[str, BangCommand]:
- return self.get_guild_setting(guild, BangCommandCog.SETTING_COMMANDS)
-
- def _set_commands(self, guild: Guild, commands: dict[str, BangCommand]) -> None:
- self.set_guild_setting(guild, BangCommandCog.SETTING_COMMANDS, commands)
-
- bang = Group(
- name='bangcommand',
- description='Provides custom informational chat !commands.',
- guild_only=True,
- default_permissions=MOD_PERMISSIONS,
- )
-
- @bang.command()
- async def define(self, interaction: Interaction, name: str, definition: str, mod_only: bool = False) -> None:
- """
- Defines or redefines a bang command.
-
- Parameters
- ----------
- interaction: Interaction
- name: string
- name of the command (lowercase a-z, underscores, and hyphens)
- definition: string
- content of the command
- mod_only: bool
- whether the command will only be recognized when a mod uses it
- """
- if not BangCommandCog._is_valid_name(name):
- self.log(interaction.guild, f'{interaction.user.name} used command /bangcommand define {name} {definition}')
- await interaction.response.send_message(
- f'{CONFIG["failure_emoji"]} Invalid command name. Names must consist of lowercase letters, underscores, and hyphens (no spaces).',
- ephemeral=True,
- )
- return
- name = BangCommandCog._normalize_name(name)
- cmds = self._get_commands(interaction.guild)
- cmds[name] = {
- 'content': definition,
- 'mod_only': mod_only,
- 'version': _CURRENT_DATA_VERSION,
- }
- self._set_commands(interaction.guild, cmds)
- await interaction.response.send_message(
- f'{CONFIG["success_emoji"]} Command `!{name}` has been defined.\n\n{blockquote_markdown(definition)}',
- ephemeral=True,
- )
-
- @bang.command()
- async def undefine(self, interaction: Interaction, name: str) -> None:
- """
- Removes a bang command.
-
- Parameters
- ----------
- interaction: Interaction
- name: string
- name of the previously defined command
- """
- name = BangCommandCog._normalize_name(name)
- cmds = self._get_commands(interaction.guild)
- if name not in cmds:
- await interaction.response.send_message(
- f'{CONFIG["failure_emoji"]} Command `!{name}` does not exist.',
- ephemeral=True,
- )
- return
- del cmds[name]
- self._set_commands(interaction.guild, cmds)
- await interaction.response.send_message(
- f'{CONFIG["success_emoji"]} Command `!{name}` removed.',
- ephemeral=True,
- )
-
- @bang.command()
- async def list(self, interaction: Interaction) -> None:
- """
- Lists all defined bang commands.
-
- Parameters
- ----------
- interaction: Interaction
- """
- cmds = self._get_commands(interaction.guild)
- if cmds is None or len(cmds) == 0:
- await interaction.response.send_message(
- f'{CONFIG["info_emoji"]} No commands defined.',
- ephemeral=True,
- )
- return
- text = '## Commands'
- for name, cmd in sorted(cmds.items()):
- text += PAGE_BREAK + f'\n- `!{name}`'
- if cmd['mod_only']:
- text += ' - **mod only**'
- text += f'\n{indent_markdown(cmd["content"])}'
- pages = paginate(text)
- await update_paged_content(interaction, None, 0, pages)
-
- @Cog.listener()
- async def on_message(self, message: Message) -> None:
- if message.guild is None or message.channel is None or not isinstance(message.channel, TextChannel):
- return
- content = message.content
- if content is None or not content.startswith('!') or not BangCommandCog._is_valid_name(content):
- return
- name = BangCommandCog._normalize_name(content)
- cmds = self._get_commands(message.guild)
- cmd = cmds.get(name, None)
- if cmd is None:
- return
- if cmd['mod_only'] and not message.author.guild_permissions.ban_members:
- return
- text = cmd["content"]
- # text = f'{text}\n\n-# {message.author.name} used `!{name}`'
- await message.channel.send(
- text,
- )
-
- @staticmethod
- def _normalize_name(name: str) -> str:
- name = name.lower().strip()
- if name.startswith('!'):
- name = name[1:]
- return name
-
- @staticmethod
- def _is_valid_name(name: Optional[str]) -> bool:
- if name is None:
- return False
- return re.match(r'^!?([a-z]+)([_-][a-z]+)*$', name) is not None
|