Parcourir la source

Help search less silly

pull/13/head
Rocketsoup il y a 2 mois
Parent
révision
36068989b8
1 fichiers modifiés avec 57 ajouts et 50 suppressions
  1. 57
    50
      rocketbot/cogs/helpcog.py

+ 57
- 50
rocketbot/cogs/helpcog.py Voir le fichier

1
 """Provides help commands for getting info on using other commands and configuration."""
1
 """Provides help commands for getting info on using other commands and configuration."""
2
 import re
2
 import re
3
-from typing import Union, Optional
3
+import time
4
+from typing import Union, Optional, TypedDict
4
 
5
 
5
 from discord import Interaction, Permissions, AppCommandType, ButtonStyle, InteractionResponse
6
 from discord import Interaction, Permissions, AppCommandType, ButtonStyle, InteractionResponse
6
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
7
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
12
 from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
13
 from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
13
 
14
 
14
 HelpTopic = Union[Command, Group, BaseCog]
15
 HelpTopic = Union[Command, Group, BaseCog]
16
+class HelpMeta(TypedDict):
17
+	id: str
18
+	text: str
19
+	topic: HelpTopic
15
 
20
 
16
 # Potential place to break text neatly in large help content
21
 # Potential place to break text neatly in large help content
17
 PAGE_BREAK = '\f'
22
 PAGE_BREAK = '\f'
39
 		return [
44
 		return [
40
 			choice_from_topic(topic, include_full_command=True)
45
 			choice_from_topic(topic, include_full_command=True)
41
 			for topic in HelpCog.shared.topics_for_keywords(current, interaction.permissions)
46
 			for topic in HelpCog.shared.topics_for_keywords(current, interaction.permissions)
42
-		]
47
+		][:25]
43
 	except BaseException as e:
48
 	except BaseException as e:
44
 		dump_stacktrace(e)
49
 		dump_stacktrace(e)
45
 		return []
50
 		return []
57
 
62
 
58
 	def __create_help_index(self) -> None:
63
 	def __create_help_index(self) -> None:
59
 		"""
64
 		"""
60
-		Populates self.topic_index and self.keyword_index. Bails if already
65
+		Populates self.id_to_topic and self.keyword_index. Bails if already
61
 		populated. Intended to be run on demand so all cogs and commands have
66
 		populated. Intended to be run on demand so all cogs and commands have
62
 		had time to get set up and synced.
67
 		had time to get set up and synced.
63
 		"""
68
 		"""
64
-		if getattr(self, 'topic_index', None) is not None:
69
+		if getattr(self, 'id_to_topic', None) is not None:
65
 			return
70
 			return
66
-		self.topic_index: dict[str, HelpTopic] = {}
67
-		self.keyword_index: dict[str, set[HelpTopic]] = {}
71
+		self.id_to_topic: dict[str, HelpTopic] = {}
72
+		self.topics: list[HelpMeta] = []
68
 
73
 
69
-		def add_text_to_index(topic: HelpTopic, text: str):
70
-			words = [
74
+		def process_text(t: str) -> str:
75
+			return ' '.join([
71
 				word
76
 				word
72
-				for word in re.split(r"[^a-zA-Z']+", text.lower())
73
-				if len(word) > 1 and word not in trivial_words
74
-			]
75
-			for word in words:
76
-				matches = self.keyword_index.get(word, set())
77
-				matches.add(topic)
78
-				self.keyword_index[word] = matches
77
+				for word in re.split(r"[^a-z']+", t.lower())
78
+				if word not in trivial_words
79
+			]).strip()
79
 
80
 
80
 		cmds = self.all_commands()
81
 		cmds = self.all_commands()
81
 		for cmd in cmds:
82
 		for cmd in cmds:
82
-			self.topic_index[f'cmd:{cmd.name}'] = cmd
83
-			self.topic_index[f'/{cmd.name}'] = cmd
84
-			add_text_to_index(cmd, cmd.name)
83
+			key = f'cmd:{cmd.name}'
84
+			self.id_to_topic[key] = cmd
85
+			self.id_to_topic[f'/{cmd.name}'] = cmd
86
+			text = cmd.name
85
 			if cmd.description:
87
 			if cmd.description:
86
-				add_text_to_index(cmd, cmd.description)
88
+				text += f' {cmd.description}'
89
+			if cmd.extras.get('long_description', None):
90
+				text += f' {cmd.extras["long_description"]}'
91
+			self.topics.append({ 'id': key, 'text': process_text(text), 'topic': cmd })
87
 			if isinstance(cmd, Group):
92
 			if isinstance(cmd, Group):
88
 				for subcmd in cmd.commands:
93
 				for subcmd in cmd.commands:
89
-					self.topic_index[f'subcmd:{cmd.name}.{subcmd.name}'] = subcmd
90
-					self.topic_index[f'/{cmd.name} {subcmd.name}'] = subcmd
91
-					add_text_to_index(subcmd, cmd.name)
92
-					add_text_to_index(subcmd, subcmd.name)
94
+					key = f'subcmd:{cmd.name}.{subcmd.name}'
95
+					self.id_to_topic[key] = subcmd
96
+					self.id_to_topic[f'/{cmd.name} {subcmd.name}'] = subcmd
97
+					text = cmd.name
98
+					text += f' {subcmd.name}'
93
 					if subcmd.description:
99
 					if subcmd.description:
94
-						add_text_to_index(subcmd, subcmd.description)
100
+						text += f' {subcmd.description}'
101
+					if subcmd.extras.get('long_description', None):
102
+						text += f' {subcmd.extras["long_description"]}'
103
+					self.topics.append({ 'id': key, 'text': process_text(text), 'topic': subcmd })
95
 		for cog_qname, cog in self.bot.cogs.items():
104
 		for cog_qname, cog in self.bot.cogs.items():
96
 			if not isinstance(cog, BaseCog):
105
 			if not isinstance(cog, BaseCog):
97
 				continue
106
 				continue
98
 			key = f'cog:{cog_qname}'
107
 			key = f'cog:{cog_qname}'
99
-			self.topic_index[key] = cog
100
-			add_text_to_index(cog, cog.qualified_name)
101
-			if cog.description:
102
-				add_text_to_index(cog, cog.description)
108
+			self.id_to_topic[key] = cog
109
+			text = cog.qualified_name
110
+			if cog.short_description:
111
+				text += f' {cog.short_description}'
112
+			if cog.long_description:
113
+				text += f' {cog.long_description}'
114
+			self.topics.append({ 'id': key, 'text': process_text(text), 'topic': cog })
103
 
115
 
104
 	def topic_for_help_symbol(self, symbol: str) -> Optional[HelpTopic]:
116
 	def topic_for_help_symbol(self, symbol: str) -> Optional[HelpTopic]:
105
 		self.__create_help_index()
117
 		self.__create_help_index()
106
-		return self.topic_index.get(symbol, None)
118
+		return self.id_to_topic.get(symbol, None)
107
 
119
 
108
 	def all_commands(self) -> list[Union[Command, Group]]:
120
 	def all_commands(self) -> list[Union[Command, Group]]:
109
 		# PyCharm not interpreting conditional return type correctly.
121
 		# PyCharm not interpreting conditional return type correctly.
149
 		return topics
161
 		return topics
150
 
162
 
151
 	def topics_for_keywords(self, search: str, permissions: Optional[Permissions]) -> list[HelpTopic]:
163
 	def topics_for_keywords(self, search: str, permissions: Optional[Permissions]) -> list[HelpTopic]:
164
+		start_time = time.perf_counter()
152
 		self.__create_help_index()
165
 		self.__create_help_index()
153
 
166
 
154
 		# Break into words (or word fragments)
167
 		# Break into words (or word fragments)
155
 		words: list[str] = [
168
 		words: list[str] = [
156
-			word.lower()
157
-			for word in re.split(r"[^a-zA-Z']+", search)
158
-			# if len(word) > 1 and word not in trivial_words
169
+			word
170
+			for word in re.split(r"[^a-z']+", search.lower())
159
 		]
171
 		]
160
 
172
 
161
-		# FIXME: This is a super weird way of doing this. Converting word fragments
162
-		# to known indexed keywords, then collecting those associated results. Should
163
-		# just keep corpuses of searchable, normalized text for each topic and do a
164
-		# direct `in` test.
165
-		matching_topics_set = None
166
-		for word in words:
167
-			word_matches = set()
168
-			for k in self.keyword_index.keys():
169
-				if word in k:
170
-					topics = self.keyword_index.get(k, None)
171
-					if topics is not None:
172
-						word_matches.update(topics)
173
-			if matching_topics_set is None:
174
-				matching_topics_set = word_matches
175
-			else:
176
-				matching_topics_set = matching_topics_set & word_matches
173
+		# Find matches
174
+		def topic_matches(meta: HelpMeta) -> bool:
175
+			for word in words:
176
+				if word not in meta['text']:
177
+					return False
178
+			return True
179
+		matching_topics: list[HelpTopic] = [ topic['topic'] for topic in self.topics if topic_matches(topic) ]
177
 
180
 
178
 		# Filter by accessibility
181
 		# Filter by accessibility
179
 		accessible_topics = [
182
 		accessible_topics = [
180
 			topic
183
 			topic
181
-			for topic in matching_topics_set or {}
184
+			for topic in matching_topics
182
 			if ((isinstance(topic, Command) or isinstance(topic, Group)) and can_use_command(topic, permissions)) or \
185
 			if ((isinstance(topic, Command) or isinstance(topic, Group)) and can_use_command(topic, permissions)) or \
183
 			   (isinstance(topic, BaseCog) and can_use_cog(topic, permissions))
186
 			   (isinstance(topic, BaseCog) and can_use_cog(topic, permissions))
184
 		]
187
 		]
185
 
188
 
186
 		# Sort and return
189
 		# Sort and return
187
-		return sorted(accessible_topics, key=lambda topic: (
190
+		result = sorted(accessible_topics, key=lambda topic: (
188
 			isinstance(topic, Command),
191
 			isinstance(topic, Command),
189
 			isinstance(topic, BaseCog),
192
 			isinstance(topic, BaseCog),
190
 			topic.qualified_name if isinstance(topic, BaseCog) else topic.name
193
 			topic.qualified_name if isinstance(topic, BaseCog) else topic.name
191
 		))
194
 		))
195
+		duration = time.perf_counter() - start_time
196
+		if duration > 0.01:
197
+			self.log(None, f'search "{search}" took {duration} seconds')
198
+		return result
192
 
199
 
193
 	@command(
200
 	@command(
194
 		name='help',
201
 		name='help',

Chargement…
Annuler
Enregistrer