Experimental Discord bot written in Python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

generalcog.py 5.0KB

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