Просмотр исходного кода

Moving help to its own cog

pull/13/head
Rocketsoup 2 месяцев назад
Родитель
Сommit
1b89526523
4 измененных файлов: 407 добавлений и 348 удалений
  1. 2
    0
      rocketbot/bot.py
  2. 23
    0
      rocketbot/cogs/basecog.py
  3. 1
    348
      rocketbot/cogs/generalcog.py
  4. 381
    0
      rocketbot/cogs/helpcog.py

+ 2
- 0
rocketbot/bot.py Просмотреть файл

85
 from rocketbot.cogs.configcog import ConfigCog
85
 from rocketbot.cogs.configcog import ConfigCog
86
 from rocketbot.cogs.crosspostcog import CrossPostCog
86
 from rocketbot.cogs.crosspostcog import CrossPostCog
87
 from rocketbot.cogs.generalcog import GeneralCog
87
 from rocketbot.cogs.generalcog import GeneralCog
88
+from rocketbot.cogs.helpcog import HelpCog
88
 from rocketbot.cogs.joinraidcog import JoinRaidCog
89
 from rocketbot.cogs.joinraidcog import JoinRaidCog
89
 from rocketbot.cogs.logcog import LoggingCog
90
 from rocketbot.cogs.logcog import LoggingCog
90
 from rocketbot.cogs.patterncog import PatternCog
91
 from rocketbot.cogs.patterncog import PatternCog
102
 	# Optional
103
 	# Optional
103
 	await rocketbot.add_cog(AutoKickCog(rocketbot))
104
 	await rocketbot.add_cog(AutoKickCog(rocketbot))
104
 	await rocketbot.add_cog(CrossPostCog(rocketbot))
105
 	await rocketbot.add_cog(CrossPostCog(rocketbot))
106
+	await rocketbot.add_cog(HelpCog(rocketbot))
105
 	await rocketbot.add_cog(JoinRaidCog(rocketbot))
107
 	await rocketbot.add_cog(JoinRaidCog(rocketbot))
106
 	await rocketbot.add_cog(LoggingCog(rocketbot))
108
 	await rocketbot.add_cog(LoggingCog(rocketbot))
107
 	await rocketbot.add_cog(PatternCog(rocketbot))
109
 	await rocketbot.add_cog(PatternCog(rocketbot))

+ 23
- 0
rocketbot/cogs/basecog.py Просмотреть файл

68
 		except discord.InteractionResponded:
68
 		except discord.InteractionResponded:
69
 			await interaction.followup.send(f"An error occurred: {message}", ephemeral=True)
69
 			await interaction.followup.send(f"An error occurred: {message}", ephemeral=True)
70
 
70
 
71
+	@property
72
+	def basecogs(self) -> list['BaseCog']:
73
+		"""
74
+		List of BaseCog instances. Cogs that do not inherit from BaseCog are omitted.
75
+		"""
76
+		return [
77
+			bcog
78
+			for bcog in self.bot.cogs.values()
79
+			if isinstance(bcog, BaseCog)
80
+		]
81
+
82
+	@property
83
+	def basecog_map(self) -> dict[str, 'BaseCog']:
84
+		"""
85
+		Map of qualified names to BaseCog instances. Cogs that do not inherit
86
+		from BaseCog are omitted.
87
+		"""
88
+		return {
89
+			qname: bcog
90
+			for qname, bcog in self.bot.cogs.items()
91
+			if isinstance(bcog, BaseCog)
92
+		}
93
+
71
 	# Config
94
 	# Config
72
 
95
 
73
 	@classmethod
96
 	@classmethod

+ 1
- 348
rocketbot/cogs/generalcog.py Просмотреть файл

17
 from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace, MOD_PERMISSIONS
17
 from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace, MOD_PERMISSIONS
18
 from rocketbot.storage import ConfigKey, Storage
18
 from rocketbot.storage import ConfigKey, Storage
19
 
19
 
20
-
21
-trivial_words = {
22
-	'a', 'an', 'and', 'are', "aren't", 'as', 'by', 'can', 'for', 'have', 'if', 'in',
23
-	'is', 'it', 'its', "it's", 'not', 'of', 'on', 'or', 'than', 'that', 'the', 'then',
24
-	'there', 'them', 'they', "they're", 'this', 'to', 'when', 'with',
25
-}
26
-
27
-async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
28
-	choices: list[Choice] = []
29
-	try:
30
-		if current.startswith('/'):
31
-			current = current[1:]
32
-		current = current.lower().strip()
33
-		user_permissions = interaction.permissions
34
-		cmds = GeneralCog.shared.get_command_list(user_permissions)
35
-		return [
36
-			Choice(name=f'/{cmdname} command', value=f'cmd:{cmdname}')
37
-			for cmdname in sorted(cmds.keys())
38
-			if len(current) == 0 or current in cmdname
39
-		][:25]
40
-	except BaseException as e:
41
-		dump_stacktrace(e)
42
-	return choices
43
-
44
-async def subcommand_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
45
-	try:
46
-		current = current.lower().strip()
47
-		cmd_name = interaction.namespace['topic']
48
-		cmd = GeneralCog.shared.object_for_help_symbol(cmd_name)
49
-		if not isinstance(cmd, Group):
50
-			return []
51
-		user_permissions = interaction.permissions
52
-		if cmd is None or not isinstance(cmd, Group):
53
-			print(f'No command found named {cmd_name}')
54
-			return []
55
-		grp = cmd
56
-		subcmds = GeneralCog.shared.get_subcommand_list(grp, user_permissions)
57
-		if subcmds is None:
58
-			print(f'Subcommands for {cmd_name} was None')
59
-			return []
60
-		return [
61
-			Choice(name=f'{subcmd_name} subcommand', value=f'subcmd:{cmd_name}.{subcmd_name}')
62
-			for subcmd_name in sorted(subcmds.keys())
63
-			if len(current) == 0 or current in subcmd_name
64
-		][:25]
65
-	except BaseException as e:
66
-		dump_stacktrace(e)
67
-	return []
68
-
69
-async def cog_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
70
-	try:
71
-		current = current.lower().strip()
72
-		return [
73
-			Choice(name=f'⚙ {cog.qualified_name} module', value=f'cog:{cog.qualified_name}')
74
-			for cog in sorted(GeneralCog.shared.bot.cogs.values(), key=lambda c: c.qualified_name)
75
-			if isinstance(cog, BaseCog) and
76
-			   can_use_cog(cog, interaction.permissions) and
77
-			   (len(cog.get_commands()) > 0 or len(cog.settings) > 0) and \
78
-			   (len(current) == 0 or current in cog.qualified_name.lower())
79
-		]
80
-	except BaseException as e:
81
-		dump_stacktrace(e)
82
-	return []
83
-
84
-async def topic_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
85
-	command_choices = await command_autocomplete(interaction, current)
86
-	cog_choices = await cog_autocomplete(interaction, current)
87
-	return (command_choices + cog_choices)[:25]
88
-
89
-async def subtopic_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
90
-	subcommand_choices = await subcommand_autocomplete(interaction, current)
91
-	return subcommand_choices[:25]
92
-
93
-def can_use_command(cmd: Union[Group, Command], user_permissions: Optional[Permissions]) -> bool:
94
-	if user_permissions is None:
95
-		return False
96
-	if cmd.parent and not can_use_command(cmd.parent, user_permissions):
97
-		return False
98
-	return cmd.default_permissions is None or cmd.default_permissions.is_subset(user_permissions)
99
-
100
-def can_use_cog(cog: BaseCog, user_permissions: Optional[Permissions]) -> bool:
101
-	return user_permissions is not None and MOD_PERMISSIONS.is_subset(user_permissions)
102
-
103
 class GeneralCog(BaseCog, name='General'):
20
 class GeneralCog(BaseCog, name='General'):
104
 	"""
21
 	"""
105
 	Cog for handling high-level bot functionality and commands. Should be the
22
 	Cog for handling high-level bot functionality and commands. Should be the
175
 			)
92
 			)
176
 
93
 
177
 	@command(
94
 	@command(
178
-		description='Simple test reply',
95
+		description='Greets the user',
179
 		extras={
96
 		extras={
180
 			'long_description': 'Replies to the command message. Useful to ensure the ' + \
97
 			'long_description': 'Replies to the command message. Useful to ensure the ' + \
181
 				'bot is working properly.',
98
 				'bot is working properly.',
232
 			f'messages by {user.mention}> from the past {describe_timedelta(age)}.',
149
 			f'messages by {user.mention}> from the past {describe_timedelta(age)}.',
233
 			ephemeral=True,
150
 			ephemeral=True,
234
 		)
151
 		)
235
-
236
-	def __create_help_index(self) -> None:
237
-		if getattr(self, 'obj_index', None) is not None:
238
-			return
239
-		self.obj_index: dict[str, Union[Command, Group, BaseCog]] = {}
240
-		self.keyword_index: dict[str, set[Union[Command, Group, BaseCog]]] = {}
241
-
242
-		def add_text_to_index(obj, text: str):
243
-			words = [
244
-				word
245
-				for word in re.split(r"[^a-zA-Z']+", text.lower())
246
-				if len(word) > 1 and word not in trivial_words
247
-			]
248
-			for word in words:
249
-				matches = self.keyword_index.get(word, set())
250
-				matches.add(obj)
251
-				self.keyword_index[word] = matches
252
-
253
-		for cmd in self.bot.tree.get_commands(type=AppCommandType.chat_input):
254
-			key = f'cmd:{cmd.name}'
255
-			self.obj_index[key] = cmd
256
-			add_text_to_index(cmd, cmd.name)
257
-			if cmd.description:
258
-				add_text_to_index(cmd, cmd.description)
259
-			if isinstance(cmd, Group):
260
-				for subcmd in cmd.commands:
261
-					key = f'subcmd:{cmd.name}.{subcmd.name}'
262
-					self.obj_index[key] = subcmd
263
-					add_text_to_index(subcmd, subcmd.name)
264
-					if subcmd.description:
265
-						add_text_to_index(subcmd, subcmd.description)
266
-		for cog_qname, cog in self.bot.cogs.items():
267
-			if not isinstance(cog, BaseCog):
268
-				continue
269
-			key = f'cog:{cog_qname}'
270
-			self.obj_index[key] = cog
271
-			add_text_to_index(cog, cog.qualified_name)
272
-			if cog.description:
273
-				add_text_to_index(cog, cog.description)
274
-		print(self.obj_index.keys())
275
-		print(self.keyword_index.keys())
276
-
277
-	def object_for_help_symbol(self, symbol: str) -> Optional[Union[Command, Group, BaseCog]]:
278
-		self.__create_help_index()
279
-		return self.obj_index.get(symbol, None)
280
-
281
-	@command(name='help')
282
-	@guild_only()
283
-	@autocomplete(topic=topic_autocomplete, subtopic=subtopic_autocomplete)
284
-	async def help_command(self, interaction: Interaction, topic: Optional[str] = None, subtopic: Optional[str] = None) -> None:
285
-		"""
286
-		Shows help for using commands and subcommands and configuring modules.
287
-
288
-		`/help` will show a list of top-level topics.
289
-
290
-		`/help /<command_name>` will show help about a specific command or
291
-		list a command's subcommands.
292
-
293
-		`/help /<command_name> <subcommand_name>` will show help about a
294
-		specific subcommand.
295
-
296
-		`/help <module_name>` will show help about configuring a module.
297
-
298
-		`/help <keywords>` will do a text search for topics.
299
-
300
-		Parameters
301
-		----------
302
-		interaction: Interaction
303
-		topic: Optional[str]
304
-			Optional topic to get specific help for. Getting help on a command can optionally start with a leading slash.
305
-		subtopic: Optional[str]
306
-			Optional subtopic to get specific help for.
307
-		"""
308
-		print(f'help_command(interaction, {topic}, {subtopic})')
309
-
310
-		# General help
311
-		if topic is None:
312
-			await self.__send_general_help(interaction)
313
-			return
314
-
315
-		# Specific object reference
316
-		obj = self.object_for_help_symbol(subtopic) if subtopic else self.object_for_help_symbol(topic)
317
-		if obj:
318
-			await self.__send_object_help(interaction, obj)
319
-			return
320
-
321
-		# Text search
322
-		keywords = [
323
-			word
324
-			for word in re.split(r"[^a-zA-Z']+", topic.lower())
325
-			if len(word) > 0 and word not in trivial_words
326
-		]
327
-		matching_objects_set = None
328
-		for keyword in keywords:
329
-			objs = self.keyword_index.get(keyword, None)
330
-			if objs is not None:
331
-				if matching_objects_set is None:
332
-					matching_objects_set = objs
333
-				else:
334
-					matching_objects_set = matching_objects_set & objs
335
-		accessible_objects = [
336
-			obj
337
-			for obj in matching_objects_set or {}
338
-			if ((isinstance(obj, Command) or isinstance(obj, Group)) and can_use_command(obj, interaction.permissions)) or \
339
-			   (isinstance(obj, BaseCog) and can_use_cog(obj, interaction.permissions))
340
-		]
341
-		await self.__send_keyword_help(interaction, accessible_objects)
342
-
343
-	async def __send_object_help(self, interaction: Interaction, obj: Union[Command, Group, BaseCog]) -> None:
344
-		if isinstance(obj, Command):
345
-			if obj.parent:
346
-				await self.__send_subcommand_help(interaction, obj.parent, obj)
347
-			else:
348
-				await self.__send_command_help(interaction, obj)
349
-			return
350
-		if isinstance(obj, Group):
351
-			await self.__send_command_help(interaction, obj)
352
-			return
353
-		if isinstance(obj, BaseCog):
354
-			await self.__send_cog_help(interaction, obj)
355
-			return
356
-		print(f'No help for object {obj}')
357
-		await interaction.response.send_message(
358
-			f'{CONFIG["failure_emoji"]} Failed to get help info.',
359
-			ephemeral=True,
360
-			delete_after=10,
361
-		)
362
-
363
-	def get_command_list(self, permissions: Optional[Permissions] = None) -> dict[str, Union[Command, Group]]:
364
-		return { cmd.name: cmd for cmd in self.bot.tree.get_commands() if can_use_command(cmd, permissions) }
365
-
366
-	def get_subcommand_list(self, cmd: Group, permissions: Optional[Permissions] = None) -> dict[str, Command]:
367
-		return {
368
-			subcmd.name: subcmd
369
-			for subcmd in cmd.commands
370
-			if can_use_command(subcmd, permissions)
371
-		} if can_use_command(cmd, permissions) else {}
372
-
373
-	async def __send_general_help(self, interaction: Interaction) -> None:
374
-		user_permissions: Permissions = interaction.permissions
375
-		all_commands = sorted(self.get_command_list(user_permissions).items())
376
-		all_cog_tuples = [
377
-			cog_tuple
378
-			for cog_tuple in sorted(self.bot.cogs.items())
379
-			if isinstance(cog_tuple[1], BaseCog) and \
380
-			   can_use_cog(cog_tuple[1], user_permissions) and \
381
-			   (len(cog_tuple[1].settings) > 0)
382
-		]
383
-
384
-		text = f'## :information_source: Help'
385
-		if len(all_commands) + len(all_cog_tuples) == 0:
386
-			text = 'Nothing available for your permissions!'
387
-
388
-		if len(all_commands) > 0:
389
-			text += '\n### Commands'
390
-			text += '\nType `/help /commandname` for more information.'
391
-			for cmd_name, cmd in sorted(self.get_command_list(user_permissions).items()):
392
-				text += f'\n- `/{cmd_name}`: {cmd.description}'
393
-				if isinstance(cmd, Group):
394
-					subcommand_count = len(cmd.commands)
395
-					text += f' ({subcommand_count} subcommands)'
396
-
397
-		if len(all_cog_tuples) > 0:
398
-			text += '\n### Module Configuration'
399
-			for cog_name, cog in all_cog_tuples:
400
-				has_enabled = next((s for s in cog.settings if s.name == 'enabled'), None) is not None
401
-				text += f'\n- **{cog_name}**: {cog.short_description}'
402
-				if has_enabled:
403
-					text += f'\n   - `/enable {cog.config_prefix}` or `/disable {cog.config_prefix}`'
404
-				for setting in cog.settings:
405
-					if setting.name == 'enabled':
406
-						continue
407
-					text += f'\n   - `/get` or `/set {cog.config_prefix}_{setting.name}`'
408
-		await interaction.response.send_message(
409
-			text,
410
-			ephemeral=True,
411
-		)
412
-
413
-	async def __send_keyword_help(self, interaction: Interaction, matching_objects: Optional[list[Union[Command, Group, BaseCog]]]) -> None:
414
-		matching_commands = [
415
-			cmd
416
-			for cmd in matching_objects or []
417
-			if isinstance(cmd, Command) or isinstance(cmd, Group)
418
-		]
419
-		matching_cogs = [
420
-			cog
421
-			for cog in matching_objects or []
422
-			if isinstance(cog, BaseCog)
423
-		]
424
-		if len(matching_commands) + len(matching_cogs) == 0:
425
-			await interaction.response.send_message(
426
-				f'{CONFIG["failure_emoji"]} No available help topics found.',
427
-				ephemeral=True,
428
-				delete_after=10,
429
-			)
430
-			return
431
-		if len(matching_objects) == 1:
432
-			obj = matching_objects[0]
433
-			await self.__send_object_help(interaction, obj)
434
-			return
435
-
436
-		text = '## :information_source: Matching Help Topics'
437
-		if len(matching_commands) > 0:
438
-			text += '\n### Commands'
439
-			for cmd in matching_commands:
440
-				if cmd.parent:
441
-					text += f'\n- `/{cmd.parent.name} {cmd.name}`'
442
-				else:
443
-					text += f'\n- `/{cmd.name}`'
444
-		if len(matching_cogs) > 0:
445
-			text += '\n### Cogs'
446
-			for cog in matching_cogs:
447
-				text += f'\n- {cog.qualified_name}'
448
-		await interaction.response.send_message(
449
-			text,
450
-			ephemeral=True,
451
-		)
452
-
453
-	async def __send_command_help(self, interaction: Interaction, command_or_group: Union[Command, Group], addendum: Optional[str] = None) -> None:
454
-		text = ''
455
-		if addendum is not None:
456
-			text += addendum + '\n\n'
457
-		text += f'## :information_source: Command Help\n`/{command_or_group.name}`\n\n{command_or_group.description}'
458
-		if isinstance(command_or_group, Group):
459
-			subcmds: dict[str, Command] = self.get_subcommand_list(command_or_group, permissions=interaction.permissions)
460
-			if len(subcmds) > 0:
461
-				text += '\n\n### Subcommands:'
462
-				for subcmd_name, subcmd in sorted(subcmds.items()):
463
-					text += f'\n- `{subcmd_name}`: {subcmd.description}'
464
-		else:
465
-			params = command_or_group.parameters
466
-			if len(params) > 0:
467
-				text += '\n\n### Parameters:'
468
-				for param in params:
469
-					text += f'\n- `{param.name}`: {param.description}'
470
-		await interaction.response.send_message(text, ephemeral=True)
471
-
472
-	async def __send_subcommand_help(self, interaction: Interaction, group: Group, subcommand: Command) -> None:
473
-		text = f'## :information_source: Subcommand Help\n`/{group.name} {subcommand.name}`\n\n{subcommand.description}\n\n{subcommand.description}'
474
-		params = subcommand.parameters
475
-		if len(params) > 0:
476
-			text += '\n\n### Parameters:'
477
-			for param in params:
478
-				text += f'\n- `{param.name}`: {param.description}'
479
-		await interaction.response.send_message(text, ephemeral=True)
480
-
481
-	async def __send_cog_help(self, interaction: Interaction, cog: BaseCog) -> None:
482
-		text = f'## :information_source: Module Help\n{cog.qualified_name}'
483
-		if cog.description is not None:
484
-			text += f'\n{cog.description}'
485
-		settings = cog.settings
486
-		if len(settings) > 0:
487
-			text += '\n### Configuration'
488
-			enabled_setting = next((s for s in settings if s.name == 'enabled'), None)
489
-			if enabled_setting is not None:
490
-				text += f'\n- `/enable {cog.config_prefix}` or `/disable {cog.config_prefix}`'
491
-			for setting in sorted(settings, key=lambda s: s.name):
492
-				if setting.name == 'enabled':
493
-					continue
494
-				text += f'\n- `/get` or `/set {cog.config_prefix}_{setting.name}` - {setting.description}'
495
-		await interaction.response.send_message(
496
-			text,
497
-			ephemeral=True,
498
-		)

+ 381
- 0
rocketbot/cogs/helpcog.py Просмотреть файл

1
+import re
2
+from typing import Union, Optional
3
+
4
+from discord import Interaction, Permissions, AppCommandType
5
+from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
6
+
7
+from config import CONFIG
8
+from rocketbot.bot import Rocketbot
9
+from rocketbot.cogs.basecog import BaseCog
10
+from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
11
+
12
+async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
13
+	"""Autocomplete handler for top-level command names."""
14
+	choices: list[Choice] = []
15
+	try:
16
+		if current.startswith('/'):
17
+			current = current[1:]
18
+		current = current.lower().strip()
19
+		user_permissions = interaction.permissions
20
+		cmds = HelpCog.shared.get_command_list(user_permissions)
21
+		return [
22
+			Choice(name=f'/{cmdname} command', value=f'cmd:{cmdname}')
23
+			for cmdname in sorted(cmds.keys())
24
+			if len(current) == 0 or current in cmdname
25
+		][:25]
26
+	except BaseException as e:
27
+		dump_stacktrace(e)
28
+	return choices
29
+
30
+async def subcommand_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
31
+	"""Autocomplete handler for subcommand names. Command taken from previous command token."""
32
+	try:
33
+		current = current.lower().strip()
34
+		cmd_name = interaction.namespace['topic']
35
+		cmd = HelpCog.shared.object_for_help_symbol(cmd_name)
36
+		if not isinstance(cmd, Group):
37
+			return []
38
+		user_permissions = interaction.permissions
39
+		if cmd is None or not isinstance(cmd, Group):
40
+			print(f'No command found named {cmd_name}')
41
+			return []
42
+		grp = cmd
43
+		subcmds = HelpCog.shared.get_subcommand_list(grp, user_permissions)
44
+		if subcmds is None:
45
+			print(f'Subcommands for {cmd_name} was None')
46
+			return []
47
+		return [
48
+			Choice(name=f'{subcmd_name} subcommand', value=f'subcmd:{cmd_name}.{subcmd_name}')
49
+			for subcmd_name in sorted(subcmds.keys())
50
+			if len(current) == 0 or current in subcmd_name
51
+		][:25]
52
+	except BaseException as e:
53
+		dump_stacktrace(e)
54
+	return []
55
+
56
+async def cog_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
57
+	"""Autocomplete handler for cog names."""
58
+	try:
59
+		current = current.lower().strip()
60
+		return [
61
+			Choice(name=f'⚙ {cog.qualified_name} module', value=f'cog:{cog.qualified_name}')
62
+			for cog in sorted(HelpCog.shared.bot.cogs.values(), key=lambda c: c.qualified_name)
63
+			if isinstance(cog, BaseCog) and
64
+			   can_use_cog(cog, interaction.permissions) and
65
+			   (len(cog.get_commands()) > 0 or len(cog.settings) > 0) and \
66
+			   (len(current) == 0 or current in cog.qualified_name.lower())
67
+		]
68
+	except BaseException as e:
69
+		dump_stacktrace(e)
70
+	return []
71
+
72
+async def topic_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
73
+	"""Autocomplete handler that combines slash commands and cog names."""
74
+	command_choices = await command_autocomplete(interaction, current)
75
+	cog_choices = await cog_autocomplete(interaction, current)
76
+	return (command_choices + cog_choices)[:25]
77
+
78
+async def subtopic_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
79
+	"""Autocomplete handler for subtopic names. Currently just handles subcommands."""
80
+	subcommand_choices = await subcommand_autocomplete(interaction, current)
81
+	return subcommand_choices[:25]
82
+
83
+class HelpCog(BaseCog, name='Help'):
84
+	shared: Optional['HelpCog'] = None
85
+
86
+	def __init__(self, bot: Rocketbot):
87
+		super().__init__(
88
+			bot,
89
+			config_prefix='help',
90
+			short_description='Provides help on using commands and modules.'
91
+		)
92
+		HelpCog.shared = self
93
+
94
+	def __create_help_index(self) -> None:
95
+		"""
96
+		Populates self.obj_index and self.keyword_index. Bails if already
97
+		populated. Intended to be run on demand so all cogs and commands have
98
+		had time to get set up and synced.
99
+		"""
100
+		if getattr(self, 'obj_index', None) is not None:
101
+			return
102
+		self.obj_index: dict[str, Union[Command, Group, BaseCog]] = {}
103
+		self.keyword_index: dict[str, set[Union[Command, Group, BaseCog]]] = {}
104
+
105
+		def add_text_to_index(obj, text: str):
106
+			words = [
107
+				word
108
+				for word in re.split(r"[^a-zA-Z']+", text.lower())
109
+				if len(word) > 1 and word not in trivial_words
110
+			]
111
+			for word in words:
112
+				matches = self.keyword_index.get(word, set())
113
+				matches.add(obj)
114
+				self.keyword_index[word] = matches
115
+
116
+		# PyCharm not interpreting conditional return type correctly.
117
+		# noinspection PyTypeChecker
118
+		cmds: list[Union[Command, Group]] = self.bot.tree.get_commands(type=AppCommandType.chat_input)
119
+		for cmd in cmds:
120
+			key = f'cmd:{cmd.name}'
121
+			self.obj_index[key] = cmd
122
+			add_text_to_index(cmd, cmd.name)
123
+			if cmd.description:
124
+				add_text_to_index(cmd, cmd.description)
125
+			if isinstance(cmd, Group):
126
+				for subcmd in cmd.commands:
127
+					key = f'subcmd:{cmd.name}.{subcmd.name}'
128
+					self.obj_index[key] = subcmd
129
+					add_text_to_index(subcmd, subcmd.name)
130
+					if subcmd.description:
131
+						add_text_to_index(subcmd, subcmd.description)
132
+		for cog_qname, cog in self.bot.cogs.items():
133
+			if not isinstance(cog, BaseCog):
134
+				continue
135
+			key = f'cog:{cog_qname}'
136
+			self.obj_index[key] = cog
137
+			add_text_to_index(cog, cog.qualified_name)
138
+			if cog.description:
139
+				add_text_to_index(cog, cog.description)
140
+		print(self.obj_index.keys())
141
+		print(self.keyword_index.keys())
142
+
143
+	def object_for_help_symbol(self, symbol: str) -> Optional[Union[Command, Group, BaseCog]]:
144
+		self.__create_help_index()
145
+		return self.obj_index.get(symbol, None)
146
+
147
+	@command(name='help')
148
+	@guild_only()
149
+	@autocomplete(topic=topic_autocomplete, subtopic=subtopic_autocomplete)
150
+	async def help_command(self, interaction: Interaction, topic: Optional[str] = None, subtopic: Optional[str] = None) -> None:
151
+		"""
152
+		Shows help for using commands and subcommands and configuring modules.
153
+
154
+		`/help` will show a list of top-level topics.
155
+
156
+		`/help /<command_name>` will show help about a specific command or
157
+		list a command's subcommands.
158
+
159
+		`/help /<command_name> <subcommand_name>` will show help about a
160
+		specific subcommand.
161
+
162
+		`/help <module_name>` will show help about configuring a module.
163
+
164
+		`/help <keywords>` will do a text search for topics.
165
+
166
+		Parameters
167
+		----------
168
+		interaction: Interaction
169
+		topic: Optional[str]
170
+			Optional topic to get specific help for. Getting help on a command can optionally start with a leading slash.
171
+		subtopic: Optional[str]
172
+			Optional subtopic to get specific help for.
173
+		"""
174
+		print(f'help_command(interaction, {topic}, {subtopic})')
175
+
176
+		# General help
177
+		if topic is None:
178
+			await self.__send_general_help(interaction)
179
+			return
180
+
181
+		# Specific object reference
182
+		obj = self.object_for_help_symbol(subtopic) if subtopic else self.object_for_help_symbol(topic)
183
+		if obj:
184
+			await self.__send_object_help(interaction, obj)
185
+			return
186
+
187
+		# Text search
188
+		keywords = [
189
+			word
190
+			for word in re.split(r"[^a-zA-Z']+", topic.lower())
191
+			if len(word) > 0 and word not in trivial_words
192
+		]
193
+		matching_objects_set = None
194
+		for keyword in keywords:
195
+			objs = self.keyword_index.get(keyword, None)
196
+			if objs is not None:
197
+				if matching_objects_set is None:
198
+					matching_objects_set = objs
199
+				else:
200
+					matching_objects_set = matching_objects_set & objs
201
+		accessible_objects = [
202
+			obj
203
+			for obj in matching_objects_set or {}
204
+			if ((isinstance(obj, Command) or isinstance(obj, Group)) and can_use_command(obj, interaction.permissions)) or \
205
+			   (isinstance(obj, BaseCog) and can_use_cog(obj, interaction.permissions))
206
+		]
207
+		await self.__send_keyword_help(interaction, accessible_objects)
208
+
209
+	async def __send_object_help(self, interaction: Interaction, obj: Union[Command, Group, BaseCog]) -> None:
210
+		if isinstance(obj, Command):
211
+			if obj.parent:
212
+				await self.__send_subcommand_help(interaction, obj.parent, obj)
213
+			else:
214
+				await self.__send_command_help(interaction, obj)
215
+			return
216
+		if isinstance(obj, Group):
217
+			await self.__send_command_help(interaction, obj)
218
+			return
219
+		if isinstance(obj, BaseCog):
220
+			await self.__send_cog_help(interaction, obj)
221
+			return
222
+		print(f'No help for object {obj}')
223
+		await interaction.response.send_message(
224
+			f'{CONFIG["failure_emoji"]} Failed to get help info.',
225
+			ephemeral=True,
226
+			delete_after=10,
227
+		)
228
+
229
+	def get_command_list(self, permissions: Optional[Permissions] = None) -> dict[str, Union[Command, Group]]:
230
+		return { cmd.name: cmd for cmd in self.bot.tree.get_commands() if can_use_command(cmd, permissions) }
231
+
232
+	def get_subcommand_list(self, cmd: Group, permissions: Optional[Permissions] = None) -> dict[str, Command]:
233
+		return {
234
+			subcmd.name: subcmd
235
+			for subcmd in cmd.commands
236
+			if can_use_command(subcmd, permissions)
237
+		} if can_use_command(cmd, permissions) else {}
238
+
239
+	async def __send_general_help(self, interaction: Interaction) -> None:
240
+		user_permissions: Permissions = interaction.permissions
241
+		all_commands = sorted(self.get_command_list(user_permissions).items())
242
+		all_cog_tuples: list[tuple[str, BaseCog]] = [
243
+			cog_tuple
244
+			for cog_tuple in sorted(self.basecog_map.items())
245
+			if can_use_cog(cog_tuple[1], user_permissions) and \
246
+			   (len(cog_tuple[1].settings) > 0)
247
+		]
248
+
249
+		text = f'## :information_source: Help'
250
+		if len(all_commands) + len(all_cog_tuples) == 0:
251
+			text = 'Nothing available for your permissions!'
252
+
253
+		if len(all_commands) > 0:
254
+			text += '\n### Commands'
255
+			text += '\nType `/help /commandname` for more information.'
256
+			for cmd_name, cmd in sorted(self.get_command_list(user_permissions).items()):
257
+				text += f'\n- `/{cmd_name}`: {cmd.description}'
258
+				if isinstance(cmd, Group):
259
+					subcommand_count = len(cmd.commands)
260
+					text += f' ({subcommand_count} subcommands)'
261
+
262
+		if len(all_cog_tuples) > 0:
263
+			text += '\n### Module Configuration'
264
+			for cog_name, cog in all_cog_tuples:
265
+				has_enabled = next((s for s in cog.settings if s.name == 'enabled'), None) is not None
266
+				text += f'\n- **{cog_name}**: {cog.short_description}'
267
+				if has_enabled:
268
+					text += f'\n   - `/enable {cog.config_prefix}` or `/disable {cog.config_prefix}`'
269
+				for setting in cog.settings:
270
+					if setting.name == 'enabled':
271
+						continue
272
+					text += f'\n   - `/get` or `/set {cog.config_prefix}_{setting.name}`'
273
+		await interaction.response.send_message(
274
+			text,
275
+			ephemeral=True,
276
+		)
277
+
278
+	async def __send_keyword_help(self, interaction: Interaction, matching_objects: Optional[list[Union[Command, Group, BaseCog]]]) -> None:
279
+		matching_commands = [
280
+			cmd
281
+			for cmd in matching_objects or []
282
+			if isinstance(cmd, Command) or isinstance(cmd, Group)
283
+		]
284
+		matching_cogs = [
285
+			cog
286
+			for cog in matching_objects or []
287
+			if isinstance(cog, BaseCog)
288
+		]
289
+		if len(matching_commands) + len(matching_cogs) == 0:
290
+			await interaction.response.send_message(
291
+				f'{CONFIG["failure_emoji"]} No available help topics found.',
292
+				ephemeral=True,
293
+				delete_after=10,
294
+			)
295
+			return
296
+		if len(matching_objects) == 1:
297
+			obj = matching_objects[0]
298
+			await self.__send_object_help(interaction, obj)
299
+			return
300
+
301
+		text = '## :information_source: Matching Help Topics'
302
+		if len(matching_commands) > 0:
303
+			text += '\n### Commands'
304
+			for cmd in matching_commands:
305
+				if cmd.parent:
306
+					text += f'\n- `/{cmd.parent.name} {cmd.name}`'
307
+				else:
308
+					text += f'\n- `/{cmd.name}`'
309
+		if len(matching_cogs) > 0:
310
+			text += '\n### Cogs'
311
+			for cog in matching_cogs:
312
+				text += f'\n- {cog.qualified_name}'
313
+		await interaction.response.send_message(
314
+			text,
315
+			ephemeral=True,
316
+		)
317
+
318
+	async def __send_command_help(self, interaction: Interaction, command_or_group: Union[Command, Group], addendum: Optional[str] = None) -> None:
319
+		text = ''
320
+		if addendum is not None:
321
+			text += addendum + '\n\n'
322
+		text += f'## :information_source: Command Help\n`/{command_or_group.name}`\n\n{command_or_group.description}'
323
+		if isinstance(command_or_group, Group):
324
+			subcmds: dict[str, Command] = self.get_subcommand_list(command_or_group, permissions=interaction.permissions)
325
+			if len(subcmds) > 0:
326
+				text += '\n\n### Subcommands:'
327
+				for subcmd_name, subcmd in sorted(subcmds.items()):
328
+					text += f'\n- `{subcmd_name}`: {subcmd.description}'
329
+		else:
330
+			params = command_or_group.parameters
331
+			if len(params) > 0:
332
+				text += '\n\n### Parameters:'
333
+				for param in params:
334
+					text += f'\n- `{param.name}`: {param.description}'
335
+		await interaction.response.send_message(text, ephemeral=True)
336
+
337
+	async def __send_subcommand_help(self, interaction: Interaction, group: Group, subcommand: Command) -> None:
338
+		text = f'## :information_source: Subcommand Help\n`/{group.name} {subcommand.name}`\n\n{subcommand.description}\n\n{subcommand.description}'
339
+		params = subcommand.parameters
340
+		if len(params) > 0:
341
+			text += '\n\n### Parameters:'
342
+			for param in params:
343
+				text += f'\n- `{param.name}`: {param.description}'
344
+		await interaction.response.send_message(text, ephemeral=True)
345
+
346
+	async def __send_cog_help(self, interaction: Interaction, cog: BaseCog) -> None:
347
+		text = f'## :information_source: Module Help\n{cog.qualified_name}'
348
+		if cog.description is not None:
349
+			text += f'\n{cog.description}'
350
+		settings = cog.settings
351
+		if len(settings) > 0:
352
+			text += '\n### Configuration'
353
+			enabled_setting = next((s for s in settings if s.name == 'enabled'), None)
354
+			if enabled_setting is not None:
355
+				text += f'\n- `/enable {cog.config_prefix}` or `/disable {cog.config_prefix}`'
356
+			for setting in sorted(settings, key=lambda s: s.name):
357
+				if setting.name == 'enabled':
358
+					continue
359
+				text += f'\n- `/get` or `/set {cog.config_prefix}_{setting.name}` - {setting.description}'
360
+		await interaction.response.send_message(
361
+			text,
362
+			ephemeral=True,
363
+		)
364
+
365
+# Exclusions from keyword indexing
366
+trivial_words = {
367
+	'a', 'an', 'and', 'are', "aren't", 'as', 'by', 'can', 'for', 'have', 'if', 'in',
368
+	'is', 'it', 'its', "it's", 'not', 'of', 'on', 'or', 'than', 'that', 'the', 'then',
369
+	'there', 'them', 'they', "they're", 'this', 'to', 'when', 'with',
370
+}
371
+
372
+def can_use_command(cmd: Union[Group, Command], user_permissions: Optional[Permissions]) -> bool:
373
+	if user_permissions is None:
374
+		return False
375
+	if cmd.parent and not can_use_command(cmd.parent, user_permissions):
376
+		return False
377
+	return cmd.default_permissions is None or cmd.default_permissions.is_subset(user_permissions)
378
+
379
+def can_use_cog(cog: BaseCog, user_permissions: Optional[Permissions]) -> bool:
380
+	# "Using" a cog for now means configuring it, and only mods can configure cogs.
381
+	return user_permissions is not None and MOD_PERMISSIONS.is_subset(user_permissions)

Загрузка…
Отмена
Сохранить