Experimental Discord bot written in Python
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. from discord import Guild, Member, Message
  2. from discord.ext import commands
  3. import re
  4. from datetime import timedelta
  5. from cogs.basecog import BaseCog, BotMessage, BotMessageReaction
  6. from config import CONFIG
  7. from storage import Storage
  8. class URLSpamContext:
  9. def __init__(self, spam_message: Message):
  10. self.spam_message = spam_message
  11. self.is_deleted = False
  12. self.is_kicked = False
  13. self.is_banned = False
  14. class URLSpamCog(BaseCog):
  15. CONFIG_KEY_EARLY_URL_TIMEOUT = "urlspam_early_url_timeout"
  16. CONFIG_KEY_EARLY_URL_ACTION = "urlspam_early_url_action"
  17. def __init__(self, bot):
  18. super().__init__(bot)
  19. def __early_url_timeout(self, guild: Guild) -> int:
  20. return Storage.get_config_value(guild, self.CONFIG_KEY_EARLY_URL_TIMEOUT) or \
  21. self.get_cog_default('early_url_timeout')
  22. def __early_url_action(self, guild: Guild) -> str:
  23. return Storage.get_config_value(guild, self.CONFIG_KEY_EARLY_URL_ACTION) or \
  24. self.get_cog_default('early_url_action')
  25. @commands.Cog.listener()
  26. async def on_message(self, message: Message):
  27. if message.author is None or \
  28. message.author.bot or \
  29. message.guild is None or \
  30. message.channel is None or \
  31. message.content is None:
  32. return
  33. action = self.__early_url_action(message.guild)
  34. if action == 'nothing':
  35. return
  36. if not self.__contains_url(message.content):
  37. return
  38. join_age = message.created_at - message.author.joined_at
  39. join_age_str = self.__format_timedelta(join_age)
  40. if join_age.total_seconds() < self.__early_url_timeout(message.guild):
  41. if action == 'modwarn':
  42. bm = BotMessage(
  43. message.guild,
  44. f'User {message.author.mention} posted a URL ' + \
  45. f'{join_age_str} after joining.',
  46. type = BotMessage.TYPE_MOD_WARNING,
  47. context = URLSpamContext(message))
  48. bm.quote = message.content
  49. await bm.add_reaction(BotMessageReaction(CONFIG['trash_emoji'], True, 'Delete message'))
  50. await bm.add_reaction(BotMessageReaction(CONFIG['kick_emoji'], True, 'Kick user'))
  51. await bm.add_reaction(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
  52. await self.post_message(bm)
  53. self.log(message.guild, f'New user {message.author.name} ' + \
  54. f'({message.author.id}) posted URL. Mods alerted.')
  55. elif action == 'delete':
  56. await message.delete()
  57. bm = BotMessage(
  58. message.guild,
  59. f'User {message.author.mention} posted a URL ' + \
  60. f'{join_age_str} after joining. Message deleted.',
  61. type = BotMessage.TYPE_INFO,
  62. context = URLSpamContext(message))
  63. bm.quote = message.content
  64. await bm.add_reaction(BotMessageReaction(CONFIG['kick_emoji'], True, 'Kick user'))
  65. await bm.add_reaction(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
  66. await self.post_message(bm)
  67. self.log(message.guild, f'New user {message.author.name} ' + \
  68. f'({message.author.id}) posted URL. Message deleted.')
  69. elif action == 'kick':
  70. await message.author.kick(reason='User posted a link ' + \
  71. f'{join_age_str} after joining')
  72. bm = BotMessage(
  73. message.guild,
  74. f'User {message.author.mention} posted a URL ' + \
  75. f'{join_age_str} after joining. Kicked by bot.',
  76. type = BotMessage.TYPE_INFO,
  77. context = URLSpamContext(message))
  78. bm.quote = message.content
  79. await bm.add_reaction(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
  80. await self.post_message(bm)
  81. self.log(message.guild, f'New user {message.author.name} ' + \
  82. f'({message.author.id}) posted URL. User kicked.')
  83. elif action == 'ban':
  84. await message.author.ban(reason='User posted a link ' + \
  85. f'{join_age_str} after joining', delete_message_days=1)
  86. bm = BotMessage(
  87. message.guild,
  88. f'User {message.author.mention} posted a URL ' + \
  89. f'{join_age_str} after joining. Banned by bot.',
  90. type = BotMessage.TYPE_INFO,
  91. context = URLSpamContext(message))
  92. bm.quote = message.content
  93. await self.post_message(bm)
  94. self.log(message.guild, f'New user {message.author.name} ' + \
  95. f'({message.author.id}) posted URL. User banned.')
  96. async def on_mod_react(self,
  97. bot_message: BotMessage,
  98. reaction: BotMessageReaction,
  99. reacted_by: Member) -> None:
  100. context: URLSpamContext = bot_message.context
  101. if context is None:
  102. return
  103. sm: Message = context.spam_message
  104. if reaction.emoji == CONFIG['trash_emoji']:
  105. if not context.is_deleted:
  106. await sm.delete()
  107. context.is_deleted = True
  108. self.log(sm.guild, f'URL spam by {sm.author.name} deleted by {reacted_by.name}')
  109. elif reaction.emoji == CONFIG['kick_emoji']:
  110. if not context.is_deleted:
  111. await sm.delete()
  112. context.is_deleted = True
  113. if not context.is_kicked:
  114. await sm.author.kick(reason=f'Rocketbot: Kicked for URL spam by {reacted_by.name}')
  115. context.is_kicked = True
  116. self.log(sm.guild, f'URL spammer {sm.author.name} kicked by {reacted_by.name}')
  117. elif reaction.emoji == CONFIG['ban_emoji']:
  118. if not context.is_banned:
  119. await sm.author.ban(reason=f'Rocketbot: Banned for URL spam by {reacted_by.name}', delete_message_days=1)
  120. context.is_deleted = True
  121. context.is_kicked = True
  122. context.is_banned = True
  123. self.log(sm.guild, f'URL spammer {sm.author.name} banned by {reacted_by.name}')
  124. else:
  125. return
  126. await bot_message.set_reactions(BotMessageReaction.standard_set(
  127. did_delete=context.is_deleted,
  128. did_kick=context.is_kicked,
  129. did_ban=context.is_banned))
  130. def __contains_url(self, text: str) -> bool:
  131. p = re.compile(r'http(?:s)?://[^\s]+')
  132. return p.search(text) is not None
  133. def __format_timedelta(self, timespan: timedelta) -> str:
  134. parts = []
  135. d = timespan.days
  136. h = timespan.seconds // 3600
  137. m = (timespan.seconds // 60) % 60
  138. s = timespan.seconds % 60
  139. if d > 0:
  140. parts.append(f'{d} days')
  141. if d > 0 or h > 0:
  142. parts.append(f'{h} hours')
  143. if d > 0 or h > 0 or m > 0:
  144. parts.append(f'{m} minutes')
  145. parts.append(f'{s} seconds')
  146. # Limit the precision to the two most significant elements
  147. while len(parts) > 2:
  148. parts.pop(-1)
  149. return ' '.join(parts)