Experimental Discord bot written in Python
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

generalcog.py 5.5KB

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