Explorar el Código

Other slash commands working

pull/13/head
Rocketsoup hace 2 meses
padre
commit
6bcca013c4

+ 10
- 0
rocketbot/bot.py Ver fichero

16
 	"""
16
 	"""
17
 	def __init__(self, command_prefix, **kwargs):
17
 	def __init__(self, command_prefix, **kwargs):
18
 		super().__init__(command_prefix, **kwargs)
18
 		super().__init__(command_prefix, **kwargs)
19
+		self.__commands_set_up = False
19
 
20
 
20
 	async def on_command_error(self, context: commands.Context, exception: BaseException) -> None:
21
 	async def on_command_error(self, context: commands.Context, exception: BaseException) -> None:
21
 		bot_log(None, None, f'Command error')
22
 		bot_log(None, None, f'Command error')
43
 			traceback.format_exc())
44
 			traceback.format_exc())
44
 
45
 
45
 	async def on_ready(self):
46
 	async def on_ready(self):
47
+		if not self.__commands_set_up:
48
+			await self.__set_up_commands()
49
+		bot_log(None, None, 'Bot done initializing')
50
+		print('----------------------------------------------------------')
51
+
52
+	async def __set_up_commands(self):
53
+		if self.__commands_set_up:
54
+			return
55
+		self.__commands_set_up = True
46
 		for cog in self.cogs.values():
56
 		for cog in self.cogs.values():
47
 			if isinstance(cog, BaseCog):
57
 			if isinstance(cog, BaseCog):
48
 				bcog: BaseCog = cog
58
 				bcog: BaseCog = cog

+ 6
- 11
rocketbot/cogs/autokickcog.py Ver fichero

53
 	STATE_KEY_RECENT_KICKS = "AutoKickCog.recent_joins"
53
 	STATE_KEY_RECENT_KICKS = "AutoKickCog.recent_joins"
54
 
54
 
55
 	def __init__(self, bot):
55
 	def __init__(self, bot):
56
-		super().__init__(bot, 'autokick')
56
+		super().__init__(
57
+			bot,
58
+			config_prefix='autokick',
59
+			name='auto-kick',
60
+			short_description='Automatically kicks all new users as soon as they join.',
61
+		)
57
 		self.add_setting(AutoKickCog.SETTING_ENABLED)
62
 		self.add_setting(AutoKickCog.SETTING_ENABLED)
58
 		self.add_setting(AutoKickCog.SETTING_BAN_COUNT)
63
 		self.add_setting(AutoKickCog.SETTING_BAN_COUNT)
59
 		self.add_setting(AutoKickCog.SETTING_OFFLINE_ONLY)
64
 		self.add_setting(AutoKickCog.SETTING_OFFLINE_ONLY)
61
 		timer: Loop = self.status_check_timer
66
 		timer: Loop = self.status_check_timer
62
 		timer.start()
67
 		timer.start()
63
 
68
 
64
-	@commands.group(
65
-		brief='Automatically kicks all new users as soon as they join',
66
-	)
67
-	@commands.has_permissions(ban_members=True)
68
-	@commands.guild_only()
69
-	async def autokick(self, context: commands.Context):
70
-		"""Auto-kick"""
71
-		if context.invoked_subcommand is None:
72
-			await context.send_help()
73
-
74
 	@commands.Cog.listener()
69
 	@commands.Cog.listener()
75
 	async def on_member_join(self, member: Member) -> None:
70
 	async def on_member_join(self, member: Member) -> None:
76
 		"""Event handler"""
71
 		"""Event handler"""

+ 32
- 6
rocketbot/cogs/basecog.py Ver fichero

4
 from datetime import datetime, timedelta, timezone
4
 from datetime import datetime, timedelta, timezone
5
 from typing import Optional, ForwardRef
5
 from typing import Optional, ForwardRef
6
 
6
 
7
-from discord import Guild, Member, Message, RawReactionActionEvent, TextChannel
7
+import discord
8
+from discord import Guild, Interaction, Member, Message, RawReactionActionEvent, TextChannel
8
 from discord.abc import GuildChannel
9
 from discord.abc import GuildChannel
9
-from discord.ext import commands
10
+from discord.app_commands import AppCommandError
11
+from discord.app_commands.errors import CommandInvokeError
12
+from discord.ext.commands import Cog
10
 
13
 
11
 from config import CONFIG
14
 from config import CONFIG
12
 from rocketbot.botmessage import BotMessage, BotMessageReaction
15
 from rocketbot.botmessage import BotMessage, BotMessageReaction
13
 from rocketbot.cogsetting import CogSetting
16
 from rocketbot.cogsetting import CogSetting
14
 from rocketbot.collections import AgeBoundDict
17
 from rocketbot.collections import AgeBoundDict
15
 from rocketbot.storage import Storage
18
 from rocketbot.storage import Storage
16
-from rocketbot.utils import bot_log
19
+from rocketbot.utils import bot_log, dump_stacktrace
17
 
20
 
18
 Rocketbot = ForwardRef('rocketbot.bot.Rocketbot')
21
 Rocketbot = ForwardRef('rocketbot.bot.Rocketbot')
19
 
22
 
22
 		self.member = member
25
 		self.member = member
23
 		self.last_warned = warn_time
26
 		self.last_warned = warn_time
24
 
27
 
25
-class BaseCog(commands.Cog):
28
+class BaseCog(Cog):
26
 	STATE_KEY_RECENT_WARNINGS = "BaseCog.recent_warnings"
29
 	STATE_KEY_RECENT_WARNINGS = "BaseCog.recent_warnings"
27
 
30
 
28
 	"""
31
 	"""
29
 	Superclass for all Rocketbot cogs. Provides lots of conveniences for
32
 	Superclass for all Rocketbot cogs. Provides lots of conveniences for
30
 	common tasks.
33
 	common tasks.
31
 	"""
34
 	"""
32
-	def __init__(self, bot: Rocketbot, config_prefix: Optional[str]):
35
+	def __init__(
36
+			self,
37
+			bot: Rocketbot,
38
+			config_prefix: Optional[str],
39
+			name: str,
40
+			short_description: str,
41
+			long_description: Optional[str] = None,
42
+	):
33
 		"""
43
 		"""
34
 		Parameters
44
 		Parameters
35
 		----------
45
 		----------
44
 		self.are_settings_setup: bool = False
54
 		self.are_settings_setup: bool = False
45
 		self.settings: list[CogSetting] = []
55
 		self.settings: list[CogSetting] = []
46
 		self.config_prefix: Optional[str] = config_prefix
56
 		self.config_prefix: Optional[str] = config_prefix
57
+		self.name: str = name
58
+		self.short_description: str = short_description
59
+		self.long_description: str = long_description
60
+
61
+	async def cog_app_command_error(self, interaction: Interaction, error: AppCommandError) -> None:
62
+		if isinstance(error, CommandInvokeError):
63
+			error = error.original
64
+		dump_stacktrace(error)
65
+		message = f"\nException: {error.__class__.__name__}, "\
66
+				  f"Command: {interaction.command.qualified_name if interaction.command else None}, "\
67
+				  f"User: {interaction.user}, "\
68
+				  f"Time: {discord.utils.format_dt(interaction.created_at, style='F')}"
69
+		try:
70
+			await interaction.response.send_message(f"An error occurred: {message}", ephemeral=True)
71
+		except discord.InteractionResponded:
72
+			await interaction.followup.send(f"An error occurred: {message}", ephemeral=True)
47
 
73
 
48
 	# Config
74
 	# Config
49
 
75
 
189
 		await message.update()
215
 		await message.update()
190
 		return message.is_sent()
216
 		return message.is_sent()
191
 
217
 
192
-	@commands.Cog.listener()
218
+	@Cog.listener()
193
 	async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
219
 	async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
194
 		"""Event handler"""
220
 		"""Event handler"""
195
 		# Avoid any unnecessary requests. Gets called for every reaction
221
 		# Avoid any unnecessary requests. Gets called for every reaction

+ 74
- 55
rocketbot/cogs/configcog.py Ver fichero

1
 """
1
 """
2
 Cog handling general configuration for a guild.
2
 Cog handling general configuration for a guild.
3
 """
3
 """
4
-from discord import Guild, TextChannel
5
-from discord.ext import commands
4
+from typing import Union, Optional
5
+
6
+from discord import Guild, Permissions, TextChannel, Interaction, Role, User
7
+from discord.app_commands import Group
6
 from discord.ext.commands import Bot
8
 from discord.ext.commands import Bot
7
 
9
 
8
 from config import CONFIG
10
 from config import CONFIG
15
 	"""
17
 	"""
16
 
18
 
17
 	def __init__(self, bot: Bot) -> None:
19
 	def __init__(self, bot: Bot) -> None:
18
-		super().__init__(bot, 'config')
20
+		super().__init__(
21
+			bot,
22
+			config_prefix='config',
23
+			name='configuration',
24
+			short_description='Manages general bot configuration.',
25
+		)
19
 
26
 
20
-	@commands.group(
21
-		brief='Manages general bot configuration'
27
+	config = Group(
28
+		name='config',
29
+		description='Manages general bot configuration',
30
+		guild_only=True,
31
+		default_permissions=Permissions(Permissions.manage_messages.flag)
22
 	)
32
 	)
23
-	@commands.has_permissions(ban_members=True)
24
-	@commands.guild_only()
25
-	async def config(self, context: commands.Context):
26
-		"""General guild configuration command group"""
27
-		if context.invoked_subcommand is None:
28
-			await context.send_help()
29
 
33
 
30
 	@config.command(
34
 	@config.command(
31
-		brief='Sets the mod warning channel',
32
-		description='Run this command in the channel where bot messages ' +
33
-			'intended for server moderators should be sent. Other bot ' +
34
-			'messages may still be posted in the channel a command was ' +
35
-			'invoked in. If no output channel is set, mod-related messages ' +
36
-			'will not be posted!',
35
+		description='Sets the mod warning channel',
36
+		extras={
37
+			'long_description': 'Run this command in the channel where bot messages ' +
38
+				'intended for server moderators should be sent. Other bot ' +
39
+				'messages may still be posted in the channel a command was ' +
40
+				'invoked in. If no output channel is set, mod-related messages ' +
41
+				'will not be posted!',
42
+		}
37
 	)
43
 	)
38
-	async def setwarningchannel(self, context: commands.Context) -> None:
44
+	async def setwarningchannel(self, interaction: Interaction) -> None:
39
 		"""Command handler"""
45
 		"""Command handler"""
40
-		guild: Guild = context.guild
41
-		channel: TextChannel = context.channel
46
+		guild: Guild = interaction.guild
47
+		channel: TextChannel = interaction.channel
42
 		Storage.set_config_value(guild, ConfigKey.WARNING_CHANNEL_ID,
48
 		Storage.set_config_value(guild, ConfigKey.WARNING_CHANNEL_ID,
43
-			context.channel.id)
44
-		await context.message.reply(
49
+			interaction.channel.id)
50
+		await interaction.response.send_message(
45
 			f'{CONFIG["success_emoji"]} Warning channel updated to {channel.mention}.',
51
 			f'{CONFIG["success_emoji"]} Warning channel updated to {channel.mention}.',
46
-			mention_author=False)
52
+			ephemeral=True,
53
+		)
47
 
54
 
48
 	@config.command(
55
 	@config.command(
49
-		brief='Shows the mod warning channel',
50
-		description='Shows the configured channel (if any) where mod ' + \
51
-			'warnings will be posted.',
56
+		description='Shows the mod warning channel',
57
+		extras={
58
+			'long_description': 'Shows the configured channel (if any) where mod ' + \
59
+				'warnings will be posted.',
60
+		},
52
 	)
61
 	)
53
-	async def getwarningchannel(self, context: commands.Context) -> None:
62
+	async def getwarningchannel(self, interaction: Interaction) -> None:
54
 		"""Command handler"""
63
 		"""Command handler"""
55
-		guild: Guild = context.guild
64
+		guild: Guild = interaction.guild
56
 		channel_id = Storage.get_config_value(guild, ConfigKey.WARNING_CHANNEL_ID)
65
 		channel_id = Storage.get_config_value(guild, ConfigKey.WARNING_CHANNEL_ID)
57
 		if channel_id is None:
66
 		if channel_id is None:
58
-			await context.message.reply(
67
+			await interaction.response.send_message(
59
 				f'{CONFIG["info_emoji"]} No warning channel is configured.',
68
 				f'{CONFIG["info_emoji"]} No warning channel is configured.',
60
-				mention_author=False)
69
+				ephemeral=True,
70
+			)
61
 		else:
71
 		else:
62
 			channel = guild.get_channel(channel_id)
72
 			channel = guild.get_channel(channel_id)
63
-			await context.message.reply(
73
+			await interaction.response.send_message(
64
 				f'{CONFIG["info_emoji"]} Warning channel is configured as {channel.mention}.',
74
 				f'{CONFIG["info_emoji"]} Warning channel is configured as {channel.mention}.',
65
-				mention_author=False)
75
+				ephemeral=True,
76
+			)
66
 
77
 
67
 	@config.command(
78
 	@config.command(
68
-		brief='Sets a user/role to mention in warning messages',
69
-		usage='<@user|@role>',
70
-		description='Configures an role or other prefix to include at the ' +
71
-			'beginning of warning messages. If the intent is to get the ' +
72
-			'attention of certain users, be sure to specify a properly ' +
73
-			'formed @ tag, not just the name of the user/role.'
79
+		description='Sets a user/role to mention in warning messages',
80
+		extras={
81
+			'usage': '<@user|@role>',
82
+			'long_description': 'Configures an role or other prefix to include at the ' +
83
+				'beginning of warning messages. If the intent is to get the ' +
84
+				'attention of certain users, be sure to specify a properly ' +
85
+				'formed @ tag, not just the name of the user/role.',
86
+		},
74
 	)
87
 	)
75
 	async def setwarningmention(self,
88
 	async def setwarningmention(self,
76
-			context: commands.Context,
77
-			mention: str = None) -> None:
89
+			interaction: Interaction,
90
+			mention: Optional[Union[User, Role]] = None) -> None:
78
 		"""Command handler"""
91
 		"""Command handler"""
79
-		guild: Guild = context.guild
80
-		Storage.set_config_value(guild, ConfigKey.WARNING_MENTION, mention)
92
+		guild: Guild = interaction.guild
93
+		Storage.set_config_value(guild, ConfigKey.WARNING_MENTION, mention.mention if mention else None)
81
 		if mention is None:
94
 		if mention is None:
82
-			await context.message.reply(
95
+			await interaction.response.send_message(
83
 				f'{CONFIG["success_emoji"]} Warning messages will not tag anyone.',
96
 				f'{CONFIG["success_emoji"]} Warning messages will not tag anyone.',
84
-				mention_author=False)
97
+				ephemeral=True,
98
+			)
85
 		else:
99
 		else:
86
-			await context.message.reply(
100
+			await interaction.response.send_message(
87
 				f'{CONFIG["success_emoji"]} Warning messages will now tag {mention}.',
101
 				f'{CONFIG["success_emoji"]} Warning messages will now tag {mention}.',
88
-				mention_author=False)
102
+				ephemeral=True,
103
+			)
89
 
104
 
90
 	@config.command(
105
 	@config.command(
91
-		brief='Shows the user/role to mention in warning messages',
92
-		description='Shows the text, if any, that will be prefixed on any ' +
93
-			'warning messages.'
106
+		description='Shows the user/role to mention in warning messages',
107
+		extras={
108
+			'long_description': 'Shows the text, if any, that will be prefixed on any ' +
109
+				'warning messages.',
110
+		},
94
 	)
111
 	)
95
-	async def getwarningmention(self, context: commands.Context) -> None:
112
+	async def getwarningmention(self, interaction: Interaction) -> None:
96
 		"""Command handler"""
113
 		"""Command handler"""
97
-		guild: Guild = context.guild
114
+		guild: Guild = interaction.guild
98
 		mention: str = Storage.get_config_value(guild, ConfigKey.WARNING_MENTION)
115
 		mention: str = Storage.get_config_value(guild, ConfigKey.WARNING_MENTION)
99
 		if mention is None:
116
 		if mention is None:
100
-			await context.message.reply(
117
+			await interaction.response.send_message(
101
 				f'{CONFIG["info_emoji"]} No warning mention configured.',
118
 				f'{CONFIG["info_emoji"]} No warning mention configured.',
102
-				mention_author=False)
119
+				ephemeral=True,
120
+			)
103
 		else:
121
 		else:
104
-			await context.message.reply(
122
+			await interaction.response.send_message(
105
 				f'{CONFIG["info_emoji"]} Warning messages will tag {mention}',
123
 				f'{CONFIG["info_emoji"]} Warning messages will tag {mention}',
106
-				mention_author=False)
124
+				ephemeral=True,
125
+			)

+ 9
- 14
rocketbot/cogs/crosspostcog.py Ver fichero

6
 from typing import Optional
6
 from typing import Optional
7
 
7
 
8
 from discord import Member, Message, utils as discordutils, TextChannel
8
 from discord import Member, Message, utils as discordutils, TextChannel
9
-from discord.ext import commands
9
+from discord.ext.commands import Cog
10
 
10
 
11
 from config import CONFIG
11
 from config import CONFIG
12
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
12
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
83
 			'messages like "lol" or a single emoji.',
83
 			'messages like "lol" or a single emoji.',
84
 		usage='<character_count:int>',
84
 		usage='<character_count:int>',
85
 		min_value=1)
85
 		min_value=1)
86
-	SETTING_TIMESPAN = CogSetting('timespan', float,
86
+	SETTING_TIMESPAN = CogSetting('timespan', timedelta,
87
 		brief='time window to look for dupe messages',
87
 		brief='time window to look for dupe messages',
88
 		description='The number of seconds of message history to look at ' + \
88
 		description='The number of seconds of message history to look at ' + \
89
 			'when looking for duplicates. Shorter values are preferred, ' + \
89
 			'when looking for duplicates. Shorter values are preferred, ' + \
95
 	STATE_KEY_SPAM_CONTEXT = "CrossPostCog.spam_context"
95
 	STATE_KEY_SPAM_CONTEXT = "CrossPostCog.spam_context"
96
 
96
 
97
 	def __init__(self, bot):
97
 	def __init__(self, bot):
98
-		super().__init__(bot, 'crosspost')
98
+		super().__init__(
99
+			bot,
100
+			config_prefix='crosspost',
101
+			name='crosspost detection',
102
+			short_description='Manages crosspost detection and handling.',
103
+		)
99
 		self.add_setting(CrossPostCog.SETTING_ENABLED)
104
 		self.add_setting(CrossPostCog.SETTING_ENABLED)
100
 		self.add_setting(CrossPostCog.SETTING_WARN_COUNT)
105
 		self.add_setting(CrossPostCog.SETTING_WARN_COUNT)
101
 		self.add_setting(CrossPostCog.SETTING_DUPE_WARN_COUNT)
106
 		self.add_setting(CrossPostCog.SETTING_DUPE_WARN_COUNT)
328
 		# print(message)
333
 		# print(message)
329
 		pass
334
 		pass
330
 
335
 
331
-	@commands.Cog.listener()
336
+	@Cog.listener()
332
 	async def on_message(self, message: Message):
337
 	async def on_message(self, message: Message):
333
 		"""Event handler"""
338
 		"""Event handler"""
334
 		if message.author is None or \
339
 		if message.author is None or \
340
 			return
345
 			return
341
 		self.__trace("--ON MESSAGE--")
346
 		self.__trace("--ON MESSAGE--")
342
 		await self.__record_message(message)
347
 		await self.__record_message(message)
343
-
344
-	@commands.group(
345
-		brief='Manages crosspost detection and handling',
346
-	)
347
-	@commands.has_permissions(ban_members=True)
348
-	@commands.guild_only()
349
-	async def crosspost(self, context: commands.Context):
350
-		"""Detects members posting messages in multiple channels in a short period of time."""
351
-		if context.invoked_subcommand is None:
352
-			await context.send_help()

+ 73
- 87
rocketbot/cogs/generalcog.py Ver fichero

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
5
 from typing import Optional
7
 
6
 
8
-from discord import Message
7
+from discord import Interaction, Message, User
8
+from discord.app_commands import command, default_permissions, guild_only, Transform
9
 from discord.errors import DiscordException
9
 from discord.errors import DiscordException
10
-from discord.ext import commands
10
+from discord.ext.commands import Cog
11
 
11
 
12
 from config import CONFIG
12
 from config import CONFIG
13
 from rocketbot.bot import Rocketbot
13
 from rocketbot.bot import Rocketbot
14
 from rocketbot.cogs.basecog import BaseCog, BotMessage
14
 from rocketbot.cogs.basecog import BaseCog, BotMessage
15
-from rocketbot.utils import timedelta_from_str, describe_timedelta, dump_stacktrace
15
+from rocketbot.utils import describe_timedelta, TimeDeltaTransformer
16
 from rocketbot.storage import ConfigKey, Storage
16
 from rocketbot.storage import ConfigKey, Storage
17
 
17
 
18
 class GeneralCog(BaseCog, name='General'):
18
 class GeneralCog(BaseCog, name='General'):
21
 	first cog added to the bot.
21
 	first cog added to the bot.
22
 	"""
22
 	"""
23
 	def __init__(self, bot: Rocketbot):
23
 	def __init__(self, bot: Rocketbot):
24
-		super().__init__(bot, None)
24
+		super().__init__(
25
+			bot,
26
+			config_prefix=None,
27
+			name='',
28
+			short_description='',
29
+		)
25
 		self.is_connected = False
30
 		self.is_connected = False
26
-		self.is_ready = False
27
-		self.is_first_ready = True
28
 		self.is_first_connect = True
31
 		self.is_first_connect = True
29
 		self.last_disconnect_time: Optional[datetime] = None
32
 		self.last_disconnect_time: Optional[datetime] = None
30
 		self.noteworthy_disconnect_duration = timedelta(seconds=5)
33
 		self.noteworthy_disconnect_duration = timedelta(seconds=5)
31
 
34
 
32
-	@commands.Cog.listener()
35
+	@Cog.listener()
33
 	async def on_connect(self):
36
 	async def on_connect(self):
34
 		"""Event handler"""
37
 		"""Event handler"""
35
 		if self.is_first_connect:
38
 		if self.is_first_connect:
42
 				self.log(None, f'Reconnected after {disconnect_duration.total_seconds()} seconds')
45
 				self.log(None, f'Reconnected after {disconnect_duration.total_seconds()} seconds')
43
 		self.is_connected = True
46
 		self.is_connected = True
44
 
47
 
45
-	@commands.Cog.listener()
48
+	@Cog.listener()
46
 	async def on_disconnect(self):
49
 	async def on_disconnect(self):
47
 		"""Event handler"""
50
 		"""Event handler"""
48
 		self.last_disconnect_time = datetime.now(timezone.utc)
51
 		self.last_disconnect_time = datetime.now(timezone.utc)
49
 		# self.log(None, 'Disconnected')
52
 		# self.log(None, 'Disconnected')
50
 
53
 
51
-	@commands.Cog.listener()
52
-	async def on_ready(self):
53
-		"""Event handler"""
54
-		self.log(None, 'Bot done initializing')
55
-		self.is_ready = True
56
-		if self.is_first_ready:
57
-			print('----------------------------------------------------------')
58
-			self.is_first_ready = False
59
-
60
-	@commands.Cog.listener()
54
+	@Cog.listener()
61
 	async def on_resumed(self):
55
 	async def on_resumed(self):
62
 		"""Event handler"""
56
 		"""Event handler"""
63
 		disconnect_duration = datetime.now(timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None
57
 		disconnect_duration = datetime.now(timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None
64
 		if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration:
58
 		if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration:
65
 			self.log(None, f'Session resumed after {disconnect_duration.total_seconds()} seconds')
59
 			self.log(None, f'Session resumed after {disconnect_duration.total_seconds()} seconds')
66
 
60
 
67
-	@commands.command(
68
-		brief='Posts a test warning',
69
-		description='Tests whether a warning channel is configured for this ' + \
70
-			'guild by posting a test warning. If a mod mention is ' + \
71
-			'configured, that user/role will be tagged in the test warning.',
61
+	@command(
62
+		description='Posts a test warning',
63
+		extras={
64
+			'long_description': 'Tests whether a warning channel is configured for this ' + \
65
+				'guild by posting a test warning. If a mod mention is ' + \
66
+				'configured, that user/role will be tagged in the test warning.',
67
+		},
72
 	)
68
 	)
73
-	@commands.has_permissions(ban_members=True)
74
-	@commands.guild_only()
75
-	async def testwarn(self, context):
69
+	@guild_only()
70
+	@default_permissions(manage_messages=True)
71
+	async def testwarn(self, interaction: Interaction):
76
 		"""Command handler"""
72
 		"""Command handler"""
77
-		if Storage.get_config_value(context.guild, ConfigKey.WARNING_CHANNEL_ID) is None:
78
-			await context.message.reply(
73
+		if Storage.get_config_value(interaction.guild, ConfigKey.WARNING_CHANNEL_ID) is None:
74
+			await interaction.response.send_message(
79
 				f'{CONFIG["warning_emoji"]} No warning channel set!',
75
 				f'{CONFIG["warning_emoji"]} No warning channel set!',
80
-				mention_author=False)
76
+				ephemeral=True,
77
+			)
81
 		else:
78
 		else:
82
 			bm = BotMessage(
79
 			bm = BotMessage(
83
-				context.guild,
84
-				f'Test warning message (requested by {context.author.name})',
80
+				interaction.guild,
81
+				f'Test warning message (requested by {interaction.user.name})',
85
 				type=BotMessage.TYPE_MOD_WARNING)
82
 				type=BotMessage.TYPE_MOD_WARNING)
86
 			await self.post_message(bm)
83
 			await self.post_message(bm)
84
+			await interaction.response.send_message(
85
+				'Warning issued',
86
+				ephemeral=True,
87
+			)
87
 
88
 
88
-	@commands.command(
89
-		brief='Simple test reply',
90
-		description='Replies to the command message. Useful to ensure the ' + \
91
-			'bot is working properly.',
89
+	@command(
90
+		description='Simple test reply',
91
+		extras={
92
+			'long_description': 'Replies to the command message. Useful to ensure the ' + \
93
+				'bot is working properly.',
94
+		},
92
 	)
95
 	)
93
-	async def hello(self, context):
96
+	async def hello(self, interaction: Interaction):
94
 		"""Command handler"""
97
 		"""Command handler"""
95
-		await context.message.reply(
96
-			f'Hey, {context.author.name}!',
97
-		 	mention_author=False)
98
+		await interaction.response.send_message(
99
+			f'Hey, {interaction.user.name}!',
100
+		 	ephemeral=True,
101
+		)
98
 
102
 
99
-	@commands.command(
100
-		brief='Shuts down the bot',
101
-		description='Causes the bot script to terminate. Only usable by a ' + \
102
-			'user with server admin permissions.',
103
+	@command(
104
+		description='Shuts down the bot',
105
+		extras={
106
+			'long_description': 'Causes the bot script to terminate. Only usable by a ' + \
107
+				'user with server admin permissions.',
108
+		},
103
 	)
109
 	)
104
-	@commands.has_permissions(administrator=True)
105
-	@commands.guild_only()
106
-	async def shutdown(self, context: commands.Context):
110
+	@guild_only()
111
+	@default_permissions(manage_messages=True)
112
+	async def shutdown(self, interaction: Interaction):
107
 		"""Command handler"""
113
 		"""Command handler"""
108
-		await context.message.add_reaction('👋')
114
+		await interaction.response.send_message('👋', ephemeral=True)
109
 		await self.bot.close()
115
 		await self.bot.close()
110
 
116
 
111
-	@commands.command(
112
-		brief='Mass deletes messages',
113
-		description='Deletes recent messages by the given user. The user ' +
114
-			'can be either an @ mention or a numeric user ID. The age is ' +
115
-			'a duration, such as "30s", "5m", "1h30m". Only the most ' +
116
-			'recent 100 messages in each channel are searched.',
117
-		usage='<user:id|mention> <age:timespan>'
117
+	@command(
118
+		description='Mass deletes messages',
119
+		extras={
120
+			'long_description': 'Deletes recent messages by the given user. The user ' +
121
+				'can be either an @ mention or a numeric user ID. The age is ' +
122
+				'a duration, such as "30s", "5m", "1h30m". Only the most ' +
123
+				'recent 100 messages in each channel are searched.',
124
+			'usage': '<user:id|mention> <age:timespan>',
125
+		},
118
 	)
126
 	)
119
-	@commands.has_permissions(manage_messages=True)
120
-	@commands.guild_only()
121
-	async def deletemessages(self, context, user: str, age: str) -> None:
127
+	@guild_only()
128
+	@default_permissions(manage_messages=True)
129
+	async def deletemessages(self, interaction: Interaction, user: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None:
122
 		"""Command handler"""
130
 		"""Command handler"""
123
-		member_id = self.__parse_member_id(user)
124
-		if member_id is None:
125
-			await context.message.reply(
126
-				f'{CONFIG["failure_emoji"]} user must be a mention or numeric user id',
127
-				mention_author=False)
128
-			return
129
-		try:
130
-			age_delta: timedelta = timedelta_from_str(age)
131
-		except ValueError:
132
-			await context.message.reply(
133
-				f'{CONFIG["failure_emoji"]} age must be a timespan, like "30s", "10m", "1h30m"',
134
-				mention_author=False)
135
-			return
136
-		cutoff: datetime = datetime.now(timezone.utc) - age_delta
131
+		member_id = user.id
132
+		cutoff: datetime = datetime.now(timezone.utc) - age
137
 		def predicate(message: Message) -> bool:
133
 		def predicate(message: Message) -> bool:
138
 			return str(message.author.id) == member_id and message.created_at >= cutoff
134
 			return str(message.author.id) == member_id and message.created_at >= cutoff
139
 		deleted_messages = []
135
 		deleted_messages = []
140
-		for channel in context.guild.text_channels:
136
+		for channel in interaction.guild.text_channels:
141
 			try:
137
 			try:
142
 				deleted_messages += await channel.purge(limit=100, check=predicate)
138
 				deleted_messages += await channel.purge(limit=100, check=predicate)
143
 			except DiscordException:
139
 			except DiscordException:
144
 				# XXX: Sloppily glossing over access errors instead of checking access
140
 				# XXX: Sloppily glossing over access errors instead of checking access
145
 				pass
141
 				pass
146
-		await context.message.reply(
142
+		await interaction.response.send_message(
147
 			f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} ' + \
143
 			f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} ' + \
148
-			f'messages by <@!{member_id}> from the past {describe_timedelta(age_delta)}.',
149
-			mention_author=False)
150
-
151
-	def __parse_member_id(self, arg: str) -> Optional[str]:
152
-		p = re.compile('^<@!?([0-9]+)>$')
153
-		m = p.match(arg)
154
-		if m:
155
-			return m.group(1)
156
-		p = re.compile('^([0-9]+)$')
157
-		m = p.match(arg)
158
-		if m:
159
-			return m.group(1)
160
-		return None
144
+			f'messages by {user.mention}> from the past {describe_timedelta(age)}.',
145
+			ephemeral=True,
146
+		)

+ 39
- 27
rocketbot/cogs/joinagecog.py Ver fichero

1
 import weakref
1
 import weakref
2
 
2
 
3
 from datetime import datetime, timedelta, timezone
3
 from datetime import datetime, timedelta, timezone
4
-from discord import Guild, Member
5
-from discord.ext import commands
4
+from typing import Optional
5
+
6
+from discord import Guild, Member, Interaction
7
+from discord.app_commands import Group, guild_only, default_permissions, Transform
8
+from discord.ext.commands import Cog
6
 
9
 
7
 from config import CONFIG
10
 from config import CONFIG
8
 from rocketbot.bot import Rocketbot
11
 from rocketbot.bot import Rocketbot
9
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
12
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
10
 from rocketbot.collections import AgeBoundList
13
 from rocketbot.collections import AgeBoundList
11
 from rocketbot.storage import Storage
14
 from rocketbot.storage import Storage
12
-from rocketbot.utils import timedelta_from_str
15
+from rocketbot.utils import TimeDeltaTransformer
16
+
13
 
17
 
14
 class JoinAgeQueryContext:
18
 class JoinAgeQueryContext:
15
 	"""
19
 	"""
16
 	Data about a join age query
20
 	Data about a join age query
17
 	"""
21
 	"""
18
-	def __init__(self, join_members: list, timespan: str):
22
+	def __init__(self, join_members: list[Member], timespan: timedelta):
19
 		self.join_members = list(join_members)
23
 		self.join_members = list(join_members)
20
-		self.timespan = timespan
21
-		self.kicked_members = set()
22
-		self.banned_members = set()
23
-		self.results_message_ref = None
24
+		self.timespan: timedelta = timespan
25
+		self.kicked_members: set[Member] = set()
26
+		self.banned_members: set[Member] = set()
27
+		self.results_message_ref: Optional[weakref.ReferenceType[BotMessage]] = None
24
 
28
 
25
 class JoinAgeCog(BaseCog, name='Join Age'):
29
 class JoinAgeCog(BaseCog, name='Join Age'):
26
 	"""
30
 	"""
29
 	SETTING_ENABLED = CogSetting('enabled', bool,
33
 	SETTING_ENABLED = CogSetting('enabled', bool,
30
 		brief='join age',
34
 		brief='join age',
31
 		description='Whether this cog is enabled for a guild.')
35
 		description='Whether this cog is enabled for a guild.')
32
-	SETTING_JOIN_TIME = CogSetting('jointime', float,
36
+	SETTING_JOIN_TIME = CogSetting('jointime', timedelta,
33
 		brief='maximum length of time to track new joins',
37
 		brief='maximum length of time to track new joins',
34
 		description='The number of seconds of join history to maintain.',
38
 		description='The number of seconds of join history to maintain.',
35
 		usage='<seconds:float>',
39
 		usage='<seconds:float>',
38
 	STATE_KEY_RECENT_JOINS = "JoinAgeCog.recent_joins"
42
 	STATE_KEY_RECENT_JOINS = "JoinAgeCog.recent_joins"
39
 
43
 
40
 	def __init__(self, bot: Rocketbot):
44
 	def __init__(self, bot: Rocketbot):
41
-		super().__init__(bot, 'joinage')
45
+		super().__init__(
46
+			bot,
47
+			config_prefix='joinage',
48
+			name='join age',
49
+			short_description='Tracks recently joined users with options to mass kick or ban.',
50
+		)
42
 		self.add_setting(JoinAgeCog.SETTING_ENABLED)
51
 		self.add_setting(JoinAgeCog.SETTING_ENABLED)
43
 		self.add_setting(JoinAgeCog.SETTING_JOIN_TIME)
52
 		self.add_setting(JoinAgeCog.SETTING_JOIN_TIME)
44
 
53
 
45
-	@commands.group(
46
-		brief='Tracks recently joined users with options to mass kick or ban',
54
+	joinage = Group(
55
+		name='joinage',
56
+		description='Queries for users who joined in the past span of time',
57
+		extras={
58
+			'long_description': 'Searches for users who joined the server recently. ' + \
59
+				'Can use time spans like 30s, 5m, 1h, 7d, etc.',
60
+			'usage': '<time_period>',
61
+		}
47
 	)
62
 	)
48
-	@commands.has_permissions(ban_members=True)
49
-	@commands.guild_only()
50
-	async def joinage(self, context: commands.Context):
51
-		"""Join age tracking"""
52
-		if context.invoked_subcommand is None:
53
-			await context.send_help()
54
 
63
 
55
 	@joinage.command(
64
 	@joinage.command(
56
-		brief='Queries for users who joined in the past span of time',
57
-		description='Searches for users who joined the server recently. ' + \
58
-			'Can use time spans like 30s, 5m, 1h, 7d, etc.',
59
-		usage='<time_period>'
65
+		name='search',
66
+		description='Searches for users who joined recently',
60
 	)
67
 	)
61
-	async def search(self, context: commands.Context, timespan: str):
68
+	@guild_only()
69
+	@default_permissions(ban_members=True)
70
+	async def search(self, interaction: Interaction, timespan: Transform[timedelta, TimeDeltaTransformer]) -> None:
62
 		"""Command handler"""
71
 		"""Command handler"""
63
-		guild: Guild = context.guild
72
+		guild: Guild = interaction.guild
64
 		recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
73
 		recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
65
 		if recent_joins is None:
74
 		if recent_joins is None:
66
 			max_age: timedelta = timedelta(seconds=self.get_guild_setting(guild, self.SETTING_JOIN_TIME))
75
 			max_age: timedelta = timedelta(seconds=self.get_guild_setting(guild, self.SETTING_JOIN_TIME))
67
 			recent_joins = AgeBoundList(max_age, lambda i, member0 : member0.joined_at)
76
 			recent_joins = AgeBoundList(max_age, lambda i, member0 : member0.joined_at)
68
 			Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
77
 			Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
69
 		results: list = []
78
 		results: list = []
70
-		ts: timedelta = timedelta_from_str(timespan)
71
-		cutoff: datetime = datetime.now(timezone.utc) - ts
79
+		cutoff: datetime = datetime.now(timezone.utc) - timespan
72
 		for member in recent_joins:
80
 		for member in recent_joins:
73
 			if member.joined_at > cutoff:
81
 			if member.joined_at > cutoff:
74
 				results.append(member)
82
 				results.append(member)
80
 		ctx.results_message_ref = weakref.ref(msg)
88
 		ctx.results_message_ref = weakref.ref(msg)
81
 		await self.__update_results_message(ctx)
89
 		await self.__update_results_message(ctx)
82
 		await self.post_message(msg)
90
 		await self.post_message(msg)
91
+		await interaction.response.send_message(
92
+			"Search started",
93
+			ephemeral=True,
94
+		)
83
 
95
 
84
 	async def on_mod_react(self,
96
 	async def on_mod_react(self,
85
 			bot_message: BotMessage,
97
 			bot_message: BotMessage,
105
 			await self.__update_results_message(ctx)
117
 			await self.__update_results_message(ctx)
106
 			self.log(guild, f'Users banned by {reacted_by.name}')
118
 			self.log(guild, f'Users banned by {reacted_by.name}')
107
 
119
 
108
-	@commands.Cog.listener()
120
+	@Cog.listener()
109
 	async def on_member_join(self, member: Member) -> None:
121
 	async def on_member_join(self, member: Member) -> None:
110
 		"""Event handler"""
122
 		"""Event handler"""
111
 		guild: Guild = member.guild
123
 		guild: Guild = member.guild

+ 9
- 14
rocketbot/cogs/joinraidcog.py Ver fichero

4
 import weakref
4
 import weakref
5
 from datetime import datetime, timedelta
5
 from datetime import datetime, timedelta
6
 from discord import Guild, Member
6
 from discord import Guild, Member
7
-from discord.ext import commands
7
+from discord.ext.commands import Cog
8
 
8
 
9
 from config import CONFIG
9
 from config import CONFIG
10
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
10
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
38
 				'window to trigger a mod warning.',
38
 				'window to trigger a mod warning.',
39
 			usage='<count:int>',
39
 			usage='<count:int>',
40
 			min_value=2)
40
 			min_value=2)
41
-	SETTING_JOIN_TIME = CogSetting('jointime', float,
41
+	SETTING_JOIN_TIME = CogSetting('jointime', timedelta,
42
 			brief='time window length to look for joins',
42
 			brief='time window length to look for joins',
43
 			description='The number of seconds of join history to look ' + \
43
 			description='The number of seconds of join history to look ' + \
44
 				'at when counting recent joins. If joincount or more ' + \
44
 				'at when counting recent joins. If joincount or more ' + \
51
 	STATE_KEY_LAST_RAID = "JoinRaidCog.last_raid"
51
 	STATE_KEY_LAST_RAID = "JoinRaidCog.last_raid"
52
 
52
 
53
 	def __init__(self, bot):
53
 	def __init__(self, bot):
54
-		super().__init__(bot, 'joinraid')
54
+		super().__init__(
55
+			bot,
56
+			config_prefix='joinraid',
57
+			name='join raid',
58
+			short_description='Manages join raid detection and handling.',
59
+		)
55
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)
60
 		self.add_setting(JoinRaidCog.SETTING_ENABLED)
56
 		self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)
61
 		self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)
57
 		self.add_setting(JoinRaidCog.SETTING_JOIN_TIME)
62
 		self.add_setting(JoinRaidCog.SETTING_JOIN_TIME)
58
 
63
 
59
-	@commands.group(
60
-		brief='Manages join raid detection and handling',
61
-	)
62
-	@commands.has_permissions(ban_members=True)
63
-	@commands.guild_only()
64
-	async def joinraid(self, context: commands.Context):
65
-		"""Join raid detection command group"""
66
-		if context.invoked_subcommand is None:
67
-			await context.send_help()
68
-
69
 	async def on_mod_react(self,
64
 	async def on_mod_react(self,
70
 			bot_message: BotMessage,
65
 			bot_message: BotMessage,
71
 			reaction: BotMessageReaction,
66
 			reaction: BotMessageReaction,
90
 			await self.__update_warning_message(raid)
85
 			await self.__update_warning_message(raid)
91
 			self.log(guild, f'Join raid users banned by {reacted_by.name}')
86
 			self.log(guild, f'Join raid users banned by {reacted_by.name}')
92
 
87
 
93
-	@commands.Cog.listener()
88
+	@Cog.listener()
94
 	async def on_member_join(self, member: Member) -> None:
89
 	async def on_member_join(self, member: Member) -> None:
95
 		"""Event handler"""
90
 		"""Event handler"""
96
 		guild: Guild = member.guild
91
 		guild: Guild = member.guild

+ 36
- 40
rocketbot/cogs/logcog.py Ver fichero

2
 Cog for detecting large numbers of guild joins in a short period of time.
2
 Cog for detecting large numbers of guild joins in a short period of time.
3
 """
3
 """
4
 from collections.abc import Sequence
4
 from collections.abc import Sequence
5
-from datetime import datetime, timezone, timedelta
5
+from datetime import datetime, timedelta, timezone
6
 
6
 
7
 from discord import AuditLogAction, AuditLogEntry, Emoji, Guild, GuildSticker, Invite, Member, Message, RawBulkMessageDeleteEvent, RawMessageDeleteEvent, RawMessageUpdateEvent, Role, Thread, User
7
 from discord import AuditLogAction, AuditLogEntry, Emoji, Guild, GuildSticker, Invite, Member, Message, RawBulkMessageDeleteEvent, RawMessageDeleteEvent, RawMessageUpdateEvent, Role, Thread, User
8
 from discord.abc import GuildChannel
8
 from discord.abc import GuildChannel
9
-from discord.ext import commands, tasks
9
+from discord.ext import tasks
10
+from discord.ext.commands import Cog
10
 from discord.utils import escape_markdown
11
 from discord.utils import escape_markdown
11
 from typing import Optional, Tuple, Union, Callable
12
 from typing import Optional, Tuple, Union, Callable
12
 import difflib
13
 import difflib
44
 	STATE_EVENT_BUFFER = 'LoggingCog.eventBuffer'
45
 	STATE_EVENT_BUFFER = 'LoggingCog.eventBuffer'
45
 
46
 
46
 	def __init__(self, bot):
47
 	def __init__(self, bot):
47
-		super().__init__(bot, 'logging')
48
+		super().__init__(
49
+			bot,
50
+			config_prefix='logging',
51
+			name='logging',
52
+			short_description='Manages event logging',
53
+		)
48
 		self.add_setting(LoggingCog.SETTING_ENABLED)
54
 		self.add_setting(LoggingCog.SETTING_ENABLED)
49
 		self.flush_buffers.start()
55
 		self.flush_buffers.start()
50
 		self.buffered_guilds: set[Guild] = set()
56
 		self.buffered_guilds: set[Guild] = set()
52
 	def cog_unload(self) -> None:
58
 	def cog_unload(self) -> None:
53
 		self.flush_buffers.cancel()
59
 		self.flush_buffers.cancel()
54
 
60
 
55
-	@commands.group(
56
-		brief='Manages event logging',
57
-	)
58
-	@commands.has_permissions(ban_members=True)
59
-	@commands.guild_only()
60
-	async def logging(self, context: commands.Context):
61
-		"""Logging command group"""
62
-		if context.invoked_subcommand is None:
63
-			await context.send_help()
64
-
65
 	# Events - Channels
61
 	# Events - Channels
66
 
62
 
67
-	@commands.Cog.listener()
63
+	@Cog.listener()
68
 	async def on_guild_channel_delete(self, channel: GuildChannel) -> None:
64
 	async def on_guild_channel_delete(self, channel: GuildChannel) -> None:
69
 		"""
65
 		"""
70
 		Called whenever a guild channel is deleted or created.
66
 		Called whenever a guild channel is deleted or created.
80
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
76
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
81
 		await bot_message.update()
77
 		await bot_message.update()
82
 
78
 
83
-	@commands.Cog.listener()
79
+	@Cog.listener()
84
 	async def on_guild_channel_create(self, channel: GuildChannel) -> None:
80
 	async def on_guild_channel_create(self, channel: GuildChannel) -> None:
85
 		"""
81
 		"""
86
 		Called whenever a guild channel is deleted or created.
82
 		Called whenever a guild channel is deleted or created.
96
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
92
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
97
 		await bot_message.update()
93
 		await bot_message.update()
98
 
94
 
99
-	@commands.Cog.listener()
95
+	@Cog.listener()
100
 	async def on_guild_channel_update(self, before: GuildChannel, after: GuildChannel) -> None:
96
 	async def on_guild_channel_update(self, before: GuildChannel, after: GuildChannel) -> None:
101
 		"""
97
 		"""
102
 		Called whenever a guild channel is updated. e.g. changed name, topic,
98
 		Called whenever a guild channel is updated. e.g. changed name, topic,
129
 
125
 
130
 	# Events - Guilds
126
 	# Events - Guilds
131
 
127
 
132
-	@commands.Cog.listener()
128
+	@Cog.listener()
133
 	async def on_guild_available(self, guild: Guild) -> None:
129
 	async def on_guild_available(self, guild: Guild) -> None:
134
 		pass
130
 		pass
135
 
131
 
136
-	@commands.Cog.listener()
132
+	@Cog.listener()
137
 	async def on_guild_unavailable(self, guild: Guild) -> None:
133
 	async def on_guild_unavailable(self, guild: Guild) -> None:
138
 		pass
134
 		pass
139
 
135
 
140
-	@commands.Cog.listener()
136
+	@Cog.listener()
141
 	async def on_guild_update(self, before: Guild, after: Guild) -> None:
137
 	async def on_guild_update(self, before: Guild, after: Guild) -> None:
142
 		pass
138
 		pass
143
 
139
 
144
-	@commands.Cog.listener()
140
+	@Cog.listener()
145
 	async def on_guild_emojis_update(self, guild: Guild, before: Sequence[Emoji], after: Sequence[Emoji]) -> None:
141
 	async def on_guild_emojis_update(self, guild: Guild, before: Sequence[Emoji], after: Sequence[Emoji]) -> None:
146
 		pass
142
 		pass
147
 
143
 
148
-	@commands.Cog.listener()
144
+	@Cog.listener()
149
 	async def on_guild_stickers_update(self, guild: Guild, before: Sequence[GuildSticker], after: Sequence[GuildSticker]) -> None:
145
 	async def on_guild_stickers_update(self, guild: Guild, before: Sequence[GuildSticker], after: Sequence[GuildSticker]) -> None:
150
 		pass
146
 		pass
151
 
147
 
152
-	@commands.Cog.listener()
148
+	@Cog.listener()
153
 	async def on_invite_create(self, invite: Invite) -> None:
149
 	async def on_invite_create(self, invite: Invite) -> None:
154
 		"""
150
 		"""
155
 		Called when an `Invite` is created. You must have manage_channels to receive this.
151
 		Called when an `Invite` is created. You must have manage_channels to receive this.
167
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
163
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
168
 		await bot_message.update()
164
 		await bot_message.update()
169
 
165
 
170
-	@commands.Cog.listener()
166
+	@Cog.listener()
171
 	async def on_invite_delete(self, invite: Invite) -> None:
167
 	async def on_invite_delete(self, invite: Invite) -> None:
172
 		"""
168
 		"""
173
 		Called when an `Invite` is deleted. You must have manage_channels to receive this.
169
 		Called when an `Invite` is deleted. You must have manage_channels to receive this.
186
 
182
 
187
 	# Events - Members
183
 	# Events - Members
188
 
184
 
189
-	@commands.Cog.listener()
185
+	@Cog.listener()
190
 	async def on_member_join(self, member: Member) -> None:
186
 	async def on_member_join(self, member: Member) -> None:
191
 		"""
187
 		"""
192
 		Called when a Member joins a Guild.
188
 		Called when a Member joins a Guild.
250
 			bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
246
 			bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
251
 		await bot_message.update()
247
 		await bot_message.update()
252
 
248
 
253
-	@commands.Cog.listener()
249
+	@Cog.listener()
254
 	async def on_member_remove(self, member: Member) -> None:
250
 	async def on_member_remove(self, member: Member) -> None:
255
 		"""
251
 		"""
256
 		Called when a Member leaves a Guild.
252
 		Called when a Member leaves a Guild.
284
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
280
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
285
 		await bot_message.update()
281
 		await bot_message.update()
286
 
282
 
287
-	@commands.Cog.listener()
283
+	@Cog.listener()
288
 	async def on_member_update(self, before: Member, after: Member) -> None:
284
 	async def on_member_update(self, before: Member, after: Member) -> None:
289
 		"""
285
 		"""
290
 		Called when a Member updates their profile.
286
 		Called when a Member updates their profile.
346
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
342
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
347
 		await bot_message.update()
343
 		await bot_message.update()
348
 
344
 
349
-	@commands.Cog.listener()
345
+	@Cog.listener()
350
 	async def on_user_update(self, before: User, after: User) -> None:
346
 	async def on_user_update(self, before: User, after: User) -> None:
351
 		"""
347
 		"""
352
 		Called when a User updates their profile.
348
 		Called when a User updates their profile.
378
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
374
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
379
 		await bot_message.update()
375
 		await bot_message.update()
380
 
376
 
381
-	@commands.Cog.listener()
377
+	@Cog.listener()
382
 	async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None:
378
 	async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None:
383
 		"""
379
 		"""
384
 		Called when user gets banned from a Guild.
380
 		Called when user gets banned from a Guild.
419
 				return entry
415
 				return entry
420
 		return None
416
 		return None
421
 
417
 
422
-	@commands.Cog.listener()
418
+	@Cog.listener()
423
 	async def on_member_unban(self, guild: Guild, user: Union[User, Member]) -> None:
419
 	async def on_member_unban(self, guild: Guild, user: Union[User, Member]) -> None:
424
 		"""
420
 		"""
425
 		Called when a User gets unbanned from a Guild.
421
 		Called when a User gets unbanned from a Guild.
472
 	async def before_flush_buffers_start(self) -> None:
468
 	async def before_flush_buffers_start(self) -> None:
473
 		await self.bot.wait_until_ready()
469
 		await self.bot.wait_until_ready()
474
 
470
 
475
-	@commands.Cog.listener()
471
+	@Cog.listener()
476
 	async def on_message(self, message: Message) -> None:
472
 	async def on_message(self, message: Message) -> None:
477
 		"""
473
 		"""
478
 		Called when a Message is created and sent.
474
 		Called when a Message is created and sent.
481
 		"""
477
 		"""
482
 		# print(f"Saw message {message.id} \"{message.content}\"")
478
 		# print(f"Saw message {message.id} \"{message.content}\"")
483
 
479
 
484
-	@commands.Cog.listener()
480
+	@Cog.listener()
485
 	async def on_message_edit(self, before: Message, after: Message) -> None:
481
 	async def on_message_edit(self, before: Message, after: Message) -> None:
486
 		"""
482
 		"""
487
 		Called when a Message receives an update event. If the message is not
483
 		Called when a Message receives an update event. If the message is not
512
 
508
 
513
 		self.__buffer_event(guild, 'edit', BufferedMessageEditEvent(guild, channel, before, after))
509
 		self.__buffer_event(guild, 'edit', BufferedMessageEditEvent(guild, channel, before, after))
514
 
510
 
515
-	@commands.Cog.listener()
511
+	@Cog.listener()
516
 	async def on_raw_message_edit(self, payload: RawMessageUpdateEvent) -> None:
512
 	async def on_raw_message_edit(self, payload: RawMessageUpdateEvent) -> None:
517
 		"""
513
 		"""
518
 		Called when a message is edited. Unlike on_message_edit(), this is called
514
 		Called when a message is edited. Unlike on_message_edit(), this is called
632
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
628
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
633
 		await bot_message.update()
629
 		await bot_message.update()
634
 
630
 
635
-	@commands.Cog.listener()
631
+	@Cog.listener()
636
 	async def on_raw_message_delete(self, payload: RawMessageDeleteEvent) -> None:
632
 	async def on_raw_message_delete(self, payload: RawMessageDeleteEvent) -> None:
637
 		"""
633
 		"""
638
 		Called when a message is deleted. Unlike on_message_delete(), this is
634
 		Called when a message is deleted. Unlike on_message_delete(), this is
660
 			return
656
 			return
661
 		self.__buffer_event(guild, 'delete', BufferedMessageDeleteEvent(guild, channel, payload.message_id, message))
657
 		self.__buffer_event(guild, 'delete', BufferedMessageDeleteEvent(guild, channel, payload.message_id, message))
662
 
658
 
663
-	@commands.Cog.listener()
659
+	@Cog.listener()
664
 	async def on_raw_bulk_message_delete(self, payload: RawBulkMessageDeleteEvent) -> None:
660
 	async def on_raw_bulk_message_delete(self, payload: RawBulkMessageDeleteEvent) -> None:
665
 		"""
661
 		"""
666
 		Called when a bulk delete is triggered. Unlike on_bulk_message_delete(),
662
 		Called when a bulk delete is triggered. Unlike on_bulk_message_delete(),
740
 
736
 
741
 	# Events - Roles
737
 	# Events - Roles
742
 
738
 
743
-	@commands.Cog.listener()
739
+	@Cog.listener()
744
 	async def on_guild_role_create(self, role: Role) -> None:
740
 	async def on_guild_role_create(self, role: Role) -> None:
745
 		"""
741
 		"""
746
 		Called when a Guild creates or deletes a new Role.
742
 		Called when a Guild creates or deletes a new Role.
756
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
752
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
757
 		await bot_message.update()
753
 		await bot_message.update()
758
 
754
 
759
-	@commands.Cog.listener()
755
+	@Cog.listener()
760
 	async def on_guild_role_delete(self, role: Role) -> None:
756
 	async def on_guild_role_delete(self, role: Role) -> None:
761
 		"""
757
 		"""
762
 		Called when a Guild creates or deletes a new Role.
758
 		Called when a Guild creates or deletes a new Role.
772
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
768
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
773
 		await bot_message.update()
769
 		await bot_message.update()
774
 
770
 
775
-	@commands.Cog.listener()
771
+	@Cog.listener()
776
 	async def on_guild_role_update(self, before: Role, after: Role) -> None:
772
 	async def on_guild_role_update(self, before: Role, after: Role) -> None:
777
 		"""
773
 		"""
778
 		Called when a Role is changed guild-wide.
774
 		Called when a Role is changed guild-wide.
815
 
811
 
816
 	# Events - Threads
812
 	# Events - Threads
817
 
813
 
818
-	@commands.Cog.listener()
814
+	@Cog.listener()
819
 	async def on_thread_create(self, thread: Thread) -> None:
815
 	async def on_thread_create(self, thread: Thread) -> None:
820
 		pass
816
 		pass
821
 
817
 
822
-	@commands.Cog.listener()
818
+	@Cog.listener()
823
 	async def on_thread_update(self, before: Thread, after: Thread) -> None:
819
 	async def on_thread_update(self, before: Thread, after: Thread) -> None:
824
 		pass
820
 		pass
825
 
821
 
826
-	@commands.Cog.listener()
822
+	@Cog.listener()
827
 	async def on_thread_delete(self, thread: Thread) -> None:
823
 	async def on_thread_delete(self, thread: Thread) -> None:
828
 		pass
824
 		pass
829
 
825
 

+ 6
- 1
rocketbot/cogs/patterncog.py Ver fichero

37
 	SETTING_PATTERNS = CogSetting('patterns', None)
37
 	SETTING_PATTERNS = CogSetting('patterns', None)
38
 
38
 
39
 	def __init__(self, bot: Rocketbot):
39
 	def __init__(self, bot: Rocketbot):
40
-		super().__init__(bot, 'patterns')
40
+		super().__init__(
41
+			bot,
42
+			config_prefix='patterns',
43
+			name='patterns',
44
+			short_description='Manages message pattern matching.',
45
+		)
41
 
46
 
42
 	def __get_patterns(self, guild: Guild) -> dict[str, PatternStatement]:
47
 	def __get_patterns(self, guild: Guild) -> dict[str, PatternStatement]:
43
 		"""
48
 		"""

+ 11
- 16
rocketbot/cogs/urlspamcog.py Ver fichero

6
 from typing import Literal
6
 from typing import Literal
7
 
7
 
8
 from discord import Member, Message, utils as discordutils
8
 from discord import Member, Message, utils as discordutils
9
-from discord.ext import commands
9
+from discord.ext.commands import Cog
10
 from discord.utils import escape_markdown
10
 from discord.utils import escape_markdown
11
 
11
 
12
 from config import CONFIG
12
 from config import CONFIG
36
 			brief='action to take on spam',
36
 			brief='action to take on spam',
37
 			description='The action to take on detected URL spam.',
37
 			description='The action to take on detected URL spam.',
38
 			enum_values={'nothing', 'modwarn', 'delete', 'kick', 'ban'})
38
 			enum_values={'nothing', 'modwarn', 'delete', 'kick', 'ban'})
39
-	SETTING_JOIN_AGE = CogSetting('joinage', float,
39
+	SETTING_JOIN_AGE = CogSetting('joinage', timedelta,
40
 			brief='seconds since member joined',
40
 			brief='seconds since member joined',
41
 			description='The minimum seconds since the user joined the ' + \
41
 			description='The minimum seconds since the user joined the ' + \
42
 				'server before they can post URLs. URLs posted by users ' + \
42
 				'server before they can post URLs. URLs posted by users ' + \
54
 				'chatwarn', 'chatwarndelete', 'delete', 'kick', 'ban'})
54
 				'chatwarn', 'chatwarndelete', 'delete', 'kick', 'ban'})
55
 
55
 
56
 	def __init__(self, bot):
56
 	def __init__(self, bot):
57
-		super().__init__(bot, 'urlspam')
57
+		super().__init__(
58
+			bot,
59
+			config_prefix='urlspam',
60
+			name='URL spam',
61
+			short_description='Manages URL spam detection.',
62
+		)
58
 		self.add_setting(URLSpamCog.SETTING_ENABLED)
63
 		self.add_setting(URLSpamCog.SETTING_ENABLED)
59
 		self.add_setting(URLSpamCog.SETTING_ACTION)
64
 		self.add_setting(URLSpamCog.SETTING_ACTION)
60
 		self.add_setting(URLSpamCog.SETTING_JOIN_AGE)
65
 		self.add_setting(URLSpamCog.SETTING_JOIN_AGE)
61
 		self.add_setting(URLSpamCog.SETTING_DECEPTIVE_ACTION)
66
 		self.add_setting(URLSpamCog.SETTING_DECEPTIVE_ACTION)
62
 
67
 
63
-	@commands.group(
64
-		brief='Manages URL spam detection',
65
-	)
66
-	@commands.has_permissions(ban_members=True)
67
-	@commands.guild_only()
68
-	async def urlspam(self, context: commands.Context):
69
-		"""URL spam command group"""
70
-		if context.invoked_subcommand is None:
71
-			await context.send_help()
72
-
73
-	@commands.Cog.listener()
68
+	@Cog.listener()
74
 	async def on_message(self, message: Message):
69
 	async def on_message(self, message: Message):
75
 		"""Event listener"""
70
 		"""Event listener"""
76
 		if message.author is None or \
71
 		if message.author is None or \
228
 					return True
223
 					return True
229
 		return False
224
 		return False
230
 
225
 
231
-	def is_url(self, s: str):
226
+	def is_url(self, s: str) -> bool:
232
 		"""Tests if a string is strictly a URL"""
227
 		"""Tests if a string is strictly a URL"""
233
 		ipv6_host_pattern = r'\[[0-9a-fA-F:]+\]'
228
 		ipv6_host_pattern = r'\[[0-9a-fA-F:]+\]'
234
 		ipv4_host_pattern = r'[0-9\.]+'
229
 		ipv4_host_pattern = r'[0-9\.]+'
239
 		pattern = r'^http[s]?://' + host_pattern + port_pattern + path_pattern + '$'
234
 		pattern = r'^http[s]?://' + host_pattern + port_pattern + path_pattern + '$'
240
 		return re.match(pattern, s, re.IGNORECASE) is not None
235
 		return re.match(pattern, s, re.IGNORECASE) is not None
241
 
236
 
242
-	def is_casual_url(self, s: str):
237
+	def is_casual_url(self, s: str) -> bool:
243
 		"""Tests if a string is a "casual URL" with no scheme included"""
238
 		"""Tests if a string is a "casual URL" with no scheme included"""
244
 		ipv6_host_pattern = r'\[[0-9a-fA-F:]+\]'
239
 		ipv6_host_pattern = r'\[[0-9a-fA-F:]+\]'
245
 		ipv4_host_pattern = r'[0-9\.]+'
240
 		ipv4_host_pattern = r'[0-9\.]+'

+ 57
- 32
rocketbot/cogs/usernamecog.py Ver fichero

3
 """
3
 """
4
 from typing import Optional
4
 from typing import Optional
5
 
5
 
6
-from discord import Guild, Member
7
-from discord.ext import commands
6
+from discord import Guild, Member, Permissions, Interaction
7
+from discord.app_commands import Group
8
+from discord.ext.commands import Cog
8
 
9
 
9
 from config import CONFIG
10
 from config import CONFIG
10
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
11
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
51
 	SETTING_PATTERNS = CogSetting('patterns', None)
52
 	SETTING_PATTERNS = CogSetting('patterns', None)
52
 
53
 
53
 	def __init__(self, bot):
54
 	def __init__(self, bot):
54
-		super().__init__(bot, 'username')
55
+		super().__init__(
56
+			bot,
57
+			config_prefix='username',
58
+			name='username patterns',
59
+			short_description='Manages username pattern detection.',
60
+		)
55
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)
61
 		self.add_setting(UsernamePatternCog.SETTING_ENABLED)
56
 
62
 
57
 	def __get_patterns(self, guild: Guild) -> list[str]:
63
 	def __get_patterns(self, guild: Guild) -> list[str]:
73
 		"""
79
 		"""
74
 		cls.set_guild_setting(guild, cls.SETTING_PATTERNS, patterns)
80
 		cls.set_guild_setting(guild, cls.SETTING_PATTERNS, patterns)
75
 
81
 
76
-	@commands.group(
77
-		brief='Manages username pattern detection'
82
+	username = Group(
83
+		name='username',
84
+		description='Manages username pattern detection.',
85
+		guild_only=True,
86
+		default_permissions=Permissions(Permissions.manage_messages.flag),
78
 	)
87
 	)
79
-	@commands.has_permissions(ban_members=True)
80
-	@commands.guild_only()
81
-	async def username(self, context: commands.Context):
82
-		"""Username pattern command group"""
83
-		if context.invoked_subcommand is None:
84
-			await context.send_help()
85
 
88
 
86
 	@username.command(
89
 	@username.command(
87
-		brief='Adds a username pattern',
88
-		description='Adds a username pattern.',
89
-		usage='<pattern>'
90
+		description='Adds a username pattern',
91
+		extras={
92
+			'long_description': 'Adds a username pattern.',
93
+			'usage': '<pattern>',
94
+		},
90
 	)
95
 	)
91
-	async def add(self, context: commands.Context, pattern: str) -> None:
96
+	async def add(self, interaction: Interaction, pattern: str) -> None:
92
 		"""Command handler"""
97
 		"""Command handler"""
93
 		norm_pattern = pattern.lower()
98
 		norm_pattern = pattern.lower()
94
-		patterns: list[str] = self.__get_patterns(context.guild)
99
+		patterns: list[str] = self.__get_patterns(interaction.guild)
95
 		if norm_pattern in patterns:
100
 		if norm_pattern in patterns:
96
-			await context.reply(f'Pattern `{norm_pattern}` already added.', mention_author=False)
101
+			await interaction.response.send_message(
102
+				f'Pattern `{norm_pattern}` already added.',
103
+				ephemeral=True
104
+			)
97
 			return
105
 			return
98
 		patterns.append(norm_pattern)
106
 		patterns.append(norm_pattern)
99
-		self.__save_patterns(context.guild, patterns)
100
-		await context.reply(f'Pattern `{norm_pattern}` added.', mention_author=False)
107
+		self.__save_patterns(interaction.guild, patterns)
108
+		await interaction.response.send_message(
109
+			f'Pattern `{norm_pattern}` added.',
110
+			ephemeral=True
111
+		)
101
 
112
 
102
 	@username.command(
113
 	@username.command(
103
-		brief='Removes a username pattern',
104
-		description='Removes an existing username pattern',
105
-		usage='<pattern>'
114
+		description='Removes a username pattern',
115
+		extras={
116
+			'long_description': 'Removes an existing username pattern',
117
+			'usage': '<pattern>',
118
+		},
106
 	)
119
 	)
107
-	async def remove(self, context: commands.Context, pattern: str) -> None:
120
+	async def remove(self, interaction: Interaction, pattern: str) -> None:
108
 		"""Command handler"""
121
 		"""Command handler"""
109
 		norm_pattern = pattern.lower()
122
 		norm_pattern = pattern.lower()
110
-		guild: Guild = context.guild
123
+		guild: Guild = interaction.guild
111
 		patterns: list[str] = self.__get_patterns(guild)
124
 		patterns: list[str] = self.__get_patterns(guild)
112
 		len_before = len(patterns)
125
 		len_before = len(patterns)
113
 		patterns = list(filter(lambda p: p != norm_pattern, patterns))
126
 		patterns = list(filter(lambda p: p != norm_pattern, patterns))
114
 		if len(patterns) == len_before:
127
 		if len(patterns) == len_before:
115
-			await context.reply(f'Pattern `{norm_pattern}` not found.', mention_author=False)
128
+			await interaction.response.send_message(
129
+				f'Pattern `{norm_pattern}` not found.',
130
+				ephemeral=True,
131
+			)
116
 			return
132
 			return
117
 		self.__save_patterns(guild, patterns)
133
 		self.__save_patterns(guild, patterns)
118
-		await context.reply(f'Pattern `{norm_pattern}` removed.', mention_author=False)
134
+		await interaction.response.send_message(
135
+			f'Pattern `{norm_pattern}` removed.',
136
+			ephemeral=True,
137
+		)
119
 
138
 
120
 	@username.command(
139
 	@username.command(
121
-		brief='Lists username patterns'
140
+		description='Lists username patterns'
122
 	)
141
 	)
123
-	async def list(self, context: commands.Context) -> None:
142
+	async def list(self, interaction: Interaction) -> None:
124
 		"""Command handler"""
143
 		"""Command handler"""
125
-		guild: Guild = context.guild
144
+		guild: Guild = interaction.guild
126
 		patterns: list[str] = self.__get_patterns(guild)
145
 		patterns: list[str] = self.__get_patterns(guild)
127
 		if len(patterns) == 0:
146
 		if len(patterns) == 0:
128
-			await context.reply('No patterns defined', mention_author=False)
147
+			await interaction.response.send_message(
148
+				'No patterns defined',
149
+				ephemeral=True,
150
+			)
129
 		else:
151
 		else:
130
 			msg = 'Patterns:\n\n> `' + '`\n> `'.join(patterns) + '`'
152
 			msg = 'Patterns:\n\n> `' + '`\n> `'.join(patterns) + '`'
131
-			await context.reply(msg, mention_author=False)
153
+			await interaction.response.send_message(
154
+				msg,
155
+				ephemeral=True,
156
+			)
132
 
157
 
133
-	@commands.Cog.listener()
158
+	@Cog.listener()
134
 	async def on_member_join(self, member: Member) -> None:
159
 	async def on_member_join(self, member: Member) -> None:
135
 		"""Event handler"""
160
 		"""Event handler"""
136
 		for pattern in self.__get_patterns(member.guild):
161
 		for pattern in self.__get_patterns(member.guild):

+ 23
- 25
rocketbot/cogsetting.py Ver fichero

1
 """
1
 """
2
 A guild configuration setting available for editing via bot commands.
2
 A guild configuration setting available for editing via bot commands.
3
 """
3
 """
4
-import inspect
5
-from typing import Any, Optional, Type, TypeVar, Coroutine, Literal
4
+from datetime import timedelta
5
+from typing import Any, Optional, Type, TypeVar, Literal
6
 
6
 
7
-from discord import Interaction, Permissions, permissions
8
-from discord.app_commands import Range, guild_only, default_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
9
 from discord.app_commands.commands import Command, Group, CommandCallback
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
14
+from rocketbot.utils import bot_log, TimeDeltaTransformer
15
 
15
 
16
 # def _fix_command(command: Command) -> None:
16
 # def _fix_command(command: Command) -> None:
17
 # 	"""
17
 # 	"""
132
 				)
132
 				)
133
 		setattr(cog.__class__, f'_cmd_get_{setting.name}', getter)
133
 		setattr(cog.__class__, f'_cmd_get_{setting.name}', getter)
134
 		getter.__qualname__ = f'{self.__class__.__name__}._cmd_get_{setting.name}'
134
 		getter.__qualname__ = f'{self.__class__.__name__}._cmd_get_{setting.name}'
135
+		getter.__self__ = cog
135
 		bot_log(None, cog.__class__, f"Creating /get {setting_name}")
136
 		bot_log(None, cog.__class__, f"Creating /get {setting_name}")
136
 		command = Command(
137
 		command = Command(
137
 			name=setting_name,
138
 			name=setting_name,
138
-			description=f'Shows value of {setting_name}',
139
+			description=f'Shows {self.brief}.',
139
 			callback=getter,
140
 			callback=getter,
140
 			parent=CogSetting.__get_group,
141
 			parent=CogSetting.__get_group,
141
 		)
142
 		)
163
 				ephemeral=True
164
 				ephemeral=True
164
 			)
165
 			)
165
 			await self.on_setting_updated(interaction.guild, setting)
166
 			await self.on_setting_updated(interaction.guild, setting)
166
-			self.log(interaction.guild, f'{interaction.message.author.name} set {key} to {new_value}')
167
+			self.log(interaction.guild, f'{interaction.user.name} set {key} to {new_value}')
167
 
168
 
168
-		type_str: str = 'any'
169
 		setter: CommandCallback = setter_general
169
 		setter: CommandCallback = setter_general
170
 		if self.datatype == int:
170
 		if self.datatype == int:
171
 			if self.min_value is not None or self.max_value is not None:
171
 			if self.min_value is not None or self.max_value is not None:
172
-				type_str = f'discord.app_commands.Range[int, {self.min_value}, {self.max_value}]'
173
 				r_min = self.min_value
172
 				r_min = self.min_value
174
 				r_max = self.max_value
173
 				r_max = self.max_value
175
 				async def setter_range(self, interaction: Interaction, new_value: Range[int, r_min, r_max]) -> None:
174
 				async def setter_range(self, interaction: Interaction, new_value: Range[int, r_min, r_max]) -> None:
176
 					await self.setter_general(interaction, new_value)
175
 					await self.setter_general(interaction, new_value)
177
 				setter = setter_range
176
 				setter = setter_range
178
 			else:
177
 			else:
179
-				type_str = 'int'
180
 				async def setter_int(self, interaction: Interaction, new_value: int) -> None:
178
 				async def setter_int(self, interaction: Interaction, new_value: int) -> None:
181
 					await self.setter_general(interaction, new_value)
179
 					await self.setter_general(interaction, new_value)
182
 				setter = setter_int
180
 				setter = setter_int
183
 		elif self.datatype == float:
181
 		elif self.datatype == float:
184
-			type_str = 'float'
185
 			async def setter_float(self, interaction: Interaction, new_value: float) -> None:
182
 			async def setter_float(self, interaction: Interaction, new_value: float) -> None:
186
 				await self.setter_general(interaction, new_value)
183
 				await self.setter_general(interaction, new_value)
187
 			setter = setter_float
184
 			setter = setter_float
185
+		elif self.datatype == timedelta:
186
+			async def setter_timedelta(self, interaction: Interaction, new_value: Transform[timedelta, TimeDeltaTransformer]) -> None:
187
+				await self.setter_general(interaction, new_value)
188
+			setter = setter_timedelta
188
 		elif getattr(self.datatype, '__origin__', None) == Literal:
189
 		elif getattr(self.datatype, '__origin__', None) == Literal:
189
-			value_list = '"' + '", "'.join(self.enum_values) + '"'
190
-			type_str = f'typing.Literal[{value_list}]'
191
-			values = self.enum_values
192
 			dt = self.datatype
190
 			dt = self.datatype
193
 			async def setter_enum(self, interaction: Interaction, new_value: dt) -> None:
191
 			async def setter_enum(self, interaction: Interaction, new_value: dt) -> None:
194
 				await self.setter_general(interaction, new_value)
192
 				await self.setter_general(interaction, new_value)
195
-
196
 			setter = setter_enum
193
 			setter = setter_enum
197
 		elif self.datatype == str:
194
 		elif self.datatype == str:
198
 			if self.enum_values is not None:
195
 			if self.enum_values is not None:
199
 				raise ValueError('Type for a setting with enum values should be typing.Literal')
196
 				raise ValueError('Type for a setting with enum values should be typing.Literal')
200
 			else:
197
 			else:
201
-				type_str = 'str'
202
 				async def setter_str(self, interaction: Interaction, new_value: str) -> None:
198
 				async def setter_str(self, interaction: Interaction, new_value: str) -> None:
203
 					await self.setter_general(interaction, new_value)
199
 					await self.setter_general(interaction, new_value)
204
 				setter = setter_str
200
 				setter = setter_str
205
 		elif setting.datatype == bool:
201
 		elif setting.datatype == bool:
206
-			type_str = f'bool'
207
 			async def setter_bool(self, interaction: Interaction, new_value: bool) -> None:
202
 			async def setter_bool(self, interaction: Interaction, new_value: bool) -> None:
208
 				await self.setter_general(interaction, new_value)
203
 				await self.setter_general(interaction, new_value)
209
 			setter = setter_bool
204
 			setter = setter_bool
211
 			raise ValueError(f'Invalid type {self.datatype}')
206
 			raise ValueError(f'Invalid type {self.datatype}')
212
 		setattr(cog.__class__, f'_cmd_set_{setting.name}', setter)
207
 		setattr(cog.__class__, f'_cmd_set_{setting.name}', setter)
213
 		setter.__qualname__ = f'{cog.__class__.__name__}._cmd_set_{setting.name}'
208
 		setter.__qualname__ = f'{cog.__class__.__name__}._cmd_set_{setting.name}'
214
-		bot_log(None, cog.__class__, f"Creating /set {setting_name} {type_str}")
209
+		setter.__self__ = cog
210
+		bot_log(None, cog.__class__, f"Creating /set {setting_name} {self.datatype}")
215
 		command = Command(
211
 		command = Command(
216
 			name=setting_name,
212
 			name=setting_name,
217
-			description=f'Sets value of {setting_name}',
213
+			description=f'Sets {self.brief}.',
218
 			callback=setter,
214
 			callback=setter,
219
 			parent=CogSetting.__set_group,
215
 			parent=CogSetting.__set_group,
220
 		)
216
 		)
226
 
222
 
227
 	def __make_enable_command(self, cog: BaseCog) -> Command:
223
 	def __make_enable_command(self, cog: BaseCog) -> Command:
228
 		setting: CogSetting = self
224
 		setting: CogSetting = self
229
-		async def enabler(self, interaction: Interaction) -> None:
225
+		async def enabler(self: BaseCog, interaction: Interaction) -> None:
230
 			print(f"invoking enable for {self.config_prefix}")
226
 			print(f"invoking enable for {self.config_prefix}")
231
 			key = f'{self.__class__.__name__}.{setting.name}'
227
 			key = f'{self.__class__.__name__}.{setting.name}'
232
 			Storage.set_config_value(interaction.guild, key, True)
228
 			Storage.set_config_value(interaction.guild, key, True)
235
 				ephemeral=True
231
 				ephemeral=True
236
 			)
232
 			)
237
 			await self.on_setting_updated(interaction.guild, setting)
233
 			await self.on_setting_updated(interaction.guild, setting)
238
-			self.log(interaction.guild, f'{interaction.message.author.name} enabled {self.__class__.__name__}')
234
+			self.log(interaction.guild, f'{interaction.user.name} enabled {self.__class__.__name__}')
239
 		setattr(cog.__class__, f'_cmd_enable', enabler)
235
 		setattr(cog.__class__, f'_cmd_enable', enabler)
240
 		enabler.__qualname__ = f'{cog.__class__.__name__}._cmd_enable'
236
 		enabler.__qualname__ = f'{cog.__class__.__name__}._cmd_enable'
237
+		enabler.__self__ = cog
241
 		bot_log(None, cog.__class__, f"Creating /enable {cog.config_prefix}")
238
 		bot_log(None, cog.__class__, f"Creating /enable {cog.config_prefix}")
242
 
239
 
243
 		command = Command(
240
 		command = Command(
252
 
249
 
253
 	def __make_disable_command(self, cog: BaseCog) -> Command:
250
 	def __make_disable_command(self, cog: BaseCog) -> Command:
254
 		setting: CogSetting = self
251
 		setting: CogSetting = self
255
-		async def disabler(self, interaction: Interaction) -> None:
252
+		async def disabler(self: BaseCog, interaction: Interaction) -> None:
256
 			print(f"invoking disable for {self.config_prefix}")
253
 			print(f"invoking disable for {self.config_prefix}")
257
 			key = f'{self.__class__.__name__}.{setting.name}'
254
 			key = f'{self.__class__.__name__}.{setting.name}'
258
 			Storage.set_config_value(interaction.guild, key, False)
255
 			Storage.set_config_value(interaction.guild, key, False)
261
 				ephemeral=True
258
 				ephemeral=True
262
 			)
259
 			)
263
 			await self.on_setting_updated(interaction.guild, setting)
260
 			await self.on_setting_updated(interaction.guild, setting)
264
-			self.log(interaction.guild, f'{interaction.message.author.name} disabled {self.__class__.__name__}')
261
+			self.log(interaction.guild, f'{interaction.user.name} disabled {self.__class__.__name__}')
265
 		setattr(cog.__class__, f'_cmd_disable', disabler)
262
 		setattr(cog.__class__, f'_cmd_disable', disabler)
266
 		disabler.__qualname__ = f'{cog.__class__.__name__}._cmd_disable'
263
 		disabler.__qualname__ = f'{cog.__class__.__name__}._cmd_disable'
264
+		disabler.__self__ = cog
267
 		bot_log(None, cog.__class__, f"Creating /disable {cog.config_prefix}")
265
 		bot_log(None, cog.__class__, f"Creating /disable {cog.config_prefix}")
268
 
266
 
269
 		command = Command(
267
 		command = Command(
314
 		)
312
 		)
315
 		cls.__enable_group = Group(
313
 		cls.__enable_group = Group(
316
 			name='enable',
314
 			name='enable',
317
-			description='Enables a set of bot functionality for this guild',
315
+			description='Enables bot functionality for this guild',
318
 			default_permissions=cls.permissions
316
 			default_permissions=cls.permissions
319
 		)
317
 		)
320
 		cls.__disable_group = Group(
318
 		cls.__disable_group = Group(
321
 			name='disable',
319
 			name='disable',
322
-			description='Disables a set of bot functionality for this guild',
320
+			description='Disables bot functionality for this guild',
323
 			default_permissions=cls.permissions
321
 			default_permissions=cls.permissions
324
 		)
322
 		)
325
 		bot.tree.add_command(cls.__set_group)
323
 		bot.tree.add_command(cls.__set_group)

+ 38
- 4
rocketbot/utils.py Ver fichero

17
 
17
 
18
 def timedelta_from_str(s: str) -> timedelta:
18
 def timedelta_from_str(s: str) -> timedelta:
19
 	"""
19
 	"""
20
-	Parses a timespan. Format examples:
20
+	Parses a timespan.
21
+
22
+	Format examples:
21
 	"30m"
23
 	"30m"
22
 	"10s"
24
 	"10s"
23
 	"90d"
25
 	"90d"
24
 	"1h30m"
26
 	"1h30m"
25
 	"73d18h22m52s"
27
 	"73d18h22m52s"
28
+
29
+	Parameters
30
+	----------
31
+	s : str
32
+		string to parse
33
+
34
+	Returns
35
+	-------
36
+	timedelta
37
+
38
+	Raises
39
+	------
40
+	ValueError
41
+		if parsing fails
26
 	"""
42
 	"""
27
-	p: re.Pattern = re.compile('^(?:[0-9]+[dhms])+$')
43
+	p: re.Pattern = re.compile('^(?:[0-9]+[a-zA-Z])+$')
28
 	if p.match(s) is None:
44
 	if p.match(s) is None:
29
-		raise ValueError("Illegal timespan value '{s}'.")
45
+		raise ValueError(f'Illegal timespan value "{s}". Examples: 30s, 5m, 1h30m, 30d')
30
 	p = re.compile('([0-9]+)([dhms])')
46
 	p = re.compile('([0-9]+)([dhms])')
31
 	days: int = 0
47
 	days: int = 0
32
 	hours: int = 0
48
 	hours: int = 0
34
 	seconds: int = 0
50
 	seconds: int = 0
35
 	for m in p.finditer(s):
51
 	for m in p.finditer(s):
36
 		scalar = int(m.group(1))
52
 		scalar = int(m.group(1))
37
-		unit = m.group(2)
53
+		unit = m.group(2).lower()
38
 		if unit == 'd':
54
 		if unit == 'd':
39
 			days = scalar
55
 			days = scalar
40
 		elif unit == 'h':
56
 		elif unit == 'h':
43
 			minutes = scalar
59
 			minutes = scalar
44
 		elif unit == 's':
60
 		elif unit == 's':
45
 			seconds = scalar
61
 			seconds = scalar
62
+		else:
63
+			raise ValueError(f'Invalid unit "{unit}". Valid units: "s"=seconds, "m"=minutes, "h"=hours, "d"=days')
46
 	return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
64
 	return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
47
 
65
 
48
 def str_from_timedelta(td: timedelta) -> str:
66
 def str_from_timedelta(td: timedelta) -> str:
152
 	if len(val) < 2 or val[0:1] not in __QUOTE_CHARS or val[-1:] not in __QUOTE_CHARS:
170
 	if len(val) < 2 or val[0:1] not in __QUOTE_CHARS or val[-1:] not in __QUOTE_CHARS:
153
 		raise ValueError(f'Not a quoted string: {val}')
171
 		raise ValueError(f'Not a quoted string: {val}')
154
 	return val[1:-1]
172
 	return val[1:-1]
173
+
174
+from discord import Interaction
175
+from discord.app_commands import Transformer
176
+from discord.ext.commands import BadArgument
177
+
178
+class TimeDeltaTransformer(Transformer):
179
+	async def transform(self, interaction: Interaction, value: Any) -> timedelta:
180
+		try:
181
+			return timedelta_from_str(str(value))
182
+		except ValueError as e:
183
+			print("Invalid time delta:", e)
184
+			raise BadArgument(str(e))
185
+
186
+	@property
187
+	def _error_display_name(self) -> str:
188
+		return 'timedelta'

Loading…
Cancelar
Guardar