| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- """
- Cog for handling most ungrouped commands and basic behaviors.
- """
- from datetime import datetime, timedelta, timezone
- from typing import Optional
-
- from discord import Interaction, Message, User
- from discord.app_commands import command, default_permissions, guild_only, Transform
- 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
- 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': 'If a warning channel is configured, it will be posted '
- 'there. If a warning role/user is configured, they will be '
- 'tagged in the message.',
- },
- )
- @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='Responds to the user with a greeting.',
- extras={
- 'long_description': 'Useful for checking bot responsiveness. Message '
- 'is only visible to the user.',
- },
- )
- async def hello(self, interaction: Interaction):
- await interaction.response.send_message(
- f'Hey, {interaction.user.name}!',
- ephemeral=True,
- )
-
- @command(
- description='Shuts down the bot.',
- extras={
- 'long_description': 'For emergency use if the bot gains sentience. 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 recent messages by a user.',
- extras={
- 'long_description': 'The age is a duration, such as "30s", "5m", "1h30m", "7d". '
- 'Only the most recent 100 messages in each channel are searched.\n\n'
- "The author can be a numeric ID if they aren't showing up in autocomplete.",
- 'usage': '<user:id|mention> <age:timespan>',
- },
- )
- @guild_only()
- @default_permissions(manage_messages=True)
- async def delete_messages(self, interaction: Interaction, author: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None:
- """
- Mass deletes messages.
-
- Parameters
- ----------
- interaction: :class:`Interaction`
- author: :class:`User`
- author of messages to delete
- age: :class:`timedelta`
- maximum age of messages to delete (e.g. 30s, 5m, 1h30s, 7d)
- """
- member_id = author.id
- cutoff: datetime = datetime.now(timezone.utc) - age
-
- # Finding and deleting messages takes time but interaction needs a timely acknowledgement.
- 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 {author.mention} from the past {describe_timedelta(age)}.',
- )
- else:
- await resp.resource.edit(
- content=f'{CONFIG["success_emoji"]} No messages found for {author.mention} '
- 'from the past {describe_timedelta(age)}.',
- )
|