Quellcode durchsuchen

Beginning of help system

pull/13/head
Rocketsoup vor 2 Monaten
Ursprung
Commit
fa49bd0725
4 geänderte Dateien mit 156 neuen und 8 gelöschten Zeilen
  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 Datei anzeigen

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

+ 1
- 2
rocketbot/cogs/basecog.py Datei anzeigen

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

+ 153
- 4
rocketbot/cogs/generalcog.py Datei anzeigen

2
 Cog for handling most ungrouped commands and basic behaviors.
2
 Cog for handling most ungrouped commands and basic behaviors.
3
 """
3
 """
4
 from datetime import datetime, timedelta, timezone
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
 from discord.errors import DiscordException
10
 from discord.errors import DiscordException
10
 from discord.ext.commands import Cog
11
 from discord.ext.commands import Cog
11
 
12
 
12
 from config import CONFIG
13
 from config import CONFIG
13
 from rocketbot.bot import Rocketbot
14
 from rocketbot.bot import Rocketbot
14
 from rocketbot.cogs.basecog import BaseCog, BotMessage
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
 from rocketbot.storage import ConfigKey, Storage
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
 class GeneralCog(BaseCog, name='General'):
65
 class GeneralCog(BaseCog, name='General'):
19
 	"""
66
 	"""
20
 	Cog for handling high-level bot functionality and commands. Should be the
67
 	Cog for handling high-level bot functionality and commands. Should be the
21
 	first cog added to the bot.
68
 	first cog added to the bot.
22
 	"""
69
 	"""
70
+
71
+	shared: Optional['GeneralCog'] = None
72
+
23
 	def __init__(self, bot: Rocketbot):
73
 	def __init__(self, bot: Rocketbot):
24
 		super().__init__(
74
 		super().__init__(
25
 			bot,
75
 			bot,
31
 		self.is_first_connect = True
81
 		self.is_first_connect = True
32
 		self.last_disconnect_time: Optional[datetime] = None
82
 		self.last_disconnect_time: Optional[datetime] = None
33
 		self.noteworthy_disconnect_duration = timedelta(seconds=5)
83
 		self.noteworthy_disconnect_duration = timedelta(seconds=5)
84
+		GeneralCog.shared = self
34
 
85
 
35
 	@Cog.listener()
86
 	@Cog.listener()
36
 	async def on_connect(self):
87
 	async def on_connect(self):
144
 			f'messages by {user.mention}> from the past {describe_timedelta(age)}.',
195
 			f'messages by {user.mention}> from the past {describe_timedelta(age)}.',
145
 			ephemeral=True,
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 Datei anzeigen

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

Laden…
Abbrechen
Speichern