Experimental Discord bot written in Python
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

generalcog.py 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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. shared: Optional['GeneralCog'] = None
  21. def __init__(self, bot: Rocketbot):
  22. super().__init__(
  23. bot,
  24. config_prefix=None,
  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. GeneralCog.shared = self
  32. @Cog.listener()
  33. async def on_connect(self):
  34. """Event handler"""
  35. if self.is_first_connect:
  36. self.log(None, 'Connected')
  37. self.is_first_connect = False
  38. else:
  39. disconnect_duration = datetime.now(
  40. timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None
  41. if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration:
  42. self.log(None, f'Reconnected after {disconnect_duration.total_seconds()} seconds')
  43. self.is_connected = True
  44. @Cog.listener()
  45. async def on_disconnect(self):
  46. """Event handler"""
  47. self.last_disconnect_time = datetime.now(timezone.utc)
  48. # self.log(None, 'Disconnected')
  49. @Cog.listener()
  50. async def on_resumed(self):
  51. """Event handler"""
  52. disconnect_duration = datetime.now(timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None
  53. if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration:
  54. self.log(None, f'Session resumed after {disconnect_duration.total_seconds()} seconds')
  55. @command(
  56. description='Posts a test warning.',
  57. extras={
  58. 'long_description': 'If a warning channel is configured, it will be posted '
  59. 'there. If a warning role/user is configured, they will be '
  60. 'tagged in the message.',
  61. },
  62. )
  63. @guild_only()
  64. @default_permissions(ban_members=True)
  65. async def test_warn(self, interaction: Interaction):
  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. self.log(interaction.guild, f'{interaction.user.name} used /test_warn')
  82. @command(
  83. description='Responds to the user with a greeting.',
  84. extras={
  85. 'long_description': 'Useful for checking bot responsiveness. Message '
  86. 'is only visible to the user.',
  87. },
  88. )
  89. async def hello(self, interaction: Interaction):
  90. await interaction.response.send_message(
  91. f'Hey, {interaction.user.name}!',
  92. ephemeral=True,
  93. )
  94. self.log(interaction.guild, f'{interaction.user.name} used /hello')
  95. @command(
  96. description='Shuts down the bot.',
  97. extras={
  98. 'long_description': 'For emergency use if the bot gains sentience. Only usable '
  99. 'by a server administrator.',
  100. },
  101. )
  102. @guild_only()
  103. @default_permissions(administrator=True)
  104. async def shutdown(self, interaction: Interaction):
  105. """Command handler"""
  106. await interaction.response.send_message('👋', ephemeral=True)
  107. self.log(interaction.guild, f'{interaction.user.name} used /shutdown')
  108. await self.bot.close()
  109. @command(
  110. description='Mass deletes recent messages by a user.',
  111. extras={
  112. 'long_description': 'The age is a duration, such as "30s", "5m", "1h30m", "7d". '
  113. 'Only the most recent 100 messages in each channel are searched.\n\n'
  114. "The author can be a numeric ID if they aren't showing up in autocomplete.",
  115. 'usage': '<user:id|mention> <age:timespan>',
  116. },
  117. )
  118. @guild_only()
  119. @default_permissions(manage_messages=True)
  120. async def delete_messages(self, interaction: Interaction, author: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None:
  121. """
  122. Mass deletes messages.
  123. Parameters
  124. ----------
  125. interaction: :class:`Interaction`
  126. author: :class:`User`
  127. author of messages to delete
  128. age: :class:`timedelta`
  129. maximum age of messages to delete (e.g. 30s, 5m, 1h30s, 7d)
  130. """
  131. member_id = author.id
  132. cutoff: datetime = datetime.now(timezone.utc) - age
  133. # Finding and deleting messages takes time but interaction needs a timely acknowledgement.
  134. resp = await interaction.response.defer(ephemeral=True, thinking=True)
  135. def predicate(message: Message) -> bool:
  136. return str(message.author.id) == member_id and message.created_at >= cutoff
  137. deleted_messages = []
  138. for channel in interaction.guild.text_channels:
  139. try:
  140. deleted_messages += await channel.purge(limit=100, check=predicate)
  141. except DiscordException:
  142. # XXX: Sloppily glossing over access errors instead of checking access
  143. pass
  144. if len(deleted_messages) > 0:
  145. await resp.resource.edit(
  146. content=f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} '
  147. f'messages by {author.mention} from the past {describe_timedelta(age)}.',
  148. )
  149. else:
  150. await resp.resource.edit(
  151. content=f'{CONFIG["success_emoji"]} No messages found for {author.mention} '
  152. f'from the past {describe_timedelta(age)}.',
  153. )
  154. self.log(interaction.guild, f'{interaction.user.name} used /delete_messages {author.id} {age}')