Experimental Discord bot written in Python
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

generalcog.py 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. """
  2. Cog for handling most ungrouped commands and basic behaviors.
  3. """
  4. import re
  5. from datetime import datetime, timedelta, timezone
  6. from typing import Optional, Union
  7. from discord import Interaction, Message, User, Permissions, AppCommandType
  8. from discord.app_commands import Command, Group, command, default_permissions, guild_only, Transform, Choice, \
  9. autocomplete
  10. from discord.errors import DiscordException
  11. from discord.ext.commands import Cog
  12. from config import CONFIG
  13. from rocketbot.bot import Rocketbot
  14. from rocketbot.cogs.basecog import BaseCog, BotMessage
  15. from rocketbot.utils import describe_timedelta, TimeDeltaTransformer, dump_stacktrace, MOD_PERMISSIONS
  16. from rocketbot.storage import ConfigKey, Storage
  17. class GeneralCog(BaseCog, name='General'):
  18. """
  19. Cog for handling high-level bot functionality and commands. Should be the
  20. first cog added to the bot.
  21. """
  22. shared: Optional['GeneralCog'] = None
  23. def __init__(self, bot: Rocketbot):
  24. super().__init__(
  25. bot,
  26. config_prefix=None,
  27. short_description='',
  28. )
  29. self.is_connected = False
  30. self.is_first_connect = True
  31. self.last_disconnect_time: Optional[datetime] = None
  32. self.noteworthy_disconnect_duration = timedelta(seconds=5)
  33. GeneralCog.shared = self
  34. @Cog.listener()
  35. async def on_connect(self):
  36. """Event handler"""
  37. if self.is_first_connect:
  38. self.log(None, 'Connected')
  39. self.is_first_connect = False
  40. else:
  41. disconnect_duration = datetime.now(
  42. timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None
  43. if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration:
  44. self.log(None, f'Reconnected after {disconnect_duration.total_seconds()} seconds')
  45. self.is_connected = True
  46. @Cog.listener()
  47. async def on_disconnect(self):
  48. """Event handler"""
  49. self.last_disconnect_time = datetime.now(timezone.utc)
  50. # self.log(None, 'Disconnected')
  51. @Cog.listener()
  52. async def on_resumed(self):
  53. """Event handler"""
  54. disconnect_duration = datetime.now(timezone.utc) - self.last_disconnect_time if self.last_disconnect_time else None
  55. if disconnect_duration is not None and disconnect_duration > self.noteworthy_disconnect_duration:
  56. self.log(None, f'Session resumed after {disconnect_duration.total_seconds()} seconds')
  57. @command(
  58. description='Posts a test warning.',
  59. extras={
  60. 'long_description': 'Simulates a warning. The configured warning channel '
  61. 'and mod mention will be used.',
  62. },
  63. )
  64. @guild_only()
  65. @default_permissions(ban_members=True)
  66. async def test_warn(self, interaction: Interaction):
  67. if Storage.get_config_value(interaction.guild, ConfigKey.WARNING_CHANNEL_ID) is None:
  68. await interaction.response.send_message(
  69. f'{CONFIG["warning_emoji"]} No warning channel set!',
  70. ephemeral=True,
  71. )
  72. else:
  73. bm = BotMessage(
  74. interaction.guild,
  75. f'Test warning message (requested by {interaction.user.name})',
  76. type=BotMessage.TYPE_MOD_WARNING)
  77. await self.post_message(bm)
  78. await interaction.response.send_message(
  79. 'Warning issued',
  80. ephemeral=True,
  81. )
  82. @command(
  83. description='Greets the user.',
  84. extras={
  85. 'long_description': 'Replies to the command message. Useful to ensure the '
  86. 'bot is responsive. Message is only visible to the user.',
  87. },
  88. )
  89. async def hello(self, interaction: Interaction):
  90. """Command handler"""
  91. await interaction.response.send_message(
  92. f'Hey, {interaction.user.name}!',
  93. ephemeral=True,
  94. )
  95. @command(
  96. description='Shuts down the bot.',
  97. extras={
  98. 'long_description': 'Terminates the bot script. Only usable by a '
  99. '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. await self.bot.close()
  108. @command(
  109. description='Mass deletes messages.',
  110. extras={
  111. 'long_description': 'Deletes recent messages by the given user. The user ' +
  112. 'can be either an @ mention or a numeric user ID. The age is ' +
  113. 'a duration, such as "30s", "5m", "1h30m". Only the most ' +
  114. 'recent 100 messages in each channel are searched.',
  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, user: User, age: Transform[timedelta, TimeDeltaTransformer]) -> None:
  121. """
  122. Mass deletes messages.
  123. Parameters
  124. ----------
  125. interaction: :class:`Interaction`
  126. user: :class:`User`
  127. user to delete messages from
  128. age: :class:`timedelta`
  129. maximum age of messages to delete
  130. """
  131. member_id = user.id
  132. cutoff: datetime = datetime.now(timezone.utc) - age
  133. resp = await interaction.response.defer(ephemeral=True, thinking=True)
  134. def predicate(message: Message) -> bool:
  135. return str(message.author.id) == member_id and message.created_at >= cutoff
  136. deleted_messages = []
  137. for channel in interaction.guild.text_channels:
  138. try:
  139. deleted_messages += await channel.purge(limit=100, check=predicate)
  140. except DiscordException:
  141. # XXX: Sloppily glossing over access errors instead of checking access
  142. pass
  143. if len(deleted_messages) > 0:
  144. await resp.resource.edit(
  145. content=f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} '
  146. f'messages by {user.mention} from the past {describe_timedelta(age)}.',
  147. )
  148. else:
  149. await resp.resource.edit(
  150. content=f'{CONFIG["success_emoji"]} No messages found for {user.mention} '
  151. 'from the past {describe_timedelta(age)}.',
  152. )