Browse Source

Beginning of help system

pull/13/head
Rocketsoup 2 months ago
parent
commit
fa49bd0725
4 changed files with 156 additions and 8 deletions
  1. 1
    1
      rocketbot/bot.py
  2. 1
    2
      rocketbot/cogs/basecog.py
  3. 153
    4
      rocketbot/cogs/generalcog.py
  4. 1
    1
      rocketbot/cogsetting.py

+ 1
- 1
rocketbot/bot.py View File

@@ -5,7 +5,6 @@ from discord import Intents
5 5
 from discord.ext import commands
6 6
 
7 7
 from config import CONFIG
8
-from rocketbot.cogs.basecog import BaseCog
9 8
 from rocketbot.cogsetting import CogSetting
10 9
 from rocketbot.utils import bot_log, dump_stacktrace
11 10
 
@@ -54,6 +53,7 @@ class Rocketbot(commands.Bot):
54 53
 			return
55 54
 		self.__commands_set_up = True
56 55
 		for cog in self.cogs.values():
56
+			from rocketbot.cogs.basecog import BaseCog
57 57
 			if isinstance(cog, BaseCog):
58 58
 				bcog: BaseCog = cog
59 59
 				if len(bcog.settings) > 0:

+ 1
- 2
rocketbot/cogs/basecog.py View File

@@ -12,14 +12,13 @@ from discord.app_commands.errors import CommandInvokeError
12 12
 from discord.ext.commands import Cog
13 13
 
14 14
 from config import CONFIG
15
+from rocketbot.bot import Rocketbot
15 16
 from rocketbot.botmessage import BotMessage, BotMessageReaction
16 17
 from rocketbot.cogsetting import CogSetting
17 18
 from rocketbot.collections import AgeBoundDict
18 19
 from rocketbot.storage import Storage
19 20
 from rocketbot.utils import bot_log, dump_stacktrace
20 21
 
21
-Rocketbot = 'rocketbot.bot.Rocketbot'
22
-
23 22
 class WarningContext:
24 23
 	def __init__(self, member: Member, warn_time: datetime):
25 24
 		self.member = member

+ 153
- 4
rocketbot/cogs/generalcog.py View File

@@ -2,24 +2,74 @@
2 2
 Cog for handling most ungrouped commands and basic behaviors.
3 3
 """
4 4
 from datetime import datetime, timedelta, timezone
5
-from typing import Optional
5
+from typing import Optional, Union
6 6
 
7
-from discord import Interaction, Message, User
8
-from discord.app_commands import command, default_permissions, guild_only, Transform
7
+from discord import Interaction, Message, User, Permissions
8
+from discord.app_commands import Command, Group, command, default_permissions, guild_only, Transform, rename, Choice, \
9
+	autocomplete
9 10
 from discord.errors import DiscordException
10 11
 from discord.ext.commands import Cog
11 12
 
12 13
 from config import CONFIG
13 14
 from rocketbot.bot import Rocketbot
14 15
 from rocketbot.cogs.basecog import BaseCog, BotMessage
15
-from rocketbot.utils import describe_timedelta, TimeDeltaTransformer
16
+from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace
16 17
 from rocketbot.storage import ConfigKey, Storage
17 18
 
19
+async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
20
+	choices: list[Choice] = []
21
+	try:
22
+		if current.startswith('/'):
23
+			current = current[1:]
24
+		current = current.lower().strip()
25
+		user_permissions = interaction.permissions
26
+		cmds = GeneralCog.shared.get_command_list(user_permissions)
27
+		return [
28
+			Choice(name=f'/{cmdname}', value=f'/{cmdname}')
29
+			for cmdname in sorted(cmds.keys())
30
+			if len(current) == 0 or cmdname.startswith(current)
31
+		]
32
+	except BaseException as e:
33
+		dump_stacktrace(e)
34
+	return choices
35
+
36
+async def subcommand_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
37
+	try:
38
+		current = current.lower().strip()
39
+		cmd_name = interaction.namespace['command']
40
+		if cmd_name.startswith('/'):
41
+			cmd_name = cmd_name[1:]
42
+		user_permissions = interaction.permissions
43
+		cmd = GeneralCog.shared.get_command_list(user_permissions).get(cmd_name)
44
+		if cmd is None or not isinstance(cmd, Group):
45
+			print(f'No command found named {cmd_name}')
46
+			return []
47
+		grp = cmd
48
+		subcmds = GeneralCog.shared.get_subcommand_list(grp, user_permissions)
49
+		if subcmds is None:
50
+			print(f'Subcommands for {cmd_name} was None')
51
+			return []
52
+		return [
53
+			Choice(name=subcmd_name, value=subcmd_name)
54
+			for subcmd_name in sorted(subcmds.keys())
55
+			if len(current) == 0 or subcmd_name.startswith(current)
56
+		]
57
+	except BaseException as e:
58
+		dump_stacktrace(e)
59
+	return []
60
+
61
+def can_use_command(cmd: Union[Group, Command], user_permissions: Optional[Permissions]) -> bool:
62
+	return user_permissions is not None and \
63
+		(cmd.default_permissions is None or cmd.default_permissions.is_subset(user_permissions))
64
+
18 65
 class GeneralCog(BaseCog, name='General'):
19 66
 	"""
20 67
 	Cog for handling high-level bot functionality and commands. Should be the
21 68
 	first cog added to the bot.
22 69
 	"""
70
+
71
+	shared: Optional['GeneralCog'] = None
72
+
23 73
 	def __init__(self, bot: Rocketbot):
24 74
 		super().__init__(
25 75
 			bot,
@@ -31,6 +81,7 @@ class GeneralCog(BaseCog, name='General'):
31 81
 		self.is_first_connect = True
32 82
 		self.last_disconnect_time: Optional[datetime] = None
33 83
 		self.noteworthy_disconnect_duration = timedelta(seconds=5)
84
+		GeneralCog.shared = self
34 85
 
35 86
 	@Cog.listener()
36 87
 	async def on_connect(self):
@@ -144,3 +195,101 @@ class GeneralCog(BaseCog, name='General'):
144 195
 			f'messages by {user.mention}> from the past {describe_timedelta(age)}.',
145 196
 			ephemeral=True,
146 197
 		)
198
+
199
+	@command(name='help')
200
+	@guild_only()
201
+	@rename(command_name='command', subcommand_name='subcommand')
202
+	@autocomplete(command_name=command_autocomplete, subcommand_name=subcommand_autocomplete)
203
+	async def help_command(self, interaction: Interaction, command_name: Optional[str] = None, subcommand_name: Optional[str] = None) -> None:
204
+		"""
205
+		Shows help for using commands and subcommands.
206
+
207
+		`/help` will show a list of top-level commands.
208
+
209
+		`/help /<command_name>` will show help about a specific command or
210
+		list a command's subcommands.
211
+
212
+		`/help /<command_name> <subcommand_name>` will show help about a
213
+		specific subcommand.
214
+
215
+		Parameters
216
+		----------
217
+		interaction: Interaction
218
+		command_name: Optional[str]
219
+			Optional name of a command to get specific help for. With or without the leading slash.
220
+		subcommand_name: Optional[str]
221
+			Optional name of a subcommand to get specific help for.
222
+		"""
223
+		print(f'help_command(interaction, {command_name}, {subcommand_name})')
224
+		cmds: list[Command] = self.bot.tree.get_commands()
225
+		if command_name is None:
226
+			await self.__send_general_help(interaction)
227
+			return
228
+
229
+		if command_name.startswith('/'):
230
+			command_name = command_name[1:]
231
+		cmd = next((c for c in cmds if c.name == command_name), None)
232
+		if cmd is None:
233
+			interaction.response.send_message(
234
+				f'Command `{command_name}` not found!',
235
+				ephemeral=True,
236
+			)
237
+			return
238
+		if subcommand_name is None:
239
+			await self.__send_command_help(interaction, cmd)
240
+			return
241
+
242
+		if not isinstance(cmd, Group):
243
+			await self.__send_command_help(interaction, cmd, addendum=f'{CONFIG["warning_emoji"]} Command does not have subcommands. Showing help for base command.')
244
+			return
245
+		grp: Group = cmd
246
+		subcmd: Command = next((c for c in grp.commands if c.name == subcommand_name), None)
247
+		if subcmd is None:
248
+			await self.__send_command_help(interaction, cmd, addendum=f'{CONFIG["warning_emoji"]} Command `/{command_name}` does not have a subcommand "{subcommand_name}". Showing help for base command.')
249
+			return
250
+		await self.__send_subcommand_help(interaction, grp, subcmd)
251
+		return
252
+
253
+	def get_command_list(self, permissions: Optional[Permissions] = None) -> dict[str, Union[Command, Group]]:
254
+		return { cmd.name: cmd for cmd in self.bot.tree.get_commands() if can_use_command(cmd, permissions) }
255
+
256
+	def get_subcommand_list(self, cmd: Group, permissions: Optional[Permissions] = None) -> dict[str, Command]:
257
+		return { subcmd.name: subcmd for subcmd in cmd.commands if can_use_command(subcmd, permissions) }
258
+
259
+	async def __send_general_help(self, interaction: Interaction) -> None:
260
+		user_permissions: Permissions = interaction.permissions
261
+		text = f'## :information_source: Commands'
262
+		for cmd_name, cmd in sorted(self.get_command_list(user_permissions).items()):
263
+			text += f'\n- `/{cmd_name}`: {cmd.description}'
264
+		await interaction.response.send_message(
265
+			text,
266
+			ephemeral=True,
267
+		)
268
+
269
+	async def __send_command_help(self, interaction: Interaction, command_or_group: Union[Command, Group], addendum: Optional[str] = None) -> None:
270
+		text = ''
271
+		if addendum is not None:
272
+			text += addendum + '\n\n'
273
+		text += f'## :information_source: Command Help\n`/{command_or_group.name}`\n\n{command_or_group.description}'
274
+		if isinstance(command_or_group, Group):
275
+			subcmds: dict[str, Command] = self.get_subcommand_list(command_or_group, permissions=interaction.permissions)
276
+			if len(subcmds) > 0:
277
+				text += '\n\n### Subcommands:'
278
+				for subcmd_name, subcmd in sorted(subcmds.items()):
279
+					text += f'\n- `{subcmd_name}`: {subcmd.description}'
280
+		else:
281
+			params = command_or_group.parameters
282
+			if len(params) > 0:
283
+				text += '\n\n### Parameters:'
284
+				for param in params:
285
+					text += f'\n- `{param.name}`: {param.description}'
286
+		await interaction.response.send_message(text, ephemeral=True)
287
+
288
+	async def __send_subcommand_help(self, interaction: Interaction, group: Group, subcommand: Command) -> None:
289
+		text = f'## :information_source: Subcommand Help\n`/{group.name} {subcommand.name}`\n\n{subcommand.description}\n\n{subcommand.description}'
290
+		params = subcommand.parameters
291
+		if len(params) > 0:
292
+			text += '\n\n### Parameters:'
293
+			for param in params:
294
+				text += f'\n- `{param.name}`: {param.description}'
295
+		await interaction.response.send_message(text, ephemeral=True)

+ 1
- 1
rocketbot/cogsetting.py View File

@@ -240,7 +240,7 @@ class CogSetting:
240 240
 
241 241
 		command = Command(
242 242
 			name=cog.config_prefix,
243
-			description=f'Enables {cog.config_prefix} functionality',
243
+			description=f'Enables {cog.name} functionality',
244 244
 			callback=enabler,
245 245
 			parent=CogSetting.__enable_group,
246 246
 		)

Loading…
Cancel
Save