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