Bläddra i källkod

Lots more help improvements

pull/13/head
Rocketsoup 2 månader sedan
förälder
incheckning
9b4e54bc09

+ 1
- 1
rocketbot/cogs/autokickcog.py Visa fil

@@ -38,7 +38,7 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
38 38
 		bool,
39 39
 		default_value=False,
40 40
 		brief='autokick',
41
-		description='Whether this cog is enabled for a guild.',
41
+		description='Whether this module is enabled for a guild.',
42 42
 	)
43 43
 	SETTING_BAN_COUNT = CogSetting(
44 44
 		'bancount',

+ 22
- 18
rocketbot/cogs/configcog.py Visa fil

@@ -32,7 +32,15 @@ class ConfigCog(BaseCog, name='Configuration'):
32 32
 		default_permissions=MOD_PERMISSIONS,
33 33
 	)
34 34
 
35
-	@config.command()
35
+	@config.command(
36
+		description='Sets mod warnings to post in the current channel.',
37
+		extras={
38
+			'long_description': 'Run this command in the channel where bot messages intended '
39
+								'for server moderators should be sent. Other bot messages may '
40
+								'still be posted in the channel a command was invoked in. **If '
41
+								'no output channel is set, mod-related messages will not be posted!**',
42
+		},
43
+	)
36 44
 	async def set_warning_channel(self, interaction: Interaction) -> None:
37 45
 		"""
38 46
 		Sets mod warnings to post in the current channel.
@@ -51,14 +59,10 @@ class ConfigCog(BaseCog, name='Configuration'):
51 59
 			ephemeral=True,
52 60
 		)
53 61
 
54
-	@config.command()
62
+	@config.command(
63
+		description='Shows the configured mod warning channel, if any.'
64
+	)
55 65
 	async def get_warning_channel(self, interaction: Interaction) -> None:
56
-		"""
57
-		Shows the mod warning channel, if any.
58
-
59
-		Shows the configured channel (if any) where mod warnings and other bot
60
-		output will be posted.
61
-		"""
62 66
 		guild: Guild = interaction.guild
63 67
 		channel_id = Storage.get_config_value(guild, ConfigKey.WARNING_CHANNEL_ID)
64 68
 		if channel_id is None:
@@ -73,22 +77,23 @@ class ConfigCog(BaseCog, name='Configuration'):
73 77
 				ephemeral=True,
74 78
 			)
75 79
 
76
-	@config.command()
80
+	@config.command(
81
+		description='Sets the user or role to tag in warning messages.',
82
+		extras={
83
+			'long_description': 'Calling this without a value disables tagging in warnings.',
84
+		}
85
+	)
77 86
 	async def set_warning_mention(self,
78 87
 			interaction: Interaction,
79 88
 			mention: Optional[Union[User, Role]] = None) -> None:
80 89
 		"""
81 90
 		Sets a user/role to mention in warning messages.
82 91
 
83
-		Configures an role or other prefix to include at the beginning of
84
-		warning messages. The intent is to get the attention of certain users
85
-		in case action is needed. Leave blank to tag no one.
86
-
87 92
 		Parameters
88 93
 		----------
89 94
 		interaction: Interaction
90 95
 		mention: User or Role
91
-			The user or role to mention in warning messages
96
+			the user or role to mention in warning messages
92 97
 		"""
93 98
 		guild: Guild = interaction.guild
94 99
 		Storage.set_config_value(guild, ConfigKey.WARNING_MENTION, mention.mention if mention else None)
@@ -103,11 +108,10 @@ class ConfigCog(BaseCog, name='Configuration'):
103 108
 				ephemeral=True,
104 109
 			)
105 110
 
106
-	@config.command()
111
+	@config.command(
112
+		description='Shows the configured user or role tagged in warning messages.',
113
+	)
107 114
 	async def get_warning_mention(self, interaction: Interaction) -> None:
108
-		"""
109
-		Shows the configured user/role to mention in warning messages.
110
-		"""
111 115
 		guild: Guild = interaction.guild
112 116
 		mention: str = Storage.get_config_value(guild, ConfigKey.WARNING_MENTION)
113 117
 		if mention is None:

+ 12
- 9
rocketbot/cogs/crosspostcog.py Visa fil

@@ -35,15 +35,6 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
35 35
 	"""
36 36
 	Detects a user posting in multiple channels in a short period
37 37
 	of time: a common pattern for spammers.
38
-
39
-	These used to be identical text, but more recent attacks have had small
40
-	variations, such as different imgur URLs. It's reasonable to treat
41
-	posting in many channels in a short period as suspicious on its own,
42
-	regardless of whether they are identical.
43
-
44
-	Repeated posts in the same channel aren't currently detected, as this can
45
-	often be for a reason or due to trying a failed post when connectivity is
46
-	poor. Minimum message length can be enforced for detection.
47 38
 	"""
48 39
 	SETTING_ENABLED = CogSetting(
49 40
 		'enabled',
@@ -121,6 +112,18 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
121 112
 			bot,
122 113
 			config_prefix='crosspost',
123 114
 			short_description='Manages crosspost detection and handling.',
115
+			long_description='Detects a user posting in multiple channels in a short period of '
116
+							 'time: a common pattern for spammers.\n'
117
+							 '\n'
118
+							 "These used to be identical text, but more recent attacks have had "
119
+							 "small variations, such as different imgur URLs. It's reasonable to "
120
+							 "treat posting in many channels in a short period as suspicious on its "
121
+							 "own, regardless of whether they are identical.\n"
122
+							 "\n"
123
+							 "Repeated posts in the same channel aren't currently detected, as "
124
+							 "this can often be for a reason or due to trying a failed post when "
125
+							 "connectivity is poor. Minimum message length can be enforced for "
126
+							 "detection.",
124 127
 		)
125 128
 		self.add_setting(CrossPostCog.SETTING_ENABLED)
126 129
 		self.add_setting(CrossPostCog.SETTING_WARN_COUNT)

+ 24
- 26
rocketbot/cogs/generalcog.py Visa fil

@@ -1,20 +1,18 @@
1 1
 """
2 2
 Cog for handling most ungrouped commands and basic behaviors.
3 3
 """
4
-import re
5 4
 from datetime import datetime, timedelta, timezone
6
-from typing import Optional, Union
5
+from typing import Optional
7 6
 
8
-from discord import Interaction, Message, User, Permissions, AppCommandType
9
-from discord.app_commands import Command, Group, command, default_permissions, guild_only, Transform, Choice, \
10
-	autocomplete
7
+from discord import Interaction, Message, User
8
+from discord.app_commands import command, default_permissions, guild_only, Transform
11 9
 from discord.errors import DiscordException
12 10
 from discord.ext.commands import Cog
13 11
 
14 12
 from config import CONFIG
15 13
 from rocketbot.bot import Rocketbot
16 14
 from rocketbot.cogs.basecog import BaseCog, BotMessage
17
-from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace, MOD_PERMISSIONS
15
+from rocketbot.utils import describe_timedelta, TimeDeltaTransformer
18 16
 from rocketbot.storage import ConfigKey, Storage
19 17
 
20 18
 class GeneralCog(BaseCog, name='General'):
@@ -66,8 +64,9 @@ class GeneralCog(BaseCog, name='General'):
66 64
 	@command(
67 65
 		description='Posts a test warning.',
68 66
 		extras={
69
-			'long_description': 'Simulates a warning. The configured warning channel '
70
-								'and mod mention will be used.',
67
+			'long_description': 'If a warning channel is configured, it will be posted '
68
+								'there. If a warning role/user is configured, they will be '
69
+								'tagged in the message.',
71 70
 		},
72 71
 	)
73 72
 	@guild_only()
@@ -90,14 +89,13 @@ class GeneralCog(BaseCog, name='General'):
90 89
 			)
91 90
 
92 91
 	@command(
93
-		description='Greets the user.',
92
+		description='Responds to the user with a greeting.',
94 93
 		extras={
95
-			'long_description': 'Replies to the command message. Useful to ensure the '
96
-								'bot is responsive. Message is only visible to the user.',
94
+			'long_description': 'Useful for checking bot responsiveness. Message '
95
+								'is only visible to the user.',
97 96
 		},
98 97
 	)
99 98
 	async def hello(self, interaction: Interaction):
100
-		"""Command handler"""
101 99
 		await interaction.response.send_message(
102 100
 			f'Hey, {interaction.user.name}!',
103 101
 		 	ephemeral=True,
@@ -106,8 +104,8 @@ class GeneralCog(BaseCog, name='General'):
106 104
 	@command(
107 105
 		description='Shuts down the bot.',
108 106
 		extras={
109
-			'long_description': 'Terminates the bot script. Only usable by a '
110
-								'server administrator.',
107
+			'long_description': 'For emergency use if the bot gains sentience. Only usable '
108
+								'by a server administrator.',
111 109
 		},
112 110
 	)
113 111
 	@guild_only()
@@ -118,32 +116,32 @@ class GeneralCog(BaseCog, name='General'):
118 116
 		await self.bot.close()
119 117
 
120 118
 	@command(
121
-		description='Mass deletes messages.',
119
+		description='Mass deletes recent messages by a user.',
122 120
 		extras={
123
-			'long_description': 'Deletes recent messages by the given user. The user ' +
124
-				'can be either an @ mention or a numeric user ID. The age is ' +
125
-				'a duration, such as "30s", "5m", "1h30m". Only the most ' +
126
-				'recent 100 messages in each channel are searched.',
121
+			'long_description': 'The age is a duration, such as "30s", "5m", "1h30m", "7d". '
122
+								'Only the most recent 100 messages in each channel are searched.\n\n'
123
+								"The author can be a numeric ID if they aren't showing up in autocomplete.",
127 124
 			'usage': '<user:id|mention> <age:timespan>',
128 125
 		},
129 126
 	)
130 127
 	@guild_only()
131 128
 	@default_permissions(manage_messages=True)
132
-	async def delete_messages(self, interaction: Interaction, user: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None:
129
+	async def delete_messages(self, interaction: Interaction, author: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None:
133 130
 		"""
134 131
 		Mass deletes messages.
135 132
 
136 133
 		Parameters
137 134
 		----------
138 135
 		interaction: :class:`Interaction`
139
-		user: :class:`User`
140
-			user to delete messages from
136
+		author: :class:`User`
137
+			author of messages to delete
141 138
 		age: :class:`timedelta`
142
-			maximum age of messages to delete
139
+			maximum age of messages to delete (e.g. 30s, 5m, 1h30s, 7d)
143 140
 		"""
144
-		member_id = user.id
141
+		member_id = author.id
145 142
 		cutoff: datetime = datetime.now(timezone.utc) - age
146 143
 
144
+		# Finding and deleting messages takes time but interaction needs a timely acknowledgement.
147 145
 		resp = await interaction.response.defer(ephemeral=True, thinking=True)
148 146
 
149 147
 		def predicate(message: Message) -> bool:
@@ -159,10 +157,10 @@ class GeneralCog(BaseCog, name='General'):
159 157
 		if len(deleted_messages) > 0:
160 158
 			await resp.resource.edit(
161 159
 				content=f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} '
162
-						f'messages by {user.mention} from the past {describe_timedelta(age)}.',
160
+						f'messages by {author.mention} from the past {describe_timedelta(age)}.',
163 161
 			)
164 162
 		else:
165 163
 			await resp.resource.edit(
166
-				content=f'{CONFIG["success_emoji"]} No messages found for {user.mention} '
164
+				content=f'{CONFIG["success_emoji"]} No messages found for {author.mention} '
167 165
 						'from the past {describe_timedelta(age)}.',
168 166
 			)

+ 117
- 173
rocketbot/cogs/helpcog.py Visa fil

@@ -4,7 +4,6 @@ from typing import Union, Optional
4 4
 
5 5
 from discord import Interaction, Permissions, AppCommandType
6 6
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
7
-from discord.ext.commands import cog
8 7
 
9 8
 from config import CONFIG
10 9
 from rocketbot.bot import Rocketbot
@@ -13,99 +12,29 @@ from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
13 12
 
14 13
 HelpTopic = Union[Command, Group, BaseCog]
15 14
 
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:
15
+def choice_from_topic(topic: HelpTopic, include_full_command: bool = False) -> Choice:
16
+	if isinstance(topic, BaseCog):
17
+		return Choice(name=f'⚙ {topic.qualified_name}', value=f'cog:{topic.qualified_name}')
18
+	if isinstance(topic, Group):
19
+		return Choice(name=f'/{topic.name}', value=f'cmd:{topic.name}')
20
+	if isinstance(topic, Command):
21
+		if topic.parent:
23 22
 			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}')
23
+				return Choice(name=f'/{topic.parent.name} {topic.name}', value=f'subcmd:{topic.parent.name}.{topic.name}')
24
+			return Choice(name=f'{topic.name}', value=f'subcmd:{topic.name}')
25
+		return Choice(name=f'/{topic.name}', value=f'cmd:{topic.name}')
27 26
 	return Choice(name='', value='')
28 27
 
29
-async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
30
-	"""Autocomplete handler for top-level command names."""
31
-	choices: list[Choice] = []
32
-	try:
33
-		if current.startswith('/'):
34
-			current = current[1:]
35
-		current = current.lower().strip()
36
-		user_permissions = interaction.permissions
37
-		cmds = HelpCog.shared.get_command_list(user_permissions)
38
-		return [
39
-			Choice(name=f'/{cmdname} command', value=f'cmd:{cmdname}')
40
-			for cmdname in sorted(cmds.keys())
41
-			if len(current) == 0 or current in cmdname
42
-		][:25]
43
-	except BaseException as e:
44
-		dump_stacktrace(e)
45
-	return choices
46
-
47
-async def subcommand_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
48
-	"""Autocomplete handler for subcommand names. Command taken from previous command token."""
49
-	try:
50
-		current = current.lower().strip()
51
-		cmd_name = interaction.namespace.get('topic', None)
52
-		cmd = HelpCog.shared.object_for_help_symbol(cmd_name)
53
-		if isinstance(cmd, Command):
54
-			# No subcommands
55
-			return []
56
-		user_permissions = interaction.permissions
57
-		if cmd is None or not isinstance(cmd, Group):
58
-			return []
59
-		grp = cmd
60
-		subcmds = HelpCog.shared.get_subcommand_list(grp, user_permissions)
61
-		if subcmds is None:
62
-			return []
63
-		return [
64
-			choice_from_obj(subcmd)
65
-			for subcmd_name, subcmd in sorted(subcmds.items())
66
-			if len(current) == 0 or current in subcmd_name
67
-		][:25]
68
-	except BaseException as e:
69
-		dump_stacktrace(e)
70
-	return []
71
-
72
-async def cog_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
73
-	"""Autocomplete handler for cog names."""
74
-	try:
75
-		current = current.lower().strip()
76
-		return [
77
-			choice_from_obj(cog)
78
-			for cog in sorted(HelpCog.shared.bot.cogs.values(), key=lambda c: c.qualified_name)
79
-			if isinstance(cog, BaseCog) and
80
-			   can_use_cog(cog, interaction.permissions) and
81
-			   (len(cog.get_commands()) > 0 or len(cog.settings) > 0) and \
82
-			   (len(current) == 0 or current in cog.qualified_name.lower())
83
-		]
84
-	except BaseException as e:
85
-		dump_stacktrace(e)
86
-	return []
87
-
88
-async def topic_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
89
-	"""Autocomplete handler that combines slash commands and cog names."""
90
-	command_choices = await command_autocomplete(interaction, current)
91
-	cog_choices = await cog_autocomplete(interaction, current)
92
-	return (command_choices + cog_choices)[:25]
93
-
94
-async def subtopic_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
95
-	"""Autocomplete handler for subtopic names. Currently just handles subcommands."""
96
-	subcommand_choices = await subcommand_autocomplete(interaction, current)
97
-	return subcommand_choices[:25]
98
-
99 28
 async def search_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
100 29
 	try:
101 30
 		if len(current) == 0:
102 31
 			return [
103
-				choice_from_obj(obj, include_full_command=True)
104
-				for obj in HelpCog.shared.all_accessible_objects(interaction.permissions)
32
+				choice_from_topic(topic, include_full_command=True)
33
+				for topic in HelpCog.shared.all_accessible_topics(interaction.permissions)
105 34
 			]
106 35
 		return [
107
-			choice_from_obj(obj, include_full_command=True)
108
-			for obj in HelpCog.shared.objects_for_keywords(current, interaction.permissions)
36
+			choice_from_topic(topic, include_full_command=True)
37
+			for topic in HelpCog.shared.topics_for_keywords(current, interaction.permissions)
109 38
 		]
110 39
 	except BaseException as e:
111 40
 		dump_stacktrace(e)
@@ -118,22 +47,22 @@ class HelpCog(BaseCog, name='Help'):
118 47
 		super().__init__(
119 48
 			bot,
120 49
 			config_prefix='help',
121
-			short_description='Provides help on using commands and modules.'
50
+			short_description='Provides help on using this bot.'
122 51
 		)
123 52
 		HelpCog.shared = self
124 53
 
125 54
 	def __create_help_index(self) -> None:
126 55
 		"""
127
-		Populates self.obj_index and self.keyword_index. Bails if already
56
+		Populates self.topic_index and self.keyword_index. Bails if already
128 57
 		populated. Intended to be run on demand so all cogs and commands have
129 58
 		had time to get set up and synced.
130 59
 		"""
131
-		if getattr(self, 'obj_index', None) is not None:
60
+		if getattr(self, 'topic_index', None) is not None:
132 61
 			return
133
-		self.obj_index: dict[str, HelpTopic] = {}
62
+		self.topic_index: dict[str, HelpTopic] = {}
134 63
 		self.keyword_index: dict[str, set[HelpTopic]] = {}
135 64
 
136
-		def add_text_to_index(obj, text: str):
65
+		def add_text_to_index(topic: HelpTopic, text: str):
137 66
 			words = [
138 67
 				word
139 68
 				for word in re.split(r"[^a-zA-Z']+", text.lower())
@@ -141,20 +70,20 @@ class HelpCog(BaseCog, name='Help'):
141 70
 			]
142 71
 			for word in words:
143 72
 				matches = self.keyword_index.get(word, set())
144
-				matches.add(obj)
73
+				matches.add(topic)
145 74
 				self.keyword_index[word] = matches
146 75
 
147 76
 		cmds = self.all_commands()
148 77
 		for cmd in cmds:
149
-			self.obj_index[f'cmd:{cmd.name}'] = cmd
150
-			self.obj_index[f'/{cmd.name}'] = cmd
78
+			self.topic_index[f'cmd:{cmd.name}'] = cmd
79
+			self.topic_index[f'/{cmd.name}'] = cmd
151 80
 			add_text_to_index(cmd, cmd.name)
152 81
 			if cmd.description:
153 82
 				add_text_to_index(cmd, cmd.description)
154 83
 			if isinstance(cmd, Group):
155 84
 				for subcmd in cmd.commands:
156
-					self.obj_index[f'subcmd:{cmd.name}.{subcmd.name}'] = subcmd
157
-					self.obj_index[f'/{cmd.name} {subcmd.name}'] = subcmd
85
+					self.topic_index[f'subcmd:{cmd.name}.{subcmd.name}'] = subcmd
86
+					self.topic_index[f'/{cmd.name} {subcmd.name}'] = subcmd
158 87
 					add_text_to_index(subcmd, cmd.name)
159 88
 					add_text_to_index(subcmd, subcmd.name)
160 89
 					if subcmd.description:
@@ -163,14 +92,14 @@ class HelpCog(BaseCog, name='Help'):
163 92
 			if not isinstance(cog, BaseCog):
164 93
 				continue
165 94
 			key = f'cog:{cog_qname}'
166
-			self.obj_index[key] = cog
95
+			self.topic_index[key] = cog
167 96
 			add_text_to_index(cog, cog.qualified_name)
168 97
 			if cog.description:
169 98
 				add_text_to_index(cog, cog.description)
170 99
 
171
-	def object_for_help_symbol(self, symbol: str) -> Optional[HelpTopic]:
100
+	def topic_for_help_symbol(self, symbol: str) -> Optional[HelpTopic]:
172 101
 		self.__create_help_index()
173
-		return self.obj_index.get(symbol, None)
102
+		return self.topic_index.get(symbol, None)
174 103
 
175 104
 	def all_commands(self) -> list[Union[Command, Group]]:
176 105
 		# PyCharm not interpreting conditional return type correctly.
@@ -202,20 +131,20 @@ class HelpCog(BaseCog, name='Help'):
202 131
 			if can_use_cog(cog, permissions)
203 132
 		]
204 133
 
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 = []
134
+	def all_accessible_topics(self, permissions: Optional[Permissions], *,
135
+							  include_cogs: bool = True,
136
+							  include_commands: bool = True,
137
+							  include_subcommands: bool = True) -> list[HelpTopic]:
138
+		topics = []
210 139
 		if include_cogs:
211
-			objs += self.all_accessible_cogs(permissions)
140
+			topics += self.all_accessible_cogs(permissions)
212 141
 		if include_commands:
213
-			objs += self.all_accessible_commands(permissions)
142
+			topics += self.all_accessible_commands(permissions)
214 143
 		if include_subcommands:
215
-			objs += self.all_accessible_subcommands(permissions)
216
-		return objs
144
+			topics += self.all_accessible_subcommands(permissions)
145
+		return topics
217 146
 
218
-	def objects_for_keywords(self, search: str, permissions: Optional[Permissions]) -> list[HelpTopic]:
147
+	def topics_for_keywords(self, search: str, permissions: Optional[Permissions]) -> list[HelpTopic]:
219 148
 		self.__create_help_index()
220 149
 
221 150
 		# Break into words (or word fragments)
@@ -229,53 +158,55 @@ class HelpCog(BaseCog, name='Help'):
229 158
 		# to known indexed keywords, then collecting those associated results. Should
230 159
 		# just keep corpuses of searchable, normalized text for each topic and do a
231 160
 		# direct `in` test.
232
-		matching_objects_set = None
161
+		matching_topics_set = None
233 162
 		for word in words:
234 163
 			word_matches = set()
235 164
 			for k in self.keyword_index.keys():
236 165
 				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
166
+					topics = self.keyword_index.get(k, None)
167
+					if topics is not None:
168
+						word_matches.update(topics)
169
+			if matching_topics_set is None:
170
+				matching_topics_set = word_matches
242 171
 			else:
243
-				matching_objects_set = matching_objects_set & word_matches
172
+				matching_topics_set = matching_topics_set & word_matches
244 173
 
245 174
 		# 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))
175
+		accessible_topics = [
176
+			topic
177
+			for topic in matching_topics_set or {}
178
+			if ((isinstance(topic, Command) or isinstance(topic, Group)) and can_use_command(topic, permissions)) or \
179
+			   (isinstance(topic, BaseCog) and can_use_cog(topic, permissions))
251 180
 		]
252 181
 
253 182
 		# 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
183
+		return sorted(accessible_topics, key=lambda topic: (
184
+			isinstance(topic, Command),
185
+			isinstance(topic, BaseCog),
186
+			topic.qualified_name if isinstance(topic, BaseCog) else topic.name
258 187
 		))
259 188
 
260
-	@command(name='help')
189
+	@command(
190
+		name='help',
191
+		description='Shows help for using commands and module configuration.',
192
+		extras={
193
+			'long_description': '`/help` will show a list of top-level topics.\n'
194
+								'\n'
195
+								"`/help /<command_name>` will show help about a specific command or list a command's subcommands.\n"
196
+								'\n'
197
+								'`/help /<command_name> <subcommand_name>` will show help about a specific subcommand.\n'
198
+								'\n'
199
+								'`/help <module_name>` will show help about configuring a module.\n'
200
+								'\n'
201
+								'`/help <keywords>` will do a text search for topics.',
202
+		}
203
+	)
261 204
 	@guild_only()
262 205
 	@autocomplete(search=search_autocomplete)
263 206
 	async def help_command(self, interaction: Interaction, search: Optional[str]) -> None:
264 207
 		"""
265 208
 		Shows help for using commands and subcommands and configuring modules.
266 209
 
267
-		`/help` will show a list of top-level topics.
268
-
269
-		`/help /<command_name>` will show help about a specific command or
270
-		list a command's subcommands.
271
-
272
-		`/help /<command_name> <subcommand_name>` will show help about a
273
-		specific subcommand.
274
-
275
-		`/help <module_name>` will show help about configuring a module.
276
-
277
-		`/help <keywords>` will do a text search for topics.
278
-
279 210
 		Parameters
280 211
 		----------
281 212
 		interaction: Interaction
@@ -285,27 +216,24 @@ class HelpCog(BaseCog, name='Help'):
285 216
 		if search is None:
286 217
 			await self.__send_general_help(interaction)
287 218
 			return
288
-		obj = self.object_for_help_symbol(search)
289
-		if obj:
290
-			await self.__send_object_help(interaction, obj)
219
+		topic = self.topic_for_help_symbol(search)
220
+		if topic:
221
+			await self.__send_topic_help(interaction, topic)
291 222
 			return
292
-		matches = self.objects_for_keywords(search, interaction.permissions)
223
+		matches = self.topics_for_keywords(search, interaction.permissions)
293 224
 		await self.__send_keyword_help(interaction, matches)
294 225
 
295
-	async def __send_object_help(self, interaction: Interaction, obj: HelpTopic) -> None:
296
-		if isinstance(obj, Command):
297
-			if obj.parent:
298
-				await self.__send_subcommand_help(interaction, obj.parent, obj)
299
-			else:
300
-				await self.__send_command_help(interaction, obj)
226
+	async def __send_topic_help(self, interaction: Interaction, topic: HelpTopic) -> None:
227
+		if isinstance(topic, Command):
228
+			await self.__send_command_help(interaction, topic)
301 229
 			return
302
-		if isinstance(obj, Group):
303
-			await self.__send_command_help(interaction, obj)
230
+		if isinstance(topic, Group):
231
+			await self.__send_command_help(interaction, topic)
304 232
 			return
305
-		if isinstance(obj, BaseCog):
306
-			await self.__send_cog_help(interaction, obj)
233
+		if isinstance(topic, BaseCog):
234
+			await self.__send_cog_help(interaction, topic)
307 235
 			return
308
-		self.log(interaction.guild, f'No help for object {obj}')
236
+		self.log(interaction.guild, f'No help for topic object {topic}')
309 237
 		await interaction.response.send_message(
310 238
 			f'{CONFIG["failure_emoji"]} Failed to get help info.',
311 239
 			ephemeral=True,
@@ -361,15 +289,15 @@ class HelpCog(BaseCog, name='Help'):
361 289
 			ephemeral=True,
362 290
 		)
363 291
 
364
-	async def __send_keyword_help(self, interaction: Interaction, matching_objects: Optional[list[HelpTopic]]) -> None:
292
+	async def __send_keyword_help(self, interaction: Interaction, matching_topics: Optional[list[HelpTopic]]) -> None:
365 293
 		matching_commands = [
366 294
 			cmd
367
-			for cmd in matching_objects or []
295
+			for cmd in matching_topics or []
368 296
 			if isinstance(cmd, Command) or isinstance(cmd, Group)
369 297
 		]
370 298
 		matching_cogs = [
371 299
 			cog
372
-			for cog in matching_objects or []
300
+			for cog in matching_topics or []
373 301
 			if isinstance(cog, BaseCog)
374 302
 		]
375 303
 		if len(matching_commands) + len(matching_cogs) == 0:
@@ -379,9 +307,9 @@ class HelpCog(BaseCog, name='Help'):
379 307
 				delete_after=10,
380 308
 			)
381 309
 			return
382
-		if len(matching_objects) == 1:
383
-			obj = matching_objects[0]
384
-			await self.__send_object_help(interaction, obj)
310
+		if len(matching_topics) == 1:
311
+			topic = matching_topics[0]
312
+			await self.__send_topic_help(interaction, topic)
385 313
 			return
386 314
 
387 315
 		text = '## :information_source: Matching Help Topics'
@@ -393,7 +321,7 @@ class HelpCog(BaseCog, name='Help'):
393 321
 				else:
394 322
 					text += f'\n- `/{cmd.name}`'
395 323
 		if len(matching_cogs) > 0:
396
-			text += '\n### Cogs'
324
+			text += '\n### Modules'
397 325
 			for cog in matching_cogs:
398 326
 				text += f'\n- {cog.qualified_name}'
399 327
 		await interaction.response.send_message(
@@ -405,37 +333,53 @@ class HelpCog(BaseCog, name='Help'):
405 333
 		text = ''
406 334
 		if addendum is not None:
407 335
 			text += addendum + '\n\n'
408
-		text += f'## :information_source: Command Help\n`/{command_or_group.name}`\n\n{command_or_group.description}'
336
+		if command_or_group.parent:
337
+			text += f'## :information_source: Subcommand Help'
338
+			text += f'\n`/{command_or_group.parent.name} {command_or_group.name}`'
339
+		else:
340
+			text += f'## :information_source: Command Help'
341
+			if isinstance(command_or_group, Group):
342
+				text += f'\n`/{command_or_group.name} subcommand_name`'
343
+			else:
344
+				text += f'\n`/{command_or_group.name}`'
345
+		if isinstance(command_or_group, Command):
346
+			optional_nesting = 0
347
+			for param in command_or_group.parameters:
348
+				text += '  '
349
+				if not param.required:
350
+					text += '['
351
+					optional_nesting += 1
352
+				text += f'_{param.name}_'
353
+			if optional_nesting > 0:
354
+				text += ']' * optional_nesting
355
+		text += f'\n\n{command_or_group.description}'
356
+		if command_or_group.extras.get('long_description'):
357
+			text += f'\n\n{command_or_group.extras["long_description"]}'
409 358
 		if isinstance(command_or_group, Group):
410 359
 			subcmds: dict[str, Command] = self.get_subcommand_list(command_or_group, permissions=interaction.permissions)
411 360
 			if len(subcmds) > 0:
412 361
 				text += '\n### Subcommands:'
413 362
 				for subcmd_name, subcmd in sorted(subcmds.items()):
414 363
 					text += f'\n- `{subcmd_name}`: {subcmd.description}'
364
+				text += f'\n-# To use a subcommand, type it after the command. e.g. `/{command_or_group.name} subcommand_name`'
365
+				text += f'\n-# Get help on a subcommand by typing `/help /{command_or_group.name} subcommand_name`'
415 366
 		else:
416 367
 			params = command_or_group.parameters
417 368
 			if len(params) > 0:
418 369
 				text += '\n### Parameters:'
419 370
 				for param in params:
420 371
 					text += f'\n- `{param.name}`: {param.description}'
421
-		await interaction.response.send_message(text, ephemeral=True)
422
-
423
-	async def __send_subcommand_help(self, interaction: Interaction, group: Group, subcommand: Command) -> None:
424
-		text = f'## :information_source: Subcommand Help'
425
-		text += f'\n`/{group.name} {subcommand.name}`'
426
-		text += f'\n\n{subcommand.description}'
427
-		params = subcommand.parameters
428
-		if len(params) > 0:
429
-			text += '\n### Parameters:'
430
-			for param in params:
431
-				text += f'\n- `{param.name}`: {param.description}'
372
+					if not param.required:
373
+						text += ' (optional)'
432 374
 		await interaction.response.send_message(text, ephemeral=True)
433 375
 
434 376
 	async def __send_cog_help(self, interaction: Interaction, cog: BaseCog) -> None:
435 377
 		text = f'## :information_source: Module Help'
436 378
 		text += f'\n**{cog.qualified_name}** module'
437
-		if cog.description is not None:
438
-			text += f'\n\n{cog.description}'
379
+		if cog.short_description is not None:
380
+			text += f'\n\n{cog.short_description}'
381
+		if cog.long_description is not None:
382
+			text += f'\n\n{cog.long_description}'
439 383
 
440 384
 		cmds = [
441 385
 			cmd

+ 4
- 1
rocketbot/cogs/joinraidcog.py Visa fil

@@ -34,7 +34,7 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
34 34
 		bool,
35 35
 		default_value=False,
36 36
 		brief='join raid detection',
37
-		description='Whether this cog is enabled for a guild.',
37
+		description='Whether this module is enabled for a guild.',
38 38
 	)
39 39
 	SETTING_JOIN_COUNT = CogSetting(
40 40
 		'joincount',
@@ -65,6 +65,9 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
65 65
 			bot,
66 66
 			config_prefix='joinraid',
67 67
 			short_description='Manages join raid detection and handling.',
68
+			long_description='Join raids consist of an unusual number of users joining in '
69
+							 'a short period of time and have shown a pattern of DMing '
70
+							 'members with spam or scams.'
68 71
 		)
69 72
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)
70 73
 		self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)

+ 1
- 1
rocketbot/cogs/logcog.py Visa fil

@@ -43,7 +43,7 @@ class LoggingCog(BaseCog, name='Logging'):
43 43
 		bool,
44 44
 		default_value=False,
45 45
 		brief='logging',
46
-		description='Whether this cog is enabled for a guild.',
46
+		description='Whether this module is enabled for a guild.',
47 47
 	)
48 48
 
49 49
 	STATE_EVENT_BUFFER = 'LoggingCog.eventBuffer'

+ 28
- 28
rocketbot/cogs/patterncog.py Visa fil

@@ -100,6 +100,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
100 100
 			bot,
101 101
 			config_prefix='patterns',
102 102
 			short_description='Manages message pattern matching.',
103
+			long_description='Patterns are a powerful but complex topic. See <https://git.rixafrix.com/ialbert/python-app-rocketbot/src/branch/main/docs/patterns.md> for full documentation.'
103 104
 		)
104 105
 		PatternCog.shared = self
105 106
 
@@ -262,10 +263,20 @@ class PatternCog(BaseCog, name='Pattern Matching'):
262 263
 		description='Manages message pattern matching.',
263 264
 		guild_only=True,
264 265
 		default_permissions=MOD_PERMISSIONS,
266
+		extras={
267
+			'long_description': 'Patterns are a powerful but complex topic. '
268
+								'See <https://git.rixafrix.com/ialbert/python-app-rocketbot/src/branch/main/docs/patterns.md> for full documentation.'
269
+		},
265 270
 	)
266 271
 
267
-	@pattern.command()
268
-	@rename(expression='if')
272
+	@pattern.command(
273
+		description='Adds or updates a custom pattern.',
274
+		extras={
275
+			'long_description': 'Patterns use a simplified expression language. Full '
276
+								'documentation found here: '
277
+								'<https://git.rixafrix.com/ialbert/python-app-rocketbot/src/branch/main/docs/patterns.md>',
278
+		},
279
+	)
269 280
 	@autocomplete(
270 281
 		name=pattern_name_autocomplete,
271 282
 		# actions=action_autocomplete
@@ -280,19 +291,15 @@ class PatternCog(BaseCog, name='Pattern Matching'):
280 291
 		"""
281 292
 		Adds a custom pattern.
282 293
 
283
-		Adds a custom pattern. Patterns use a simplified
284
-		expression language. Full documentation found here:
285
-		https://git.rixafrix.com/ialbert/python-app-rocketbot/src/branch/main/docs/patterns.md
286
-
287 294
 		Parameters
288 295
 		----------
289 296
 		interaction : Interaction
290 297
 		name : str
291
-			A name for the pattern.
298
+			a name for the new or existing pattern
292 299
 		actions : str
293
-			One or more actions to take when a message matches the expression.
300
+			actions to take when a message matches
294 301
 		expression : str
295
-			Criteria for matching chat messages.
302
+			criteria for matching chat messages
296 303
 		"""
297 304
 		pattern_str = f'{actions} if {expression}'
298 305
 		guild = interaction.guild
@@ -313,7 +320,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
313 320
 			)
314 321
 
315 322
 	@pattern.command(
316
-		description='Removes a custom pattern',
323
+		description='Removes a custom pattern.',
317 324
 		extras={
318 325
 			'usage': '<pattern_name>',
319 326
 		},
@@ -321,13 +328,13 @@ class PatternCog(BaseCog, name='Pattern Matching'):
321 328
 	@autocomplete(name=pattern_name_autocomplete)
322 329
 	async def remove(self, interaction: Interaction, name: str):
323 330
 		"""
324
-		Command handler
331
+		Removes a custom pattern.
325 332
 
326 333
 		Parameters
327 334
 		----------
328 335
 		interaction: Interaction
329 336
 		name: str
330
-			Name of the pattern to remove.
337
+			name of the pattern to remove
331 338
 		"""
332 339
 		guild = interaction.guild
333 340
 		patterns = self.get_patterns(guild)
@@ -345,16 +352,9 @@ class PatternCog(BaseCog, name='Pattern Matching'):
345 352
 			)
346 353
 
347 354
 	@pattern.command(
348
-		description='Lists all patterns',
355
+		description='Lists all patterns.',
349 356
 	)
350 357
 	async def list(self, interaction: Interaction) -> None:
351
-		"""
352
-		Command handler
353
-
354
-		Parameters
355
-		----------
356
-		interaction: Interaction
357
-		"""
358 358
 		guild = interaction.guild
359 359
 		patterns = self.get_patterns(guild)
360 360
 		if len(patterns) == 0:
@@ -369,26 +369,26 @@ class PatternCog(BaseCog, name='Pattern Matching'):
369 369
 		await interaction.response.send_message(msg, ephemeral=True)
370 370
 
371 371
 	@pattern.command(
372
-		description='Sets a pattern\'s priority level',
372
+		description="Sets a pattern's priority level.",
373 373
 		extras={
374
-			'long_description': 'Sets the priority for a pattern. Messages are checked ' +
375
-				'against patterns with the highest priority first. Patterns with ' +
376
-				'the same priority may be checked in arbitrary order. Default ' +
377
-				'priority is 100.',
374
+			'long_description': 'Messages are checked against patterns with the '
375
+								'highest priority first. Patterns with the same '
376
+								'priority may be checked in arbitrary order. Default '
377
+								'priority is 100.',
378 378
 		},
379 379
 	)
380 380
 	@autocomplete(name=pattern_name_autocomplete, priority=priority_autocomplete)
381 381
 	async def setpriority(self, interaction: Interaction, name: str, priority: int) -> None:
382 382
 		"""
383
-		Command handler
383
+		Sets a pattern's priority level.
384 384
 
385 385
 		Parameters
386 386
 		----------
387 387
 		interaction: Interaction
388 388
 		name: str
389
-			A name for the pattern
389
+			the name of the pattern
390 390
 		priority: int
391
-			Priority for evaluating the pattern. Default is 100. Higher values match first.
391
+			evaluation priority
392 392
 		"""
393 393
 		guild = interaction.guild
394 394
 		patterns = self.get_patterns(guild)

+ 11
- 4
rocketbot/cogs/usernamecog.py Visa fil

@@ -52,7 +52,7 @@ class UsernamePatternCog(BaseCog, name='Username Pattern'):
52 52
 		bool,
53 53
 		default_value=False,
54 54
 		brief='username pattern detection',
55
-		description='Whether new users are checked for common patterns.',
55
+		description='Whether new users are checked for username patterns.',
56 56
 	)
57 57
 
58 58
 	SETTING_PATTERNS = CogSetting('patterns', None, default_value=None)
@@ -62,6 +62,8 @@ class UsernamePatternCog(BaseCog, name='Username Pattern'):
62 62
 			bot,
63 63
 			config_prefix='username',
64 64
 			short_description='Manages username pattern detection.',
65
+			long_description='When new users join, if their username matches '
66
+							 'a configured pattern the mods will be alerted.'
65 67
 		)
66 68
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)
67 69
 
@@ -89,12 +91,18 @@ class UsernamePatternCog(BaseCog, name='Username Pattern'):
89 91
 		description='Manages username pattern detection.',
90 92
 		guild_only=True,
91 93
 		default_permissions=MOD_PERMISSIONS,
94
+		extras={
95
+			'long_description': 'When new users join, if their username matches '
96
+								'a configured pattern the mods will be alerted.',
97
+		},
92 98
 	)
93 99
 
94 100
 	@username.command(
95
-		description='Adds a username pattern to match against new members.',
101
+		description='Adds a username pattern.',
96 102
 		extras={
97
-			'long_description': 'Adds a username pattern.',
103
+			'long_description': 'When a user joins the server, if their username '
104
+								'matches a configured pattern the mods will be alerted. '
105
+								'Matching is currently a simple substring test.',
98 106
 			'usage': '<pattern>',
99 107
 		},
100 108
 	)
@@ -126,7 +134,6 @@ class UsernamePatternCog(BaseCog, name='Username Pattern'):
126 134
 	@username.command(
127 135
 		description='Removes a username pattern.',
128 136
 		extras={
129
-			'long_description': 'Removes an existing username pattern',
130 137
 			'usage': '<pattern>',
131 138
 		},
132 139
 	)

+ 9
- 9
rocketbot/cogsetting.py Visa fil

@@ -197,7 +197,7 @@ class CogSetting:
197 197
 			cog.log(interaction.guild, f'{interaction.user.name} set {key} to {new_value}')
198 198
 
199 199
 		setter: CommandCallback = setter_general
200
-		if self.datatype == int:
200
+		if self.datatype is int:
201 201
 			if self.min_value is not None or self.max_value is not None:
202 202
 				r_min = self.min_value
203 203
 				r_max = self.max_value
@@ -212,15 +212,15 @@ class CogSetting:
212 212
 				async def setter_int(interaction: Interaction, new_value: int) -> None:
213 213
 					await setter_general(interaction, new_value)
214 214
 				setter = setter_int
215
-		elif self.datatype == float:
215
+		elif self.datatype is float:
216 216
 			@rename(new_value=self.name)
217 217
 			@describe(new_value=self.brief)
218 218
 			async def setter_float(interaction: Interaction, new_value: float) -> None:
219 219
 				await setter_general(interaction, new_value)
220 220
 			setter = setter_float
221
-		elif self.datatype == timedelta:
221
+		elif self.datatype is timedelta:
222 222
 			@rename(new_value=self.name)
223
-			@describe(new_value=f'{self.brief} (e.g. 30s, 5m, 1h30s, or 7d)')
223
+			@describe(new_value=f'{self.brief} (e.g. 30s, 5m, 1h30s, 7d)')
224 224
 			async def setter_timedelta(interaction: Interaction, new_value: Transform[timedelta, TimeDeltaTransformer]) -> None:
225 225
 				await setter_general(interaction, new_value)
226 226
 			setter = setter_timedelta
@@ -231,7 +231,7 @@ class CogSetting:
231 231
 			async def setter_enum(interaction: Interaction, new_value: dt) -> None:
232 232
 				await setter_general(interaction, new_value)
233 233
 			setter = setter_enum
234
-		elif self.datatype == str:
234
+		elif self.datatype is str:
235 235
 			if self.enum_values is not None:
236 236
 				raise ValueError('Type for a setting with enum values should be typing.Literal')
237 237
 			else:
@@ -240,7 +240,7 @@ class CogSetting:
240 240
 				async def setter_str(interaction: Interaction, new_value: str) -> None:
241 241
 					await setter_general(interaction, new_value)
242 242
 				setter = setter_str
243
-		elif self.datatype == bool:
243
+		elif self.datatype is bool:
244 244
 			@rename(new_value=self.name)
245 245
 			@describe(new_value=self.brief)
246 246
 			async def setter_bool(interaction: Interaction, new_value: bool) -> None:
@@ -351,13 +351,13 @@ class CogSetting:
351 351
 			description='Shows a configuration value for this guild.',
352 352
 			default_permissions=MOD_PERMISSIONS,
353 353
 			extras={
354
-				'long_description': 'Settings are guild-specific. Shows the configured value or default value for a '
355
-									'variable for this guild. Use `/set` to change the value.',
354
+				'long_description': 'Settings are guild-specific. If no value is set, a default is used. Use `/set` to '
355
+									'change the value.',
356 356
 			},
357 357
 		)
358 358
 		cls.__enable_group = Group(
359 359
 			name='enable',
360
-			description='Enables a module for this guild',
360
+			description='Enables a module for this guild.',
361 361
 			default_permissions=MOD_PERMISSIONS,
362 362
 			extras={
363 363
 				'long_description': 'Modules are enabled on a per-guild basis and are off by default. Use `/disable` '

Laddar…
Avbryt
Spara