Experimental Discord bot written in Python
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

urlspamcog.py 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """
  2. Cog for detecting URLs posted by new users.
  3. """
  4. import re
  5. from datetime import timedelta
  6. from discord import Member, Message, utils as discordutils
  7. from discord.ext import commands
  8. from config import CONFIG
  9. from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
  10. from rocketbot.utils import describe_timedelta
  11. class URLSpamContext:
  12. """
  13. Data about a suspected spam message containing a URL.
  14. """
  15. def __init__(self, spam_message: Message):
  16. self.spam_message = spam_message
  17. self.is_deleted = False
  18. self.is_kicked = False
  19. self.is_banned = False
  20. class URLSpamCog(BaseCog, name='URL Spam'):
  21. """
  22. Detects users posting URLs who just joined recently: a common spam pattern.
  23. Can be configured to take immediate action or just warn the mods.
  24. """
  25. SETTING_ENABLED = CogSetting('enabled', bool,
  26. brief='URL spam detection',
  27. description='Whether URLs posted soon after joining are flagged.')
  28. SETTING_ACTION = CogSetting('action', str,
  29. brief='action to take on spam',
  30. description='The action to take on detected URL spam.',
  31. enum_values=set(['nothing', 'modwarn', 'delete', 'kick', 'ban']))
  32. SETTING_JOIN_AGE = CogSetting('joinage', float,
  33. brief='seconds since member joined',
  34. description='The minimum seconds since the user joined the ' + \
  35. 'server before they can post URLs. URLs posted by users ' + \
  36. 'who joined too recently will be flagged. Keep in mind ' + \
  37. 'many servers have a minimum 10 minute cooldown before ' + \
  38. 'new members can say anything. Setting to 0 effectively ' + \
  39. 'disables URL spam detection.',
  40. usage='<seconds:int>',
  41. min_value=0)
  42. def __init__(self, bot):
  43. super().__init__(bot)
  44. self.add_setting(URLSpamCog.SETTING_ENABLED)
  45. self.add_setting(URLSpamCog.SETTING_ACTION)
  46. self.add_setting(URLSpamCog.SETTING_JOIN_AGE)
  47. @commands.group(
  48. brief='Manages URL spam detection',
  49. )
  50. @commands.has_permissions(ban_members=True)
  51. @commands.guild_only()
  52. async def urlspam(self, context: commands.Context):
  53. 'URL spam command group'
  54. if context.invoked_subcommand is None:
  55. await context.send_help()
  56. @commands.Cog.listener()
  57. async def on_message(self, message: Message):
  58. 'Event listener'
  59. if message.author is None or \
  60. message.author.bot or \
  61. message.guild is None or \
  62. message.channel is None or \
  63. message.content is None:
  64. return
  65. if not self.get_guild_setting(message.guild, self.SETTING_ENABLED):
  66. return
  67. action = self.get_guild_setting(message.guild, self.SETTING_ACTION)
  68. join_seconds = self.get_guild_setting(message.guild, self.SETTING_JOIN_AGE)
  69. min_join_age = timedelta(seconds=join_seconds)
  70. if action == 'nothing':
  71. return
  72. if not self.__contains_url(message.content):
  73. return
  74. join_age = message.created_at - message.author.joined_at
  75. join_age_str = describe_timedelta(join_age)
  76. if join_age < min_join_age:
  77. context = URLSpamContext(message)
  78. needs_attention = False
  79. if action == 'modwarn':
  80. needs_attention = not self.was_warned_recently(message.author)
  81. self.log(message.guild, f'New user {message.author.name} ' + \
  82. f'({message.author.id}) posted URL {join_age_str} after ' + \
  83. 'joining.' + (' Mods alerted.' if needs_attention else ''))
  84. elif action == 'delete':
  85. await message.delete()
  86. context.is_deleted = True
  87. self.log(message.guild, f'New user {message.author.name} ' + \
  88. f'({message.author.id}) posted URL {join_age_str} after ' + \
  89. 'joining. Message deleted.')
  90. elif action == 'kick':
  91. await message.delete()
  92. context.is_deleted = True
  93. await message.author.kick(
  94. reason=f'Rocketbot: Posted a link {join_age_str} after joining')
  95. context.is_kicked = True
  96. self.log(message.guild, f'New user {message.author.name} ' + \
  97. f'({message.author.id}) posted URL {join_age_str} after ' + \
  98. 'joining. User kicked.')
  99. elif action == 'ban':
  100. await message.author.ban(
  101. reason=f'Rocketbot: User posted a link {join_age_str} after joining',
  102. delete_message_days=1)
  103. context.is_deleted = True
  104. context.is_kicked = True
  105. context.is_banned = True
  106. self.log(message.guild, f'New user {message.author.name} ' + \
  107. f'({message.author.id}) posted URL {join_age_str} after ' + \
  108. 'joining. User banned.')
  109. bm = BotMessage(
  110. message.guild,
  111. f'User {message.author.mention} posted a URL ' + \
  112. f'{join_age_str} after joining.',
  113. type = BotMessage.TYPE_MOD_WARNING if needs_attention else BotMessage.TYPE_INFO,
  114. context = context)
  115. bm.quote = discordutils.remove_markdown(message.clean_content)
  116. await bm.set_reactions(BotMessageReaction.standard_set(
  117. did_delete=context.is_deleted,
  118. did_kick=context.is_kicked,
  119. did_ban=context.is_banned))
  120. await self.post_message(bm)
  121. if needs_attention:
  122. self.record_warning(message.author)
  123. async def on_mod_react(self,
  124. bot_message: BotMessage,
  125. reaction: BotMessageReaction,
  126. reacted_by: Member) -> None:
  127. context: URLSpamContext = bot_message.context
  128. if context is None:
  129. return
  130. sm: Message = context.spam_message
  131. if reaction.emoji == CONFIG['trash_emoji']:
  132. if not context.is_deleted:
  133. await sm.delete()
  134. context.is_deleted = True
  135. self.log(sm.guild, f'URL spam by {sm.author.name} deleted ' + \
  136. f'by {reacted_by.name}')
  137. elif reaction.emoji == CONFIG['kick_emoji']:
  138. if not context.is_deleted:
  139. await sm.delete()
  140. context.is_deleted = True
  141. if not context.is_kicked:
  142. await sm.author.kick(
  143. reason=f'Rocketbot: Kicked for URL spam by {reacted_by.name}')
  144. context.is_kicked = True
  145. self.log(sm.guild, f'URL spammer {sm.author.name} kicked ' + \
  146. f'by {reacted_by.name}')
  147. elif reaction.emoji == CONFIG['ban_emoji']:
  148. if not context.is_banned:
  149. await sm.author.ban(
  150. reason=f'Rocketbot: Banned for URL spam by {reacted_by.name}',
  151. delete_message_days=1)
  152. context.is_deleted = True
  153. context.is_kicked = True
  154. context.is_banned = True
  155. self.log(sm.guild, f'URL spammer {sm.author.name} banned ' + \
  156. f'by {reacted_by.name}')
  157. else:
  158. return
  159. await bot_message.set_reactions(BotMessageReaction.standard_set(
  160. did_delete=context.is_deleted,
  161. did_kick=context.is_kicked,
  162. did_ban=context.is_banned))
  163. @classmethod
  164. def __contains_url(cls, text: str) -> bool:
  165. p = re.compile(r'http(?:s)?://[^\s]+')
  166. return p.search(text) is not None