""" Cog for handling most ungrouped commands and basic behaviors. """ import re from datetime import datetime, timedelta, timezone from typing import Optional, Union from discord import Interaction, Message, User, Permissions, AppCommandType from discord.app_commands import Command, Group, command, default_permissions, guild_only, Transform, Choice, \ autocomplete from discord.errors import DiscordException from discord.ext.commands import Cog from config import CONFIG from rocketbot.bot import Rocketbot from rocketbot.cogs.basecog import BaseCog, BotMessage from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace, MOD_PERMISSIONS from rocketbot.storage import ConfigKey, Storage class GeneralCog(BaseCog, name='General'): """ Cog for handling high-level bot functionality and commands. Should be the first cog added to the bot. """ shared: Optional['GeneralCog'] = None def __init__(self, bot: Rocketbot): super().__init__( bot, config_prefix=None, short_description='', ) self.is_connected = False self.is_first_connect = True self.last_disconnect_time: Optional[datetime] = None self.noteworthy_disconnect_duration = timedelta(seconds=5) GeneralCog.shared = self @Cog.listener() async def on_connect(self): """Event handler""" if self.is_first_connect: self.log(None, 'Connected') self.is_first_connect = False else: disconnect_duration = datetime.now( timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration: self.log(None, f'Reconnected after {disconnect_duration.total_seconds()} seconds') self.is_connected = True @Cog.listener() async def on_disconnect(self): """Event handler""" self.last_disconnect_time = datetime.now(timezone.utc) # self.log(None, 'Disconnected') @Cog.listener() async def on_resumed(self): """Event handler""" disconnect_duration = datetime.now(timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration: self.log(None, f'Session resumed after {disconnect_duration.total_seconds()} seconds') @command( description='Posts a test warning.', extras={ 'long_description': 'Simulates a warning. The configured warning channel ' 'and mod mention will be used.', }, ) @guild_only() @default_permissions(ban_members=True) async def test_warn(self, interaction: Interaction): if Storage.get_config_value(interaction.guild, ConfigKey.WARNING_CHANNEL_ID) is None: await interaction.response.send_message( f'{CONFIG["warning_emoji"]} No warning channel set!', ephemeral=True, ) else: bm = BotMessage( interaction.guild, f'Test warning message (requested by {interaction.user.name})', type=BotMessage.TYPE_MOD_WARNING) await self.post_message(bm) await interaction.response.send_message( 'Warning issued', ephemeral=True, ) @command( description='Greets the user.', extras={ 'long_description': 'Replies to the command message. Useful to ensure the ' 'bot is responsive. Message is only visible to the user.', }, ) async def hello(self, interaction: Interaction): """Command handler""" await interaction.response.send_message( f'Hey, {interaction.user.name}!', ephemeral=True, ) @command( description='Shuts down the bot.', extras={ 'long_description': 'Terminates the bot script. Only usable by a ' 'server administrator.', }, ) @guild_only() @default_permissions(administrator=True) async def shutdown(self, interaction: Interaction): """Command handler""" await interaction.response.send_message('👋', ephemeral=True) await self.bot.close() @command( description='Mass deletes messages.', extras={ 'long_description': 'Deletes recent messages by the given user. The user ' + 'can be either an @ mention or a numeric user ID. The age is ' + 'a duration, such as "30s", "5m", "1h30m". Only the most ' + 'recent 100 messages in each channel are searched.', 'usage': ' ', }, ) @guild_only() @default_permissions(manage_messages=True) async def delete_messages(self, interaction: Interaction, user: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None: """ Mass deletes messages. Parameters ---------- interaction: :class:`Interaction` user: :class:`User` user to delete messages from age: :class:`timedelta` maximum age of messages to delete """ member_id = user.id cutoff: datetime = datetime.now(timezone.utc) - age resp = await interaction.response.defer(ephemeral=True, thinking=True) def predicate(message: Message) -> bool: return str(message.author.id) == member_id and message.created_at >= cutoff deleted_messages = [] for channel in interaction.guild.text_channels: try: deleted_messages += await channel.purge(limit=100, check=predicate) except DiscordException: # XXX: Sloppily glossing over access errors instead of checking access pass if len(deleted_messages) > 0: await resp.resource.edit( content=f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} ' f'messages by {user.mention} from the past {describe_timedelta(age)}.', ) else: await resp.resource.edit( content=f'{CONFIG["success_emoji"]} No messages found for {user.mention} ' 'from the past {describe_timedelta(age)}.', )