Pārlūkot izejas kodu

Improved single input help search

pull/13/head
Rocketsoup 2 mēnešus atpakaļ
vecāks
revīzija
d69b755174
2 mainītis faili ar 140 papildinājumiem un 55 dzēšanām
  1. 1
    1
      rocketbot/cogs/basecog.py
  2. 139
    54
      rocketbot/cogs/helpcog.py

+ 1
- 1
rocketbot/cogs/basecog.py Parādīt failu

75
 		"""
75
 		"""
76
 		return [
76
 		return [
77
 			bcog
77
 			bcog
78
-			for bcog in self.bot.cogs.values()
78
+			for bcog in sorted(self.bot.cogs.values(), key=lambda c: c.qualified_name)
79
 			if isinstance(bcog, BaseCog)
79
 			if isinstance(bcog, BaseCog)
80
 		]
80
 		]
81
 
81
 

+ 139
- 54
rocketbot/cogs/helpcog.py Parādīt failu

1
+"""Provides help commands for getting info on using other commands and configuration."""
1
 import re
2
 import re
2
 from typing import Union, Optional
3
 from typing import Union, Optional
3
 
4
 
4
 from discord import Interaction, Permissions, AppCommandType
5
 from discord import Interaction, Permissions, AppCommandType
5
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
6
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
7
+from discord.ext.commands import cog
6
 
8
 
7
 from config import CONFIG
9
 from config import CONFIG
8
 from rocketbot.bot import Rocketbot
10
 from rocketbot.bot import Rocketbot
9
 from rocketbot.cogs.basecog import BaseCog
11
 from rocketbot.cogs.basecog import BaseCog
10
 from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
12
 from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
11
 
13
 
14
+HelpTopic = Union[Command, Group, BaseCog]
15
+
16
+def choice_from_obj(obj: HelpTopic, include_full_command: bool = False) -> Choice:
17
+	if isinstance(obj, BaseCog):
18
+		return Choice(name=f'⚙ {obj.qualified_name}', value=f'cog:{obj.qualified_name}')
19
+	if isinstance(obj, Group):
20
+		return Choice(name=f'/{obj.name}', value=f'cmd:{obj.name}')
21
+	if isinstance(obj, Command):
22
+		if obj.parent:
23
+			if include_full_command:
24
+				return Choice(name=f'/{obj.parent.name} {obj.name}', value=f'subcmd:{obj.parent.name}.{obj.name}')
25
+			return Choice(name=f'{obj.name}', value=f'subcmd:{obj.name}')
26
+		return Choice(name=f'/{obj.name}', value=f'cmd:{obj.name}')
27
+	return Choice(name='', value='')
28
+
12
 async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
29
 async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
13
 	"""Autocomplete handler for top-level command names."""
30
 	"""Autocomplete handler for top-level command names."""
14
 	choices: list[Choice] = []
31
 	choices: list[Choice] = []
31
 	"""Autocomplete handler for subcommand names. Command taken from previous command token."""
48
 	"""Autocomplete handler for subcommand names. Command taken from previous command token."""
32
 	try:
49
 	try:
33
 		current = current.lower().strip()
50
 		current = current.lower().strip()
34
-		cmd_name = interaction.namespace['topic']
51
+		cmd_name = interaction.namespace.get('topic', None)
35
 		cmd = HelpCog.shared.object_for_help_symbol(cmd_name)
52
 		cmd = HelpCog.shared.object_for_help_symbol(cmd_name)
36
-		if not isinstance(cmd, Group):
53
+		if isinstance(cmd, Command):
54
+			# No subcommands
37
 			return []
55
 			return []
38
 		user_permissions = interaction.permissions
56
 		user_permissions = interaction.permissions
39
 		if cmd is None or not isinstance(cmd, Group):
57
 		if cmd is None or not isinstance(cmd, Group):
40
-			print(f'No command found named {cmd_name}')
41
 			return []
58
 			return []
42
 		grp = cmd
59
 		grp = cmd
43
 		subcmds = HelpCog.shared.get_subcommand_list(grp, user_permissions)
60
 		subcmds = HelpCog.shared.get_subcommand_list(grp, user_permissions)
44
 		if subcmds is None:
61
 		if subcmds is None:
45
-			print(f'Subcommands for {cmd_name} was None')
46
 			return []
62
 			return []
47
 		return [
63
 		return [
48
-			Choice(name=f'{subcmd_name} subcommand', value=f'subcmd:{cmd.name}.{subcmd_name}')
49
-			for subcmd_name in sorted(subcmds.keys())
64
+			choice_from_obj(subcmd)
65
+			for subcmd_name, subcmd in sorted(subcmds.items())
50
 			if len(current) == 0 or current in subcmd_name
66
 			if len(current) == 0 or current in subcmd_name
51
 		][:25]
67
 		][:25]
52
 	except BaseException as e:
68
 	except BaseException as e:
58
 	try:
74
 	try:
59
 		current = current.lower().strip()
75
 		current = current.lower().strip()
60
 		return [
76
 		return [
61
-			Choice(name=f'⚙ {cog.qualified_name} module', value=f'cog:{cog.qualified_name}')
77
+			choice_from_obj(cog)
62
 			for cog in sorted(HelpCog.shared.bot.cogs.values(), key=lambda c: c.qualified_name)
78
 			for cog in sorted(HelpCog.shared.bot.cogs.values(), key=lambda c: c.qualified_name)
63
 			if isinstance(cog, BaseCog) and
79
 			if isinstance(cog, BaseCog) and
64
 			   can_use_cog(cog, interaction.permissions) and
80
 			   can_use_cog(cog, interaction.permissions) and
80
 	subcommand_choices = await subcommand_autocomplete(interaction, current)
96
 	subcommand_choices = await subcommand_autocomplete(interaction, current)
81
 	return subcommand_choices[:25]
97
 	return subcommand_choices[:25]
82
 
98
 
99
+async def search_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
100
+	try:
101
+		if len(current) == 0:
102
+			return [
103
+				choice_from_obj(obj, include_full_command=True)
104
+				for obj in HelpCog.shared.all_accessible_objects(interaction.permissions)
105
+			]
106
+		return [
107
+			choice_from_obj(obj, include_full_command=True)
108
+			for obj in HelpCog.shared.objects_for_keywords(current, interaction.permissions)
109
+		]
110
+	except BaseException as e:
111
+		dump_stacktrace(e)
112
+		return []
113
+
83
 class HelpCog(BaseCog, name='Help'):
114
 class HelpCog(BaseCog, name='Help'):
84
 	shared: Optional['HelpCog'] = None
115
 	shared: Optional['HelpCog'] = None
85
 
116
 
99
 		"""
130
 		"""
100
 		if getattr(self, 'obj_index', None) is not None:
131
 		if getattr(self, 'obj_index', None) is not None:
101
 			return
132
 			return
102
-		self.obj_index: dict[str, Union[Command, Group, BaseCog]] = {}
103
-		self.keyword_index: dict[str, set[Union[Command, Group, BaseCog]]] = {}
133
+		self.obj_index: dict[str, HelpTopic] = {}
134
+		self.keyword_index: dict[str, set[HelpTopic]] = {}
104
 
135
 
105
 		def add_text_to_index(obj, text: str):
136
 		def add_text_to_index(obj, text: str):
106
 			words = [
137
 			words = [
113
 				matches.add(obj)
144
 				matches.add(obj)
114
 				self.keyword_index[word] = matches
145
 				self.keyword_index[word] = matches
115
 
146
 
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)
147
+		cmds = self.all_commands()
119
 		for cmd in cmds:
148
 		for cmd in cmds:
120
 			key = f'cmd:{cmd.name}'
149
 			key = f'cmd:{cmd.name}'
121
 			self.obj_index[key] = cmd
150
 			self.obj_index[key] = cmd
126
 				for subcmd in cmd.commands:
155
 				for subcmd in cmd.commands:
127
 					key = f'subcmd:{cmd.name}.{subcmd.name}'
156
 					key = f'subcmd:{cmd.name}.{subcmd.name}'
128
 					self.obj_index[key] = subcmd
157
 					self.obj_index[key] = subcmd
158
+					add_text_to_index(subcmd, cmd.name)
129
 					add_text_to_index(subcmd, subcmd.name)
159
 					add_text_to_index(subcmd, subcmd.name)
130
 					if subcmd.description:
160
 					if subcmd.description:
131
 						add_text_to_index(subcmd, subcmd.description)
161
 						add_text_to_index(subcmd, subcmd.description)
138
 			if cog.description:
168
 			if cog.description:
139
 				add_text_to_index(cog, cog.description)
169
 				add_text_to_index(cog, cog.description)
140
 
170
 
141
-	def object_for_help_symbol(self, symbol: str) -> Optional[Union[Command, Group, BaseCog]]:
171
+	def object_for_help_symbol(self, symbol: str) -> Optional[HelpTopic]:
142
 		self.__create_help_index()
172
 		self.__create_help_index()
143
 		return self.obj_index.get(symbol, None)
173
 		return self.obj_index.get(symbol, None)
144
 
174
 
175
+	def all_commands(self) -> list[Union[Command, Group]]:
176
+		# PyCharm not interpreting conditional return type correctly.
177
+		# noinspection PyTypeChecker
178
+		cmds: list[Union[Command, Group]] = self.bot.tree.get_commands(type=AppCommandType.chat_input)
179
+		return sorted(cmds, key=lambda cmd: cmd.name)
180
+
181
+	def all_accessible_commands(self, permissions: Optional[Permissions]) -> list[Union[Command, Group]]:
182
+		return [
183
+			cmd
184
+			for cmd in self.all_commands()
185
+			if can_use_command(cmd, permissions)
186
+		]
187
+
188
+	def all_accessible_subcommands(self, permissions: Optional[Permissions]) -> list[Command]:
189
+		cmds = self.all_accessible_commands(permissions)
190
+		subcmds: list[Command] = []
191
+		for cmd in cmds:
192
+			if isinstance(cmd, Group):
193
+				for subcmd in sorted(cmd.commands, key=lambda cmd: cmd.name):
194
+					if can_use_command(subcmd, permissions):
195
+						subcmds.append(subcmd)
196
+		return subcmds
197
+
198
+	def all_accessible_cogs(self, permissions: Optional[Permissions]) -> list[BaseCog]:
199
+		return [
200
+			cog
201
+			for cog in self.basecogs
202
+			if can_use_cog(cog, permissions)
203
+		]
204
+
205
+	def all_accessible_objects(self, permissions: Optional[Permissions], *,
206
+							   include_cogs: bool = True,
207
+							   include_commands: bool = True,
208
+							   include_subcommands: bool = True) -> list[HelpTopic]:
209
+		objs = []
210
+		if include_cogs:
211
+			objs += self.all_accessible_cogs(permissions)
212
+		if include_commands:
213
+			objs += self.all_accessible_commands(permissions)
214
+		if include_subcommands:
215
+			objs += self.all_accessible_subcommands(permissions)
216
+		return objs
217
+
218
+	def objects_for_keywords(self, search: str, permissions: Optional[Permissions]) -> list[HelpTopic]:
219
+		self.__create_help_index()
220
+
221
+		# Break into words (or word fragments)
222
+		words: list[str] = [
223
+			word.lower()
224
+			for word in re.split(r"[^a-zA-Z']+", search)
225
+			# if len(word) > 1 and word not in trivial_words
226
+		]
227
+
228
+		# FIXME: This is a super weird way of doing this. Converting word fragments
229
+		# to known indexed keywords, then collecting those associated results. Should
230
+		# just keep corpuses of searchable, normalized text for each topic and do a
231
+		# direct `in` test.
232
+		matching_objects_set = None
233
+		for word in words:
234
+			word_matches = set()
235
+			for k in self.keyword_index.keys():
236
+				if word in k:
237
+					objs = self.keyword_index.get(k, None)
238
+					if objs is not None:
239
+						word_matches.update(objs)
240
+			if matching_objects_set is None:
241
+				matching_objects_set = word_matches
242
+			else:
243
+				matching_objects_set = matching_objects_set & word_matches
244
+
245
+		# Filter by accessibility
246
+		accessible_objects = [
247
+			obj
248
+			for obj in matching_objects_set or {}
249
+			if ((isinstance(obj, Command) or isinstance(obj, Group)) and can_use_command(obj, permissions)) or \
250
+			   (isinstance(obj, BaseCog) and can_use_cog(obj, permissions))
251
+		]
252
+
253
+		# Sort and return
254
+		return sorted(accessible_objects, key=lambda obj: (
255
+			isinstance(obj, Command),
256
+			isinstance(obj, BaseCog),
257
+			obj.qualified_name if isinstance(obj, BaseCog) else obj.name
258
+		))
259
+
145
 	@command(name='help')
260
 	@command(name='help')
146
 	@guild_only()
261
 	@guild_only()
147
-	@autocomplete(topic=topic_autocomplete, subtopic=subtopic_autocomplete)
148
-	async def help_command(self, interaction: Interaction, topic: Optional[str] = None, subtopic: Optional[str] = None) -> None:
262
+	@autocomplete(search=search_autocomplete)
263
+	async def help_command(self, interaction: Interaction, search: Optional[str]) -> None:
149
 		"""
264
 		"""
150
 		Shows help for using commands and subcommands and configuring modules.
265
 		Shows help for using commands and subcommands and configuring modules.
151
 
266
 
164
 		Parameters
279
 		Parameters
165
 		----------
280
 		----------
166
 		interaction: Interaction
281
 		interaction: Interaction
167
-		topic: Optional[str]
168
-			optional command, module, or keywords to get specific help for
169
-		subtopic: Optional[str]
170
-			optional subcommand to get specific help for
282
+		search: Optional[str]
283
+			search terms
171
 		"""
284
 		"""
172
-		print(f'help_command(interaction, {topic}, {subtopic})')
173
-
174
-		# General help
175
-		if topic is None:
285
+		if search is None:
176
 			await self.__send_general_help(interaction)
286
 			await self.__send_general_help(interaction)
177
 			return
287
 			return
178
-
179
-		# Specific object reference
180
-		obj = self.object_for_help_symbol(subtopic) if subtopic else self.object_for_help_symbol(topic)
288
+		obj = self.object_for_help_symbol(search)
181
 		if obj:
289
 		if obj:
182
 			await self.__send_object_help(interaction, obj)
290
 			await self.__send_object_help(interaction, obj)
183
 			return
291
 			return
292
+		matches = self.objects_for_keywords(search, interaction.permissions)
293
+		await self.__send_keyword_help(interaction, matches)
184
 
294
 
185
-		# Text search
186
-		keywords = [
187
-			word
188
-			for word in re.split(r"[^a-zA-Z']+", topic.lower())
189
-			if len(word) > 0 and word not in trivial_words
190
-		]
191
-		matching_objects_set = None
192
-		for keyword in keywords:
193
-			objs = self.keyword_index.get(keyword, None)
194
-			if objs is not None:
195
-				if matching_objects_set is None:
196
-					matching_objects_set = objs
197
-				else:
198
-					matching_objects_set = matching_objects_set & objs
199
-		accessible_objects = [
200
-			obj
201
-			for obj in matching_objects_set or {}
202
-			if ((isinstance(obj, Command) or isinstance(obj, Group)) and can_use_command(obj, interaction.permissions)) or \
203
-			   (isinstance(obj, BaseCog) and can_use_cog(obj, interaction.permissions))
204
-		]
205
-		await self.__send_keyword_help(interaction, accessible_objects)
206
-
207
-	async def __send_object_help(self, interaction: Interaction, obj: Union[Command, Group, BaseCog]) -> None:
295
+	async def __send_object_help(self, interaction: Interaction, obj: HelpTopic) -> None:
208
 		if isinstance(obj, Command):
296
 		if isinstance(obj, Command):
209
 			if obj.parent:
297
 			if obj.parent:
210
 				await self.__send_subcommand_help(interaction, obj.parent, obj)
298
 				await self.__send_subcommand_help(interaction, obj.parent, obj)
217
 		if isinstance(obj, BaseCog):
305
 		if isinstance(obj, BaseCog):
218
 			await self.__send_cog_help(interaction, obj)
306
 			await self.__send_cog_help(interaction, obj)
219
 			return
307
 			return
220
-		print(f'No help for object {obj}')
308
+		self.log(interaction.guild, f'No help for object {obj}')
221
 		await interaction.response.send_message(
309
 		await interaction.response.send_message(
222
 			f'{CONFIG["failure_emoji"]} Failed to get help info.',
310
 			f'{CONFIG["failure_emoji"]} Failed to get help info.',
223
 			ephemeral=True,
311
 			ephemeral=True,
273
 			ephemeral=True,
361
 			ephemeral=True,
274
 		)
362
 		)
275
 
363
 
276
-	async def __send_keyword_help(self, interaction: Interaction, matching_objects: Optional[list[Union[Command, Group, BaseCog]]]) -> None:
364
+	async def __send_keyword_help(self, interaction: Interaction, matching_objects: Optional[list[HelpTopic]]) -> None:
277
 		matching_commands = [
365
 		matching_commands = [
278
 			cmd
366
 			cmd
279
 			for cmd in matching_objects or []
367
 			for cmd in matching_objects or []
314
 		)
402
 		)
315
 
403
 
316
 	async def __send_command_help(self, interaction: Interaction, command_or_group: Union[Command, Group], addendum: Optional[str] = None) -> None:
404
 	async def __send_command_help(self, interaction: Interaction, command_or_group: Union[Command, Group], addendum: Optional[str] = None) -> None:
317
-		if isinstance(command_or_group, Command):
318
-			print(f"Doc:\n{command_or_group.callback.__doc__}")
319
 		text = ''
405
 		text = ''
320
 		if addendum is not None:
406
 		if addendum is not None:
321
 			text += addendum + '\n\n'
407
 			text += addendum + '\n\n'
375
 				if setting.name == 'enabled':
461
 				if setting.name == 'enabled':
376
 					continue
462
 					continue
377
 				text += f'\n- `/get` or `/set {cog.config_prefix}_{setting.name}` - {setting.brief}'
463
 				text += f'\n- `/get` or `/set {cog.config_prefix}_{setting.name}` - {setting.brief}'
378
-		print(text)
379
 		await interaction.response.send_message(
464
 		await interaction.response.send_message(
380
 			text,
465
 			text,
381
 			ephemeral=True,
466
 			ephemeral=True,

Notiek ielāde…
Atcelt
Saglabāt