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.

crosspostcog.py 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. from discord import Guild, Message, PartialEmoji
  2. from discord.ext import commands
  3. from datetime import datetime, timedelta
  4. from rscollections import AgeBoundList, SizeBoundDict
  5. from cogs.basecog import BaseCog
  6. from storage import Storage
  7. class SpamContext:
  8. def __init__(self, member, message_hash):
  9. self.member = member
  10. self.message_hash = message_hash
  11. self.age = datetime.now()
  12. self.warning_message = None
  13. self.is_kicked = False
  14. self.is_banned = False
  15. self.messages = set()
  16. self.deleted_messages = set()
  17. class CrossPostCog(BaseCog):
  18. STATE_KEY_RECENT_MESSAGES = "crosspost_recent_messages"
  19. STATE_KEY_SPAM_CONTEXT = "crosspost_spam_context"
  20. def __init__(self, bot):
  21. super().__init__(bot)
  22. self.max_recent_message_age = timedelta(minutes=2)
  23. self.max_spam_contexts = 12
  24. self.warn_messages_per_user = 3
  25. self.ban_messages_per_user = 5
  26. self.min_message_length = 10
  27. async def __record_message(self, message: Message) -> None:
  28. if message.author.permissions_in(message.channel).ban_members:
  29. # User exempt from spam detection
  30. return
  31. recent_messages = Storage.get_state_value(message.guild, self.STATE_KEY_RECENT_MESSAGES) \
  32. or AgeBoundList(self.max_recent_message_age, lambda index, message : message.created_at)
  33. recent_messages.append(message)
  34. Storage.set_state_value(message.guild, self.STATE_KEY_RECENT_MESSAGES, recent_messages)
  35. # Get all recent messages by user
  36. member_messages = [m for m in recent_messages if m.author.id == message.author.id]
  37. if len(member_messages) < self.warn_messages_per_user:
  38. return
  39. # Look for repeats
  40. hash_to_count = {}
  41. max_count = 0
  42. for m in member_messages:
  43. key = hash(m.content)
  44. count = (hash_to_count.get(key) or 0) + 1
  45. hash_to_count[key] = count
  46. max_count = max(max_count, count)
  47. if max_count < self.warn_messages_per_user:
  48. return
  49. # Handle the spam
  50. spam_lookup = Storage.get_state_value(message.guild, self.STATE_KEY_SPAM_CONTEXT) \
  51. or SizeBoundDict(self.max_spam_contexts, lambda key, context : context.age)
  52. Storage.set_state_value(message.guild, self.STATE_KEY_SPAM_CONTEXT, spam_lookup)
  53. for message_hash, count in hash_to_count.items():
  54. if count < self.warn_messages_per_user:
  55. continue
  56. key = f'{message.author.id}|{message_hash}'
  57. context = spam_lookup.get(key)
  58. is_new = context is None
  59. if context is None:
  60. context = SpamContext(message.author, message_hash)
  61. spam_lookup[key] = context
  62. context.age = message.created_at
  63. for m in member_messages:
  64. if hash(m.content) == message_hash:
  65. context.messages.add(m)
  66. await self.__update_from_context(context)
  67. async def __update_from_context(self, context: SpamContext):
  68. content = next(iter(context.messages)).content
  69. if len(context.messages) > self.ban_messages_per_user:
  70. if not context.is_banned:
  71. await context.member.ban(reason='Posting same message repeatedly', delete_message_days=1)
  72. msg = f'User {context.member.mention} auto banned for ' + \
  73. 'crosspost spamming.\n' + \
  74. '> {content}'
  75. if context.warning_message:
  76. await self.update_warn(context.warning_message, msg)
  77. await context.warning_message.clear_reaction('🗑')
  78. await context.warning_message.clear_reaction('👢')
  79. await context.warning_message.clear_reaction('🚫')
  80. else:
  81. self.warning_message = await self.warn(context.member.guild, msg)
  82. elif len(context.messages) > self.warn_messages_per_user:
  83. content = next(iter(context.messages)).content
  84. msg = f'User {context.member.mention} has posted the exact same ' + \
  85. f'message {len(context.messages)} times.\n' + \
  86. f'> {content}' + \
  87. '\n'
  88. can_delete = len(context.messages) > len(context.deleted_messages)
  89. if can_delete:
  90. msg += '\n🗑 to delete messages'
  91. else:
  92. msg += '\nAll messages deleted'
  93. if not context.is_kicked:
  94. msg += '\n👢 to kick user'
  95. elif not context.is_banned:
  96. msg += '\nUser kicked'
  97. if context.is_banned:
  98. msg += '\nUser banned'
  99. else:
  100. msg += '\n🚫 to ban user'
  101. if context.warning_message:
  102. await self.update_warn(context.warning_message, msg)
  103. else:
  104. context.warning_message = await self.warn(context.member.guild, msg)
  105. self.listen_for_reactions_to(context.warning_message)
  106. if can_delete:
  107. await context.warning_message.add_reaction('🗑')
  108. else:
  109. await context.warning_message.clear_reaction('🗑')
  110. if not context.is_kicked:
  111. await context.warning_message.add_reaction('👢')
  112. else:
  113. await context.warning_message.clear_reaction('👢')
  114. if not context.is_banned:
  115. await context.warning_message.add_reaction('🚫')
  116. else:
  117. await context.warning_message.clear_reaction('🚫')
  118. def __context_for_warning_message(self, message: Message) -> SpamContext:
  119. spam_lookup = Storage.get_state_value(message.guild, self.STATE_KEY_SPAM_CONTEXT)
  120. if spam_lookup is None:
  121. return
  122. for _, context in spam_lookup.items():
  123. if context.warning_message.id == message.id:
  124. return context
  125. return None
  126. async def on_mod_react(self, message: Message, emoji: PartialEmoji) -> None:
  127. context = self.__context_for_warning_message(message)
  128. if context is None:
  129. return
  130. if emoji.name == '🗑':
  131. await self.__delete_messages(context)
  132. elif emoji.name == '👢':
  133. await self.__kick(context)
  134. elif emoji.name == '🚫':
  135. await self.__ban(context)
  136. async def __delete_messages(self, context: SpamContext) -> None:
  137. for message in context.messages - context.deleted_messages:
  138. await message.delete()
  139. context.deleted_messages.add(message)
  140. await self.__update_from_context(context)
  141. async def __kick(self, context: SpamContext) -> None:
  142. await context.member.kick(reason='Posting same message repeatedly')
  143. context.is_kicked = True
  144. await self.__update_from_context(context)
  145. async def __ban(self, context: SpamContext) -> None:
  146. await context.member.ban(reason='Posting same message repeatedly', delete_message_days=1)
  147. context.deleted_messages |= context.messages
  148. context.is_kicked = True
  149. context.is_banned = True
  150. await self.__update_from_context(context)
  151. @commands.Cog.listener()
  152. async def on_message(self, message: Message):
  153. await self.__record_message(message)