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

Lots of help refinements

pull/13/head
Rocketsoup 2 месяцев назад
Родитель
Сommit
6898a998a2

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

57
 		super().__init__(
57
 		super().__init__(
58
 			bot,
58
 			bot,
59
 			config_prefix='autokick',
59
 			config_prefix='autokick',
60
-			name='auto-kick',
61
 			short_description='Automatically kicks all new users as soon as they join.',
60
 			short_description='Automatically kicks all new users as soon as they join.',
62
 		)
61
 		)
63
 		self.add_setting(AutoKickCog.SETTING_ENABLED)
62
 		self.add_setting(AutoKickCog.SETTING_ENABLED)

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

35
 			self,
35
 			self,
36
 			bot: Rocketbot,
36
 			bot: Rocketbot,
37
 			config_prefix: Optional[str],
37
 			config_prefix: Optional[str],
38
-			name: str,
39
 			short_description: str,
38
 			short_description: str,
40
 			long_description: Optional[str] = None,
39
 			long_description: Optional[str] = None,
41
 	):
40
 	):
53
 		self.are_settings_setup: bool = False
52
 		self.are_settings_setup: bool = False
54
 		self.settings: list[CogSetting] = []
53
 		self.settings: list[CogSetting] = []
55
 		self.config_prefix: Optional[str] = config_prefix
54
 		self.config_prefix: Optional[str] = config_prefix
56
-		self.name: str = name
57
 		self.short_description: str = short_description
55
 		self.short_description: str = short_description
58
 		self.long_description: str = long_description
56
 		self.long_description: str = long_description
59
 
57
 

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

22
 		super().__init__(
22
 		super().__init__(
23
 			bot,
23
 			bot,
24
 			config_prefix='config',
24
 			config_prefix='config',
25
-			name='configuration',
26
 			short_description='Manages general bot configuration.',
25
 			short_description='Manages general bot configuration.',
27
 		)
26
 		)
28
 
27
 
29
 	config = Group(
28
 	config = Group(
30
 		name='config',
29
 		name='config',
31
-		description='Manages general bot configuration',
30
+		description='Manages general bot configuration.',
32
 		guild_only=True,
31
 		guild_only=True,
33
 		default_permissions=MOD_PERMISSIONS,
32
 		default_permissions=MOD_PERMISSIONS,
34
 	)
33
 	)

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

89
 			'when looking for duplicates. Shorter values are preferred, ' + \
89
 			'when looking for duplicates. Shorter values are preferred, ' + \
90
 			'both to detect bots and avoid excessive memory usage.',
90
 			'both to detect bots and avoid excessive memory usage.',
91
 		usage='<seconds:int>',
91
 		usage='<seconds:int>',
92
-		min_value=1)
92
+		min_value=timedelta(seconds=1))
93
 
93
 
94
 	STATE_KEY_RECENT_MESSAGES = "CrossPostCog.recent_messages"
94
 	STATE_KEY_RECENT_MESSAGES = "CrossPostCog.recent_messages"
95
 	STATE_KEY_SPAM_CONTEXT = "CrossPostCog.spam_context"
95
 	STATE_KEY_SPAM_CONTEXT = "CrossPostCog.spam_context"
98
 		super().__init__(
98
 		super().__init__(
99
 			bot,
99
 			bot,
100
 			config_prefix='crosspost',
100
 			config_prefix='crosspost',
101
-			name='crosspost detection',
102
 			short_description='Manages crosspost detection and handling.',
101
 			short_description='Manages crosspost detection and handling.',
103
 		)
102
 		)
104
 		self.add_setting(CrossPostCog.SETTING_ENABLED)
103
 		self.add_setting(CrossPostCog.SETTING_ENABLED)

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

1
 """
1
 """
2
 Cog for handling most ungrouped commands and basic behaviors.
2
 Cog for handling most ungrouped commands and basic behaviors.
3
 """
3
 """
4
+import re
4
 from datetime import datetime, timedelta, timezone
5
 from datetime import datetime, timedelta, timezone
5
 from typing import Optional, Union
6
 from typing import Optional, Union
6
 
7
 
7
-from discord import Interaction, Message, User, Permissions
8
-from discord.app_commands import Command, Group, command, default_permissions, guild_only, Transform, rename, Choice, \
8
+from discord import Interaction, Message, User, Permissions, AppCommandType
9
+from discord.app_commands import Command, Group, command, default_permissions, guild_only, Transform, Choice, \
9
 	autocomplete
10
 	autocomplete
10
 from discord.errors import DiscordException
11
 from discord.errors import DiscordException
11
 from discord.ext.commands import Cog
12
 from discord.ext.commands import Cog
13
 from config import CONFIG
14
 from config import CONFIG
14
 from rocketbot.bot import Rocketbot
15
 from rocketbot.bot import Rocketbot
15
 from rocketbot.cogs.basecog import BaseCog, BotMessage
16
 from rocketbot.cogs.basecog import BaseCog, BotMessage
16
-from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace
17
+from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace, MOD_PERMISSIONS
17
 from rocketbot.storage import ConfigKey, Storage
18
 from rocketbot.storage import ConfigKey, Storage
18
 
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
+
19
 async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
27
 async def command_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
20
 	choices: list[Choice] = []
28
 	choices: list[Choice] = []
21
 	try:
29
 	try:
25
 		user_permissions = interaction.permissions
33
 		user_permissions = interaction.permissions
26
 		cmds = GeneralCog.shared.get_command_list(user_permissions)
34
 		cmds = GeneralCog.shared.get_command_list(user_permissions)
27
 		return [
35
 		return [
28
-			Choice(name=f'/{cmdname}', value=f'/{cmdname}')
36
+			Choice(name=f'/{cmdname} command', value=f'cmd:{cmdname}')
29
 			for cmdname in sorted(cmds.keys())
37
 			for cmdname in sorted(cmds.keys())
30
-			if len(current) == 0 or cmdname.startswith(current)
31
-		]
38
+			if len(current) == 0 or current in cmdname
39
+		][:25]
32
 	except BaseException as e:
40
 	except BaseException as e:
33
 		dump_stacktrace(e)
41
 		dump_stacktrace(e)
34
 	return choices
42
 	return choices
36
 async def subcommand_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
44
 async def subcommand_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
37
 	try:
45
 	try:
38
 		current = current.lower().strip()
46
 		current = current.lower().strip()
39
-		cmd_name = interaction.namespace['command']
40
-		if cmd_name.startswith('/'):
41
-			cmd_name = cmd_name[1:]
47
+		cmd_name = interaction.namespace['topic']
48
+		cmd = GeneralCog.shared.object_for_help_symbol(cmd_name)
49
+		if not isinstance(cmd, Group):
50
+			return []
42
 		user_permissions = interaction.permissions
51
 		user_permissions = interaction.permissions
43
-		cmd = GeneralCog.shared.get_command_list(user_permissions).get(cmd_name)
44
 		if cmd is None or not isinstance(cmd, Group):
52
 		if cmd is None or not isinstance(cmd, Group):
45
 			print(f'No command found named {cmd_name}')
53
 			print(f'No command found named {cmd_name}')
46
 			return []
54
 			return []
50
 			print(f'Subcommands for {cmd_name} was None')
58
 			print(f'Subcommands for {cmd_name} was None')
51
 			return []
59
 			return []
52
 		return [
60
 		return [
53
-			Choice(name=subcmd_name, value=subcmd_name)
61
+			Choice(name=f'{subcmd_name} subcommand', value=f'subcmd:{cmd_name}.{subcmd_name}')
54
 			for subcmd_name in sorted(subcmds.keys())
62
 			for subcmd_name in sorted(subcmds.keys())
55
-			if len(current) == 0 or subcmd_name.startswith(current)
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())
56
 		]
79
 		]
57
 	except BaseException as e:
80
 	except BaseException as e:
58
 		dump_stacktrace(e)
81
 		dump_stacktrace(e)
59
 	return []
82
 	return []
60
 
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
+
61
 def can_use_command(cmd: Union[Group, Command], user_permissions: Optional[Permissions]) -> bool:
93
 def can_use_command(cmd: Union[Group, Command], user_permissions: Optional[Permissions]) -> bool:
62
-	return user_permissions is not None and \
63
-		(cmd.default_permissions is None or cmd.default_permissions.is_subset(user_permissions))
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)
64
 
102
 
65
 class GeneralCog(BaseCog, name='General'):
103
 class GeneralCog(BaseCog, name='General'):
66
 	"""
104
 	"""
74
 		super().__init__(
112
 		super().__init__(
75
 			bot,
113
 			bot,
76
 			config_prefix=None,
114
 			config_prefix=None,
77
-			name='',
78
 			short_description='',
115
 			short_description='',
79
 		)
116
 		)
80
 		self.is_connected = False
117
 		self.is_connected = False
196
 			ephemeral=True,
233
 			ephemeral=True,
197
 		)
234
 		)
198
 
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
+
199
 	@command(name='help')
281
 	@command(name='help')
200
 	@guild_only()
282
 	@guild_only()
201
-	@rename(command_name='command', subcommand_name='subcommand')
202
-	@autocomplete(command_name=command_autocomplete, subcommand_name=subcommand_autocomplete)
203
-	async def help_command(self, interaction: Interaction, command_name: Optional[str] = None, subcommand_name: Optional[str] = None) -> None:
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:
204
 		"""
285
 		"""
205
-		Shows help for using commands and subcommands.
286
+		Shows help for using commands and subcommands and configuring modules.
206
 
287
 
207
-		`/help` will show a list of top-level commands.
288
+		`/help` will show a list of top-level topics.
208
 
289
 
209
 		`/help /<command_name>` will show help about a specific command or
290
 		`/help /<command_name>` will show help about a specific command or
210
 		list a command's subcommands.
291
 		list a command's subcommands.
212
 		`/help /<command_name> <subcommand_name>` will show help about a
293
 		`/help /<command_name> <subcommand_name>` will show help about a
213
 		specific subcommand.
294
 		specific subcommand.
214
 
295
 
296
+		`/help <module_name>` will show help about configuring a module.
297
+
298
+		`/help <keywords>` will do a text search for topics.
299
+
215
 		Parameters
300
 		Parameters
216
 		----------
301
 		----------
217
 		interaction: Interaction
302
 		interaction: Interaction
218
-		command_name: Optional[str]
219
-			Optional name of a command to get specific help for. With or without the leading slash.
220
-		subcommand_name: Optional[str]
221
-			Optional name of a subcommand to get specific help for.
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.
222
 		"""
307
 		"""
223
-		print(f'help_command(interaction, {command_name}, {subcommand_name})')
224
-		cmds: list[Command] = self.bot.tree.get_commands()
225
-		if command_name is None:
308
+		print(f'help_command(interaction, {topic}, {subtopic})')
309
+
310
+		# General help
311
+		if topic is None:
226
 			await self.__send_general_help(interaction)
312
 			await self.__send_general_help(interaction)
227
 			return
313
 			return
228
 
314
 
229
-		if command_name.startswith('/'):
230
-			command_name = command_name[1:]
231
-		cmd = next((c for c in cmds if c.name == command_name), None)
232
-		if cmd is None:
233
-			interaction.response.send_message(
234
-				f'Command `{command_name}` not found!',
235
-				ephemeral=True,
236
-			)
237
-			return
238
-		if subcommand_name is None:
239
-			await self.__send_command_help(interaction, cmd)
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)
240
 			return
319
 			return
241
 
320
 
242
-		if not isinstance(cmd, Group):
243
-			await self.__send_command_help(interaction, cmd, addendum=f'{CONFIG["warning_emoji"]} Command does not have subcommands. Showing help for base command.')
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)
244
 			return
349
 			return
245
-		grp: Group = cmd
246
-		subcmd: Command = next((c for c in grp.commands if c.name == subcommand_name), None)
247
-		if subcmd is None:
248
-			await self.__send_command_help(interaction, cmd, addendum=f'{CONFIG["warning_emoji"]} Command `/{command_name}` does not have a subcommand "{subcommand_name}". Showing help for base command.')
350
+		if isinstance(obj, Group):
351
+			await self.__send_command_help(interaction, obj)
249
 			return
352
 			return
250
-		await self.__send_subcommand_help(interaction, grp, subcmd)
251
-		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
+		)
252
 
362
 
253
 	def get_command_list(self, permissions: Optional[Permissions] = None) -> dict[str, Union[Command, Group]]:
363
 	def get_command_list(self, permissions: Optional[Permissions] = None) -> dict[str, Union[Command, Group]]:
254
 		return { cmd.name: cmd for cmd in self.bot.tree.get_commands() if can_use_command(cmd, permissions) }
364
 		return { cmd.name: cmd for cmd in self.bot.tree.get_commands() if can_use_command(cmd, permissions) }
255
 
365
 
256
 	def get_subcommand_list(self, cmd: Group, permissions: Optional[Permissions] = None) -> dict[str, Command]:
366
 	def get_subcommand_list(self, cmd: Group, permissions: Optional[Permissions] = None) -> dict[str, Command]:
257
-		return { subcmd.name: subcmd for subcmd in cmd.commands if can_use_command(subcmd, permissions) }
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 {}
258
 
372
 
259
 	async def __send_general_help(self, interaction: Interaction) -> None:
373
 	async def __send_general_help(self, interaction: Interaction) -> None:
260
 		user_permissions: Permissions = interaction.permissions
374
 		user_permissions: Permissions = interaction.permissions
261
-		text = f'## :information_source: Commands'
262
-		for cmd_name, cmd in sorted(self.get_command_list(user_permissions).items()):
263
-			text += f'\n- `/{cmd_name}`: {cmd.description}'
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}'
264
 		await interaction.response.send_message(
448
 		await interaction.response.send_message(
265
 			text,
449
 			text,
266
 			ephemeral=True,
450
 			ephemeral=True,
293
 			for param in params:
477
 			for param in params:
294
 				text += f'\n- `{param.name}`: {param.description}'
478
 				text += f'\n- `{param.name}`: {param.description}'
295
 		await interaction.response.send_message(text, ephemeral=True)
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
+		)

+ 2
- 3
rocketbot/cogs/joinraidcog.py Просмотреть файл

44
 				'at when counting recent joins. If joincount or more ' + \
44
 				'at when counting recent joins. If joincount or more ' + \
45
 				'joins occur within jointime seconds a mod warning is issued.',
45
 				'joins occur within jointime seconds a mod warning is issued.',
46
 			usage='<seconds:float>',
46
 			usage='<seconds:float>',
47
-			min_value=1.0,
48
-			max_value=900.0)
47
+			min_value=timedelta(seconds=1.0),
48
+			max_value=timedelta(seconds=900.0))
49
 
49
 
50
 	STATE_KEY_RECENT_JOINS = "JoinRaidCog.recent_joins"
50
 	STATE_KEY_RECENT_JOINS = "JoinRaidCog.recent_joins"
51
 	STATE_KEY_LAST_RAID = "JoinRaidCog.last_raid"
51
 	STATE_KEY_LAST_RAID = "JoinRaidCog.last_raid"
54
 		super().__init__(
54
 		super().__init__(
55
 			bot,
55
 			bot,
56
 			config_prefix='joinraid',
56
 			config_prefix='joinraid',
57
-			name='join raid',
58
 			short_description='Manages join raid detection and handling.',
57
 			short_description='Manages join raid detection and handling.',
59
 		)
58
 		)
60
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)
59
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)

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

48
 		super().__init__(
48
 		super().__init__(
49
 			bot,
49
 			bot,
50
 			config_prefix='logging',
50
 			config_prefix='logging',
51
-			name='logging',
52
-			short_description='Manages event logging',
51
+			short_description='Manages event logging.',
53
 		)
52
 		)
54
 		self.add_setting(LoggingCog.SETTING_ENABLED)
53
 		self.add_setting(LoggingCog.SETTING_ENABLED)
55
 		self.flush_buffers.start()
54
 		self.flush_buffers.start()

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

99
 		super().__init__(
99
 		super().__init__(
100
 			bot,
100
 			bot,
101
 			config_prefix='patterns',
101
 			config_prefix='patterns',
102
-			name='patterns',
103
 			short_description='Manages message pattern matching.',
102
 			short_description='Manages message pattern matching.',
104
 		)
103
 		)
105
 		PatternCog.shared = self
104
 		PatternCog.shared = self

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

45
 				'new members can say anything. Setting to 0 effectively ' + \
45
 				'new members can say anything. Setting to 0 effectively ' + \
46
 				'disables URL spam detection.',
46
 				'disables URL spam detection.',
47
 			usage='<seconds:int>',
47
 			usage='<seconds:int>',
48
-			min_value=0)
48
+			min_value=timedelta(seconds=0))
49
 	SETTING_DECEPTIVE_ACTION = CogSetting('deceptiveaction', Literal['nothing', 'modwarn', 'modwarndelete', 'chatwarn', 'chatwarndelete', 'delete', 'kick', 'ban'],
49
 	SETTING_DECEPTIVE_ACTION = CogSetting('deceptiveaction', Literal['nothing', 'modwarn', 'modwarndelete', 'chatwarn', 'chatwarndelete', 'delete', 'kick', 'ban'],
50
 			brief='action to take on deceptive link markdown',
50
 			brief='action to take on deceptive link markdown',
51
 			description='The action to take on chat messages with links ' + \
51
 			description='The action to take on chat messages with links ' + \
57
 		super().__init__(
57
 		super().__init__(
58
 			bot,
58
 			bot,
59
 			config_prefix='urlspam',
59
 			config_prefix='urlspam',
60
-			name='URL spam',
61
 			short_description='Manages URL spam detection.',
60
 			short_description='Manages URL spam detection.',
62
 		)
61
 		)
63
 		self.add_setting(URLSpamCog.SETTING_ENABLED)
62
 		self.add_setting(URLSpamCog.SETTING_ENABLED)

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

57
 		super().__init__(
57
 		super().__init__(
58
 			bot,
58
 			bot,
59
 			config_prefix='username',
59
 			config_prefix='username',
60
-			name='username patterns',
61
 			short_description='Manages username pattern detection.',
60
 			short_description='Manages username pattern detection.',
62
 		)
61
 		)
63
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)
62
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)

+ 117
- 62
rocketbot/cogsetting.py Просмотреть файл

5
 from typing import Any, Optional, Type, TypeVar, Literal
5
 from typing import Any, Optional, Type, TypeVar, Literal
6
 
6
 
7
 from discord import Interaction, Permissions
7
 from discord import Interaction, Permissions
8
-from discord.app_commands import Range, Transform
9
-from discord.app_commands.commands import Command, Group, CommandCallback
8
+from discord.app_commands import Range, Transform, describe
9
+from discord.app_commands.commands import Command, Group, CommandCallback, rename
10
 from discord.ext.commands import Bot
10
 from discord.ext.commands import Bot
11
 
11
 
12
 from config import CONFIG
12
 from config import CONFIG
13
 from rocketbot.storage import Storage
13
 from rocketbot.storage import Storage
14
-from rocketbot.utils import bot_log, TimeDeltaTransformer
14
+from rocketbot.utils import bot_log, TimeDeltaTransformer, MOD_PERMISSIONS
15
 
15
 
16
 # def _fix_command(command: Command) -> None:
16
 # def _fix_command(command: Command) -> None:
17
 # 	"""
17
 # 	"""
75
 		self.min_value: Optional[Any] = min_value
75
 		self.min_value: Optional[Any] = min_value
76
 		self.max_value: Optional[Any] = max_value
76
 		self.max_value: Optional[Any] = max_value
77
 		self.enum_values: Optional[set[Any]] = enum_values
77
 		self.enum_values: Optional[set[Any]] = enum_values
78
-		if self.enum_values or self.min_value is not None or self.max_value is not None:
79
-			self.description += '\n'
80
 		if self.enum_values:
78
 		if self.enum_values:
81
-			allowed_values = '`' + ('`, `'.join(enum_values)) + '`'
82
-			self.description += f'\nAllowed values: {allowed_values}'
83
-		if self.min_value is not None:
84
-			self.description += f'\nMin value: {self.min_value}'
85
-		if self.max_value is not None:
86
-			self.description += f'\nMax value: {self.max_value}'
87
-		if self.usage is None:
88
-			self.usage = f'<{self.name}>'
79
+			value_list = '`' + ('`, `'.join(self.enum_values)) + '`'
80
+			self.description += f' (Permitted values: {value_list})'
81
+		elif self.min_value is not None and self.max_value is not None:
82
+			self.description += f' (Value must be between `{self.min_value}` and `{self.max_value}`)'
83
+		elif self.min_value is not None:
84
+			self.description += f' (Minimum value: {self.min_value})'
85
+		elif self.max_value is not None:
86
+			self.description += f' (Maximum value: {self.max_value})'
89
 
87
 
90
 	def validate_value(self, new_value: Any) -> None:
88
 	def validate_value(self, new_value: Any) -> None:
91
 		"""
89
 		"""
111
 			self.__get_group.add_command(self.__make_getter_command(cog))
109
 			self.__get_group.add_command(self.__make_getter_command(cog))
112
 			self.__set_group.add_command(self.__make_setter_command(cog))
110
 			self.__set_group.add_command(self.__make_setter_command(cog))
113
 
111
 
112
+	def to_stored_value(self, native_value: Any) -> Any:
113
+		if self.datatype is timedelta:
114
+			return native_value.total_seconds()
115
+		return native_value
116
+
117
+	def to_native_value(self, stored_value: Any) -> Any:
118
+		if self.datatype is timedelta:
119
+			return timedelta(seconds=stored_value)
120
+		return stored_value
121
+
114
 	def __make_getter_command(self, cog: BaseCog) -> Command:
122
 	def __make_getter_command(self, cog: BaseCog) -> Command:
115
 		setting: CogSetting = self
123
 		setting: CogSetting = self
116
 		setting_name = setting.name
124
 		setting_name = setting.name
117
 		if cog.config_prefix is not None:
125
 		if cog.config_prefix is not None:
118
 			setting_name = f'{cog.config_prefix}_{setting_name}'
126
 			setting_name = f'{cog.config_prefix}_{setting_name}'
119
-		async def getter(self, interaction: Interaction) -> None:
127
+		datatype = self.datatype
128
+		async def getter(cog0, interaction: Interaction) -> None:
120
 			print(f"invoking getter for {setting_name}")
129
 			print(f"invoking getter for {setting_name}")
121
-			key = f'{self.__class__.__name__}.{setting.name}'
122
-			value = Storage.get_config_value(interaction.guild, key)
130
+			key = f'{cog0.__class__.__name__}.{setting.name}'
131
+			value = setting.to_native_value(Storage.get_config_value(interaction.guild, key))
123
 			if value is None:
132
 			if value is None:
124
-				value = self.get_cog_default(setting.name)
133
+				value = setting.to_native_value(cog0.get_cog_default(setting.name))
125
 				await interaction.response.send_message(
134
 				await interaction.response.send_message(
126
 					f'{CONFIG["info_emoji"]} `{setting_name}` is using default of `{value}`',
135
 					f'{CONFIG["info_emoji"]} `{setting_name}` is using default of `{value}`',
127
 					ephemeral=True
136
 					ephemeral=True
131
 					f'{CONFIG["info_emoji"]} `{setting_name}` is set to `{value}`',
140
 					f'{CONFIG["info_emoji"]} `{setting_name}` is set to `{value}`',
132
 					ephemeral=True
141
 					ephemeral=True
133
 				)
142
 				)
134
-		setattr(cog.__class__, f'_cmd_get_{setting.name}', getter)
135
-		getter.__qualname__ = f'{self.__class__.__name__}._cmd_get_{setting.name}'
136
-		getter.__self__ = cog
143
+
144
+		# We have to do some surgery to make the getter function a proper method on the cog
145
+		# that discord.py will recognize and wire up correctly. Same for other accessors below.
146
+		setattr(cog.__class__, f'_cmd_get_{setting.name}', getter)  # add method to cog class
147
+		getter.__qualname__ = f'{self.__class__.__name__}._cmd_get_{setting.name}'  # discord.py checks this to know if it's a method vs function
148
+		getter.__self__ = cog  # discord.py uses this as the self argument
149
+
137
 		bot_log(None, cog.__class__, f"Creating /get {setting_name}")
150
 		bot_log(None, cog.__class__, f"Creating /get {setting_name}")
138
 		command = Command(
151
 		command = Command(
139
 			name=setting_name,
152
 			name=setting_name,
140
 			description=f'Shows {self.brief}.',
153
 			description=f'Shows {self.brief}.',
141
 			callback=getter,
154
 			callback=getter,
142
 			parent=CogSetting.__get_group,
155
 			parent=CogSetting.__get_group,
156
+			extras={
157
+				'cog': cog,
158
+				'setting': setting,
159
+				'long_description': setting.description,
160
+			},
143
 		)
161
 		)
144
 		return command
162
 		return command
145
 
163
 
159
 				)
177
 				)
160
 				return
178
 				return
161
 			key = f'{self.__class__.__name__}.{setting.name}'
179
 			key = f'{self.__class__.__name__}.{setting.name}'
162
-			Storage.set_config_value(interaction.guild, key, new_value)
180
+			Storage.set_config_value(interaction.guild, key, setting.to_stored_value(new_value))
163
 			await interaction.response.send_message(
181
 			await interaction.response.send_message(
164
 				f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
182
 				f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
165
 				ephemeral=True
183
 				ephemeral=True
172
 			if self.min_value is not None or self.max_value is not None:
190
 			if self.min_value is not None or self.max_value is not None:
173
 				r_min = self.min_value
191
 				r_min = self.min_value
174
 				r_max = self.max_value
192
 				r_max = self.max_value
175
-				async def setter_range(self, interaction: Interaction, new_value: Range[int, r_min, r_max]) -> None:
176
-					await self.setter_general(interaction, new_value)
193
+				@rename(new_value=self.name)
194
+				@describe(new_value=self.brief)
195
+				async def setter_range(cog0, interaction: Interaction, new_value: Range[int, r_min, r_max]) -> None:
196
+					await setter_general(cog0, interaction, new_value)
177
 				setter = setter_range
197
 				setter = setter_range
178
 			else:
198
 			else:
179
-				async def setter_int(self, interaction: Interaction, new_value: int) -> None:
180
-					await self.setter_general(interaction, new_value)
199
+				@rename(new_value=self.name)
200
+				@describe(new_value=self.brief)
201
+				async def setter_int(cog0, interaction: Interaction, new_value: int) -> None:
202
+					await setter_general(cog0, interaction, new_value)
181
 				setter = setter_int
203
 				setter = setter_int
182
 		elif self.datatype == float:
204
 		elif self.datatype == float:
183
-			async def setter_float(self, interaction: Interaction, new_value: float) -> None:
184
-				await self.setter_general(interaction, new_value)
205
+			@rename(new_value=self.name)
206
+			@describe(new_value=self.brief)
207
+			async def setter_float(cog0, interaction: Interaction, new_value: float) -> None:
208
+				await setter_general(cog0, interaction, new_value)
185
 			setter = setter_float
209
 			setter = setter_float
186
 		elif self.datatype == timedelta:
210
 		elif self.datatype == timedelta:
187
-			async def setter_timedelta(self, interaction: Interaction, new_value: Transform[timedelta, TimeDeltaTransformer]) -> None:
188
-				await self.setter_general(interaction, new_value)
211
+			@rename(new_value=self.name)
212
+			@describe(new_value=f'{self.brief} (e.g. 30s, 5m, 1h30s, or 7d)')
213
+			async def setter_timedelta(cog0, interaction: Interaction, new_value: Transform[timedelta, TimeDeltaTransformer]) -> None:
214
+				await setter_general(cog0, interaction, new_value)
189
 			setter = setter_timedelta
215
 			setter = setter_timedelta
190
 		elif getattr(self.datatype, '__origin__', None) == Literal:
216
 		elif getattr(self.datatype, '__origin__', None) == Literal:
191
 			dt = self.datatype
217
 			dt = self.datatype
192
-			async def setter_enum(self, interaction: Interaction, new_value: dt) -> None:
193
-				await self.setter_general(interaction, new_value)
218
+			@rename(new_value=self.name)
219
+			@describe(new_value=self.brief)
220
+			async def setter_enum(cog0, interaction: Interaction, new_value: dt) -> None:
221
+				await setter_general(cog0, interaction, new_value)
194
 			setter = setter_enum
222
 			setter = setter_enum
195
 		elif self.datatype == str:
223
 		elif self.datatype == str:
196
 			if self.enum_values is not None:
224
 			if self.enum_values is not None:
197
 				raise ValueError('Type for a setting with enum values should be typing.Literal')
225
 				raise ValueError('Type for a setting with enum values should be typing.Literal')
198
 			else:
226
 			else:
199
-				async def setter_str(self, interaction: Interaction, new_value: str) -> None:
200
-					await self.setter_general(interaction, new_value)
227
+				@rename(new_value=self.name)
228
+				@describe(new_value=self.brief)
229
+				async def setter_str(cog0, interaction: Interaction, new_value: str) -> None:
230
+					await setter_general(cog0, interaction, new_value)
201
 				setter = setter_str
231
 				setter = setter_str
202
 		elif setting.datatype == bool:
232
 		elif setting.datatype == bool:
203
-			async def setter_bool(self, interaction: Interaction, new_value: bool) -> None:
204
-				await self.setter_general(interaction, new_value)
233
+			@rename(new_value=self.name)
234
+			@describe(new_value=self.brief)
235
+			async def setter_bool(cog0, interaction: Interaction, new_value: bool) -> None:
236
+				await setter_general(cog0, interaction, new_value)
205
 			setter = setter_bool
237
 			setter = setter_bool
206
 		elif setting.datatype is not None:
238
 		elif setting.datatype is not None:
207
 			raise ValueError(f'Invalid type {self.datatype}')
239
 			raise ValueError(f'Invalid type {self.datatype}')
214
 			description=f'Sets {self.brief}.',
246
 			description=f'Sets {self.brief}.',
215
 			callback=setter,
247
 			callback=setter,
216
 			parent=CogSetting.__set_group,
248
 			parent=CogSetting.__set_group,
249
+			extras={
250
+				'cog': cog,
251
+				'setting': setting,
252
+				'long_description': setting.description,
253
+			},
217
 		)
254
 		)
218
-		# HACK: Passing `cog` in init gets ignored and set to `None` so set after.
219
-		# This ensures the callback is passed the cog as `self` argument.
220
-		# command.cog = cog
221
-		# _fix_command(command)
222
 		return command
255
 		return command
223
 
256
 
224
 	def __make_enable_command(self, cog: BaseCog) -> Command:
257
 	def __make_enable_command(self, cog: BaseCog) -> Command:
225
 		setting: CogSetting = self
258
 		setting: CogSetting = self
226
-		async def enabler(self: BaseCog, interaction: Interaction) -> None:
227
-			print(f"invoking enable for {self.config_prefix}")
228
-			key = f'{self.__class__.__name__}.{setting.name}'
259
+		async def enabler(cog0: BaseCog, interaction: Interaction) -> None:
260
+			print(f"invoking enable for {cog0.config_prefix}")
261
+			key = f'{cog0.__class__.__name__}.{setting.name}'
229
 			Storage.set_config_value(interaction.guild, key, True)
262
 			Storage.set_config_value(interaction.guild, key, True)
230
 			await interaction.response.send_message(
263
 			await interaction.response.send_message(
231
 				f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
264
 				f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
232
 				ephemeral=True
265
 				ephemeral=True
233
 			)
266
 			)
234
-			await self.on_setting_updated(interaction.guild, setting)
235
-			self.log(interaction.guild, f'{interaction.user.name} enabled {self.__class__.__name__}')
267
+			await cog0.on_setting_updated(interaction.guild, setting)
268
+			cog0.log(interaction.guild, f'{interaction.user.name} enabled {cog0.__class__.__name__}')
236
 		setattr(cog.__class__, f'_cmd_enable', enabler)
269
 		setattr(cog.__class__, f'_cmd_enable', enabler)
237
 		enabler.__qualname__ = f'{cog.__class__.__name__}._cmd_enable'
270
 		enabler.__qualname__ = f'{cog.__class__.__name__}._cmd_enable'
238
 		enabler.__self__ = cog
271
 		enabler.__self__ = cog
240
 
273
 
241
 		command = Command(
274
 		command = Command(
242
 			name=cog.config_prefix,
275
 			name=cog.config_prefix,
243
-			description=f'Enables {cog.name} functionality',
276
+			description=f'Enables {cog.qualified_name} functionality.',
244
 			callback=enabler,
277
 			callback=enabler,
245
 			parent=CogSetting.__enable_group,
278
 			parent=CogSetting.__enable_group,
279
+			extras={
280
+				'cog': cog,
281
+				'setting': setting,
282
+				'long_description': setting.description,
283
+			},
246
 		)
284
 		)
247
-		# command.cog = cog
248
-		# _fix_command(command)
249
 		return command
285
 		return command
250
 
286
 
251
 	def __make_disable_command(self, cog: BaseCog) -> Command:
287
 	def __make_disable_command(self, cog: BaseCog) -> Command:
252
 		setting: CogSetting = self
288
 		setting: CogSetting = self
253
-		async def disabler(self: BaseCog, interaction: Interaction) -> None:
254
-			print(f"invoking disable for {self.config_prefix}")
255
-			key = f'{self.__class__.__name__}.{setting.name}'
289
+		async def disabler(cog0: BaseCog, interaction: Interaction) -> None:
290
+			print(f"invoking disable for {cog0.config_prefix}")
291
+			key = f'{cog0.__class__.__name__}.{setting.name}'
256
 			Storage.set_config_value(interaction.guild, key, False)
292
 			Storage.set_config_value(interaction.guild, key, False)
257
 			await interaction.response.send_message(
293
 			await interaction.response.send_message(
258
 				f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
294
 				f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
259
 				ephemeral=True
295
 				ephemeral=True
260
 			)
296
 			)
261
-			await self.on_setting_updated(interaction.guild, setting)
262
-			self.log(interaction.guild, f'{interaction.user.name} disabled {self.__class__.__name__}')
297
+			await cog0.on_setting_updated(interaction.guild, setting)
298
+			cog0.log(interaction.guild, f'{interaction.user.name} disabled {cog0.__class__.__name__}')
263
 		setattr(cog.__class__, f'_cmd_disable', disabler)
299
 		setattr(cog.__class__, f'_cmd_disable', disabler)
264
 		disabler.__qualname__ = f'{cog.__class__.__name__}._cmd_disable'
300
 		disabler.__qualname__ = f'{cog.__class__.__name__}._cmd_disable'
265
 		disabler.__self__ = cog
301
 		disabler.__self__ = cog
270
 			description=f'Disables {cog.config_prefix} functionality',
306
 			description=f'Disables {cog.config_prefix} functionality',
271
 			callback=disabler,
307
 			callback=disabler,
272
 			parent=CogSetting.__disable_group,
308
 			parent=CogSetting.__disable_group,
309
+			extras={
310
+				'cog': cog,
311
+				'setting': setting,
312
+				'long_description': setting.description,
313
+			},
273
 		)
314
 		)
274
-		# command.cog = cog
275
-		# _fix_command(command)
276
 		return command
315
 		return command
277
 
316
 
278
 	__has_set_up_base_commands: bool = False
317
 	__has_set_up_base_commands: bool = False
303
 		cls.__has_set_up_base_commands = True
342
 		cls.__has_set_up_base_commands = True
304
 		cls.__set_group = Group(
343
 		cls.__set_group = Group(
305
 			name='set',
344
 			name='set',
306
-			description='Sets a bot configuration value for this guild',
307
-			default_permissions=cls.permissions
345
+			description='Sets a configuration value for this guild.',
346
+			default_permissions=MOD_PERMISSIONS,
347
+			extras={
348
+				'long_description': 'Settings are guild-specific. If no value is set, a default is used. Use `/get` to '
349
+									'see the current value for this guild.',
350
+			},
308
 		)
351
 		)
309
 		cls.__get_group = Group(
352
 		cls.__get_group = Group(
310
 			name='get',
353
 			name='get',
311
-			description='Shows a configured bot value for this guild',
312
-			default_permissions=cls.permissions
354
+			description='Shows a configuration value for this guild.',
355
+			default_permissions=MOD_PERMISSIONS,
356
+			extras={
357
+				'long_description': 'Settings are guild-specific. Shows the configured value or default value for a '
358
+									'variable for this guild. Use `/set` to change the value.',
359
+			},
313
 		)
360
 		)
314
 		cls.__enable_group = Group(
361
 		cls.__enable_group = Group(
315
 			name='enable',
362
 			name='enable',
316
-			description='Enables bot functionality for this guild',
317
-			default_permissions=cls.permissions
363
+			description='Enables a module for this guild',
364
+			default_permissions=MOD_PERMISSIONS,
365
+			extras={
366
+				'long_description': 'Modules are enabled on a per-guild basis and are off by default. Use `/disable` '
367
+									'to disable an enabled module.',
368
+			},
318
 		)
369
 		)
319
 		cls.__disable_group = Group(
370
 		cls.__disable_group = Group(
320
 			name='disable',
371
 			name='disable',
321
-			description='Disables bot functionality for this guild',
322
-			default_permissions=cls.permissions
372
+			description='Disables a module for this guild.',
373
+			default_permissions=MOD_PERMISSIONS,
374
+			extras={
375
+				'long_description': 'Modules are enabled on a per-guild basis and are off by default. Use `/enable` '
376
+					                're-enable a disabled module.',
377
+			},
323
 		)
378
 		)
324
 		bot.tree.add_command(cls.__set_group)
379
 		bot.tree.add_command(cls.__set_group)
325
 		bot.tree.add_command(cls.__get_group)
380
 		bot.tree.add_command(cls.__get_group)

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