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

Lots more help improvements

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

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

38
 		bool,
38
 		bool,
39
 		default_value=False,
39
 		default_value=False,
40
 		brief='autokick',
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
 	SETTING_BAN_COUNT = CogSetting(
43
 	SETTING_BAN_COUNT = CogSetting(
44
 		'bancount',
44
 		'bancount',

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

32
 		default_permissions=MOD_PERMISSIONS,
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
 	async def set_warning_channel(self, interaction: Interaction) -> None:
44
 	async def set_warning_channel(self, interaction: Interaction) -> None:
37
 		"""
45
 		"""
38
 		Sets mod warnings to post in the current channel.
46
 		Sets mod warnings to post in the current channel.
51
 			ephemeral=True,
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
 	async def get_warning_channel(self, interaction: Interaction) -> None:
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
 		guild: Guild = interaction.guild
66
 		guild: Guild = interaction.guild
63
 		channel_id = Storage.get_config_value(guild, ConfigKey.WARNING_CHANNEL_ID)
67
 		channel_id = Storage.get_config_value(guild, ConfigKey.WARNING_CHANNEL_ID)
64
 		if channel_id is None:
68
 		if channel_id is None:
73
 				ephemeral=True,
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
 	async def set_warning_mention(self,
86
 	async def set_warning_mention(self,
78
 			interaction: Interaction,
87
 			interaction: Interaction,
79
 			mention: Optional[Union[User, Role]] = None) -> None:
88
 			mention: Optional[Union[User, Role]] = None) -> None:
80
 		"""
89
 		"""
81
 		Sets a user/role to mention in warning messages.
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
 		Parameters
92
 		Parameters
88
 		----------
93
 		----------
89
 		interaction: Interaction
94
 		interaction: Interaction
90
 		mention: User or Role
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
 		guild: Guild = interaction.guild
98
 		guild: Guild = interaction.guild
94
 		Storage.set_config_value(guild, ConfigKey.WARNING_MENTION, mention.mention if mention else None)
99
 		Storage.set_config_value(guild, ConfigKey.WARNING_MENTION, mention.mention if mention else None)
103
 				ephemeral=True,
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
 	async def get_warning_mention(self, interaction: Interaction) -> None:
114
 	async def get_warning_mention(self, interaction: Interaction) -> None:
108
-		"""
109
-		Shows the configured user/role to mention in warning messages.
110
-		"""
111
 		guild: Guild = interaction.guild
115
 		guild: Guild = interaction.guild
112
 		mention: str = Storage.get_config_value(guild, ConfigKey.WARNING_MENTION)
116
 		mention: str = Storage.get_config_value(guild, ConfigKey.WARNING_MENTION)
113
 		if mention is None:
117
 		if mention is None:

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

35
 	"""
35
 	"""
36
 	Detects a user posting in multiple channels in a short period
36
 	Detects a user posting in multiple channels in a short period
37
 	of time: a common pattern for spammers.
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
 	SETTING_ENABLED = CogSetting(
39
 	SETTING_ENABLED = CogSetting(
49
 		'enabled',
40
 		'enabled',
121
 			bot,
112
 			bot,
122
 			config_prefix='crosspost',
113
 			config_prefix='crosspost',
123
 			short_description='Manages crosspost detection and handling.',
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
 		self.add_setting(CrossPostCog.SETTING_ENABLED)
128
 		self.add_setting(CrossPostCog.SETTING_ENABLED)
126
 		self.add_setting(CrossPostCog.SETTING_WARN_COUNT)
129
 		self.add_setting(CrossPostCog.SETTING_WARN_COUNT)

+ 24
- 26
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
5
 from datetime import datetime, timedelta, timezone
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
 from discord.errors import DiscordException
9
 from discord.errors import DiscordException
12
 from discord.ext.commands import Cog
10
 from discord.ext.commands import Cog
13
 
11
 
14
 from config import CONFIG
12
 from config import CONFIG
15
 from rocketbot.bot import Rocketbot
13
 from rocketbot.bot import Rocketbot
16
 from rocketbot.cogs.basecog import BaseCog, BotMessage
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
 from rocketbot.storage import ConfigKey, Storage
16
 from rocketbot.storage import ConfigKey, Storage
19
 
17
 
20
 class GeneralCog(BaseCog, name='General'):
18
 class GeneralCog(BaseCog, name='General'):
66
 	@command(
64
 	@command(
67
 		description='Posts a test warning.',
65
 		description='Posts a test warning.',
68
 		extras={
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
 	@guild_only()
72
 	@guild_only()
90
 			)
89
 			)
91
 
90
 
92
 	@command(
91
 	@command(
93
-		description='Greets the user.',
92
+		description='Responds to the user with a greeting.',
94
 		extras={
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
 	async def hello(self, interaction: Interaction):
98
 	async def hello(self, interaction: Interaction):
100
-		"""Command handler"""
101
 		await interaction.response.send_message(
99
 		await interaction.response.send_message(
102
 			f'Hey, {interaction.user.name}!',
100
 			f'Hey, {interaction.user.name}!',
103
 		 	ephemeral=True,
101
 		 	ephemeral=True,
106
 	@command(
104
 	@command(
107
 		description='Shuts down the bot.',
105
 		description='Shuts down the bot.',
108
 		extras={
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
 	@guild_only()
111
 	@guild_only()
118
 		await self.bot.close()
116
 		await self.bot.close()
119
 
117
 
120
 	@command(
118
 	@command(
121
-		description='Mass deletes messages.',
119
+		description='Mass deletes recent messages by a user.',
122
 		extras={
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
 			'usage': '<user:id|mention> <age:timespan>',
124
 			'usage': '<user:id|mention> <age:timespan>',
128
 		},
125
 		},
129
 	)
126
 	)
130
 	@guild_only()
127
 	@guild_only()
131
 	@default_permissions(manage_messages=True)
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
 		Mass deletes messages.
131
 		Mass deletes messages.
135
 
132
 
136
 		Parameters
133
 		Parameters
137
 		----------
134
 		----------
138
 		interaction: :class:`Interaction`
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
 		age: :class:`timedelta`
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
 		cutoff: datetime = datetime.now(timezone.utc) - age
142
 		cutoff: datetime = datetime.now(timezone.utc) - age
146
 
143
 
144
+		# Finding and deleting messages takes time but interaction needs a timely acknowledgement.
147
 		resp = await interaction.response.defer(ephemeral=True, thinking=True)
145
 		resp = await interaction.response.defer(ephemeral=True, thinking=True)
148
 
146
 
149
 		def predicate(message: Message) -> bool:
147
 		def predicate(message: Message) -> bool:
159
 		if len(deleted_messages) > 0:
157
 		if len(deleted_messages) > 0:
160
 			await resp.resource.edit(
158
 			await resp.resource.edit(
161
 				content=f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} '
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
 		else:
162
 		else:
165
 			await resp.resource.edit(
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
 						'from the past {describe_timedelta(age)}.',
165
 						'from the past {describe_timedelta(age)}.',
168
 			)
166
 			)

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

4
 
4
 
5
 from discord import Interaction, Permissions, AppCommandType
5
 from discord import Interaction, Permissions, AppCommandType
6
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
6
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
7
-from discord.ext.commands import cog
8
 
7
 
9
 from config import CONFIG
8
 from config import CONFIG
10
 from rocketbot.bot import Rocketbot
9
 from rocketbot.bot import Rocketbot
13
 
12
 
14
 HelpTopic = Union[Command, Group, BaseCog]
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
 			if include_full_command:
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
 	return Choice(name='', value='')
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
 async def search_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
28
 async def search_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
100
 	try:
29
 	try:
101
 		if len(current) == 0:
30
 		if len(current) == 0:
102
 			return [
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
 		return [
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
 	except BaseException as e:
39
 	except BaseException as e:
111
 		dump_stacktrace(e)
40
 		dump_stacktrace(e)
118
 		super().__init__(
47
 		super().__init__(
119
 			bot,
48
 			bot,
120
 			config_prefix='help',
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
 		HelpCog.shared = self
52
 		HelpCog.shared = self
124
 
53
 
125
 	def __create_help_index(self) -> None:
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
 		populated. Intended to be run on demand so all cogs and commands have
57
 		populated. Intended to be run on demand so all cogs and commands have
129
 		had time to get set up and synced.
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
 			return
61
 			return
133
-		self.obj_index: dict[str, HelpTopic] = {}
62
+		self.topic_index: dict[str, HelpTopic] = {}
134
 		self.keyword_index: dict[str, set[HelpTopic]] = {}
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
 			words = [
66
 			words = [
138
 				word
67
 				word
139
 				for word in re.split(r"[^a-zA-Z']+", text.lower())
68
 				for word in re.split(r"[^a-zA-Z']+", text.lower())
141
 			]
70
 			]
142
 			for word in words:
71
 			for word in words:
143
 				matches = self.keyword_index.get(word, set())
72
 				matches = self.keyword_index.get(word, set())
144
-				matches.add(obj)
73
+				matches.add(topic)
145
 				self.keyword_index[word] = matches
74
 				self.keyword_index[word] = matches
146
 
75
 
147
 		cmds = self.all_commands()
76
 		cmds = self.all_commands()
148
 		for cmd in cmds:
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
 			add_text_to_index(cmd, cmd.name)
80
 			add_text_to_index(cmd, cmd.name)
152
 			if cmd.description:
81
 			if cmd.description:
153
 				add_text_to_index(cmd, cmd.description)
82
 				add_text_to_index(cmd, cmd.description)
154
 			if isinstance(cmd, Group):
83
 			if isinstance(cmd, Group):
155
 				for subcmd in cmd.commands:
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
 					add_text_to_index(subcmd, cmd.name)
87
 					add_text_to_index(subcmd, cmd.name)
159
 					add_text_to_index(subcmd, subcmd.name)
88
 					add_text_to_index(subcmd, subcmd.name)
160
 					if subcmd.description:
89
 					if subcmd.description:
163
 			if not isinstance(cog, BaseCog):
92
 			if not isinstance(cog, BaseCog):
164
 				continue
93
 				continue
165
 			key = f'cog:{cog_qname}'
94
 			key = f'cog:{cog_qname}'
166
-			self.obj_index[key] = cog
95
+			self.topic_index[key] = cog
167
 			add_text_to_index(cog, cog.qualified_name)
96
 			add_text_to_index(cog, cog.qualified_name)
168
 			if cog.description:
97
 			if cog.description:
169
 				add_text_to_index(cog, cog.description)
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
 		self.__create_help_index()
101
 		self.__create_help_index()
173
-		return self.obj_index.get(symbol, None)
102
+		return self.topic_index.get(symbol, None)
174
 
103
 
175
 	def all_commands(self) -> list[Union[Command, Group]]:
104
 	def all_commands(self) -> list[Union[Command, Group]]:
176
 		# PyCharm not interpreting conditional return type correctly.
105
 		# PyCharm not interpreting conditional return type correctly.
202
 			if can_use_cog(cog, permissions)
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
 		if include_cogs:
139
 		if include_cogs:
211
-			objs += self.all_accessible_cogs(permissions)
140
+			topics += self.all_accessible_cogs(permissions)
212
 		if include_commands:
141
 		if include_commands:
213
-			objs += self.all_accessible_commands(permissions)
142
+			topics += self.all_accessible_commands(permissions)
214
 		if include_subcommands:
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
 		self.__create_help_index()
148
 		self.__create_help_index()
220
 
149
 
221
 		# Break into words (or word fragments)
150
 		# Break into words (or word fragments)
229
 		# to known indexed keywords, then collecting those associated results. Should
158
 		# to known indexed keywords, then collecting those associated results. Should
230
 		# just keep corpuses of searchable, normalized text for each topic and do a
159
 		# just keep corpuses of searchable, normalized text for each topic and do a
231
 		# direct `in` test.
160
 		# direct `in` test.
232
-		matching_objects_set = None
161
+		matching_topics_set = None
233
 		for word in words:
162
 		for word in words:
234
 			word_matches = set()
163
 			word_matches = set()
235
 			for k in self.keyword_index.keys():
164
 			for k in self.keyword_index.keys():
236
 				if word in k:
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
 			else:
171
 			else:
243
-				matching_objects_set = matching_objects_set & word_matches
172
+				matching_topics_set = matching_topics_set & word_matches
244
 
173
 
245
 		# Filter by accessibility
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
 		# Sort and return
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
 	@guild_only()
204
 	@guild_only()
262
 	@autocomplete(search=search_autocomplete)
205
 	@autocomplete(search=search_autocomplete)
263
 	async def help_command(self, interaction: Interaction, search: Optional[str]) -> None:
206
 	async def help_command(self, interaction: Interaction, search: Optional[str]) -> None:
264
 		"""
207
 		"""
265
 		Shows help for using commands and subcommands and configuring modules.
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
 		Parameters
210
 		Parameters
280
 		----------
211
 		----------
281
 		interaction: Interaction
212
 		interaction: Interaction
285
 		if search is None:
216
 		if search is None:
286
 			await self.__send_general_help(interaction)
217
 			await self.__send_general_help(interaction)
287
 			return
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
 			return
222
 			return
292
-		matches = self.objects_for_keywords(search, interaction.permissions)
223
+		matches = self.topics_for_keywords(search, interaction.permissions)
293
 		await self.__send_keyword_help(interaction, matches)
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
 			return
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
 			return
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
 			return
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
 		await interaction.response.send_message(
237
 		await interaction.response.send_message(
310
 			f'{CONFIG["failure_emoji"]} Failed to get help info.',
238
 			f'{CONFIG["failure_emoji"]} Failed to get help info.',
311
 			ephemeral=True,
239
 			ephemeral=True,
361
 			ephemeral=True,
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
 		matching_commands = [
293
 		matching_commands = [
366
 			cmd
294
 			cmd
367
-			for cmd in matching_objects or []
295
+			for cmd in matching_topics or []
368
 			if isinstance(cmd, Command) or isinstance(cmd, Group)
296
 			if isinstance(cmd, Command) or isinstance(cmd, Group)
369
 		]
297
 		]
370
 		matching_cogs = [
298
 		matching_cogs = [
371
 			cog
299
 			cog
372
-			for cog in matching_objects or []
300
+			for cog in matching_topics or []
373
 			if isinstance(cog, BaseCog)
301
 			if isinstance(cog, BaseCog)
374
 		]
302
 		]
375
 		if len(matching_commands) + len(matching_cogs) == 0:
303
 		if len(matching_commands) + len(matching_cogs) == 0:
379
 				delete_after=10,
307
 				delete_after=10,
380
 			)
308
 			)
381
 			return
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
 			return
313
 			return
386
 
314
 
387
 		text = '## :information_source: Matching Help Topics'
315
 		text = '## :information_source: Matching Help Topics'
393
 				else:
321
 				else:
394
 					text += f'\n- `/{cmd.name}`'
322
 					text += f'\n- `/{cmd.name}`'
395
 		if len(matching_cogs) > 0:
323
 		if len(matching_cogs) > 0:
396
-			text += '\n### Cogs'
324
+			text += '\n### Modules'
397
 			for cog in matching_cogs:
325
 			for cog in matching_cogs:
398
 				text += f'\n- {cog.qualified_name}'
326
 				text += f'\n- {cog.qualified_name}'
399
 		await interaction.response.send_message(
327
 		await interaction.response.send_message(
405
 		text = ''
333
 		text = ''
406
 		if addendum is not None:
334
 		if addendum is not None:
407
 			text += addendum + '\n\n'
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
 		if isinstance(command_or_group, Group):
358
 		if isinstance(command_or_group, Group):
410
 			subcmds: dict[str, Command] = self.get_subcommand_list(command_or_group, permissions=interaction.permissions)
359
 			subcmds: dict[str, Command] = self.get_subcommand_list(command_or_group, permissions=interaction.permissions)
411
 			if len(subcmds) > 0:
360
 			if len(subcmds) > 0:
412
 				text += '\n### Subcommands:'
361
 				text += '\n### Subcommands:'
413
 				for subcmd_name, subcmd in sorted(subcmds.items()):
362
 				for subcmd_name, subcmd in sorted(subcmds.items()):
414
 					text += f'\n- `{subcmd_name}`: {subcmd.description}'
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
 		else:
366
 		else:
416
 			params = command_or_group.parameters
367
 			params = command_or_group.parameters
417
 			if len(params) > 0:
368
 			if len(params) > 0:
418
 				text += '\n### Parameters:'
369
 				text += '\n### Parameters:'
419
 				for param in params:
370
 				for param in params:
420
 					text += f'\n- `{param.name}`: {param.description}'
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
 		await interaction.response.send_message(text, ephemeral=True)
374
 		await interaction.response.send_message(text, ephemeral=True)
433
 
375
 
434
 	async def __send_cog_help(self, interaction: Interaction, cog: BaseCog) -> None:
376
 	async def __send_cog_help(self, interaction: Interaction, cog: BaseCog) -> None:
435
 		text = f'## :information_source: Module Help'
377
 		text = f'## :information_source: Module Help'
436
 		text += f'\n**{cog.qualified_name}** module'
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
 		cmds = [
384
 		cmds = [
441
 			cmd
385
 			cmd

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

34
 		bool,
34
 		bool,
35
 		default_value=False,
35
 		default_value=False,
36
 		brief='join raid detection',
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
 	SETTING_JOIN_COUNT = CogSetting(
39
 	SETTING_JOIN_COUNT = CogSetting(
40
 		'joincount',
40
 		'joincount',
65
 			bot,
65
 			bot,
66
 			config_prefix='joinraid',
66
 			config_prefix='joinraid',
67
 			short_description='Manages join raid detection and handling.',
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
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)
72
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)
70
 		self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)
73
 		self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)

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

43
 		bool,
43
 		bool,
44
 		default_value=False,
44
 		default_value=False,
45
 		brief='logging',
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
 	STATE_EVENT_BUFFER = 'LoggingCog.eventBuffer'
49
 	STATE_EVENT_BUFFER = 'LoggingCog.eventBuffer'

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

100
 			bot,
100
 			bot,
101
 			config_prefix='patterns',
101
 			config_prefix='patterns',
102
 			short_description='Manages message pattern matching.',
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
 		PatternCog.shared = self
105
 		PatternCog.shared = self
105
 
106
 
262
 		description='Manages message pattern matching.',
263
 		description='Manages message pattern matching.',
263
 		guild_only=True,
264
 		guild_only=True,
264
 		default_permissions=MOD_PERMISSIONS,
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
 	@autocomplete(
280
 	@autocomplete(
270
 		name=pattern_name_autocomplete,
281
 		name=pattern_name_autocomplete,
271
 		# actions=action_autocomplete
282
 		# actions=action_autocomplete
280
 		"""
291
 		"""
281
 		Adds a custom pattern.
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
 		Parameters
294
 		Parameters
288
 		----------
295
 		----------
289
 		interaction : Interaction
296
 		interaction : Interaction
290
 		name : str
297
 		name : str
291
-			A name for the pattern.
298
+			a name for the new or existing pattern
292
 		actions : str
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
 		expression : str
301
 		expression : str
295
-			Criteria for matching chat messages.
302
+			criteria for matching chat messages
296
 		"""
303
 		"""
297
 		pattern_str = f'{actions} if {expression}'
304
 		pattern_str = f'{actions} if {expression}'
298
 		guild = interaction.guild
305
 		guild = interaction.guild
313
 			)
320
 			)
314
 
321
 
315
 	@pattern.command(
322
 	@pattern.command(
316
-		description='Removes a custom pattern',
323
+		description='Removes a custom pattern.',
317
 		extras={
324
 		extras={
318
 			'usage': '<pattern_name>',
325
 			'usage': '<pattern_name>',
319
 		},
326
 		},
321
 	@autocomplete(name=pattern_name_autocomplete)
328
 	@autocomplete(name=pattern_name_autocomplete)
322
 	async def remove(self, interaction: Interaction, name: str):
329
 	async def remove(self, interaction: Interaction, name: str):
323
 		"""
330
 		"""
324
-		Command handler
331
+		Removes a custom pattern.
325
 
332
 
326
 		Parameters
333
 		Parameters
327
 		----------
334
 		----------
328
 		interaction: Interaction
335
 		interaction: Interaction
329
 		name: str
336
 		name: str
330
-			Name of the pattern to remove.
337
+			name of the pattern to remove
331
 		"""
338
 		"""
332
 		guild = interaction.guild
339
 		guild = interaction.guild
333
 		patterns = self.get_patterns(guild)
340
 		patterns = self.get_patterns(guild)
345
 			)
352
 			)
346
 
353
 
347
 	@pattern.command(
354
 	@pattern.command(
348
-		description='Lists all patterns',
355
+		description='Lists all patterns.',
349
 	)
356
 	)
350
 	async def list(self, interaction: Interaction) -> None:
357
 	async def list(self, interaction: Interaction) -> None:
351
-		"""
352
-		Command handler
353
-
354
-		Parameters
355
-		----------
356
-		interaction: Interaction
357
-		"""
358
 		guild = interaction.guild
358
 		guild = interaction.guild
359
 		patterns = self.get_patterns(guild)
359
 		patterns = self.get_patterns(guild)
360
 		if len(patterns) == 0:
360
 		if len(patterns) == 0:
369
 		await interaction.response.send_message(msg, ephemeral=True)
369
 		await interaction.response.send_message(msg, ephemeral=True)
370
 
370
 
371
 	@pattern.command(
371
 	@pattern.command(
372
-		description='Sets a pattern\'s priority level',
372
+		description="Sets a pattern's priority level.",
373
 		extras={
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
 	@autocomplete(name=pattern_name_autocomplete, priority=priority_autocomplete)
380
 	@autocomplete(name=pattern_name_autocomplete, priority=priority_autocomplete)
381
 	async def setpriority(self, interaction: Interaction, name: str, priority: int) -> None:
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
 		Parameters
385
 		Parameters
386
 		----------
386
 		----------
387
 		interaction: Interaction
387
 		interaction: Interaction
388
 		name: str
388
 		name: str
389
-			A name for the pattern
389
+			the name of the pattern
390
 		priority: int
390
 		priority: int
391
-			Priority for evaluating the pattern. Default is 100. Higher values match first.
391
+			evaluation priority
392
 		"""
392
 		"""
393
 		guild = interaction.guild
393
 		guild = interaction.guild
394
 		patterns = self.get_patterns(guild)
394
 		patterns = self.get_patterns(guild)

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

52
 		bool,
52
 		bool,
53
 		default_value=False,
53
 		default_value=False,
54
 		brief='username pattern detection',
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
 	SETTING_PATTERNS = CogSetting('patterns', None, default_value=None)
58
 	SETTING_PATTERNS = CogSetting('patterns', None, default_value=None)
62
 			bot,
62
 			bot,
63
 			config_prefix='username',
63
 			config_prefix='username',
64
 			short_description='Manages username pattern detection.',
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
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)
68
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)
67
 
69
 
89
 		description='Manages username pattern detection.',
91
 		description='Manages username pattern detection.',
90
 		guild_only=True,
92
 		guild_only=True,
91
 		default_permissions=MOD_PERMISSIONS,
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
 	@username.command(
100
 	@username.command(
95
-		description='Adds a username pattern to match against new members.',
101
+		description='Adds a username pattern.',
96
 		extras={
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
 			'usage': '<pattern>',
106
 			'usage': '<pattern>',
99
 		},
107
 		},
100
 	)
108
 	)
126
 	@username.command(
134
 	@username.command(
127
 		description='Removes a username pattern.',
135
 		description='Removes a username pattern.',
128
 		extras={
136
 		extras={
129
-			'long_description': 'Removes an existing username pattern',
130
 			'usage': '<pattern>',
137
 			'usage': '<pattern>',
131
 		},
138
 		},
132
 	)
139
 	)

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

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

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