Experimental Discord bot written in Python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. from discord import Guild, Message
  2. from discord.ext import commands
  3. from datetime import timedelta
  4. from cogs.basecog import BaseCog, BotMessage
  5. from storage import Storage
  6. class Criterion:
  7. def __init__(self, type, **kwargs):
  8. self.type = type
  9. if type == 'contains':
  10. text = kwargs['text']
  11. self.text = text
  12. self.test = lambda m : text.lower() in m.content.lower()
  13. elif type == 'joinage':
  14. min = kwargs['min']
  15. self.min = min
  16. self.test = lambda m : m.created_at - m.author.joined_at < min
  17. else:
  18. raise RuntimeError(f'Unknown criterion type "{type}"')
  19. def matches(self, message: Message) -> bool:
  20. return self.test(message)
  21. @classmethod
  22. def decode(cls, val: dict):
  23. type = val['type']
  24. if type == 'contains':
  25. return Criterion(type, text=val['text'])
  26. elif type == 'joinage':
  27. return Criterion(type, min=timedelta(seconds=val['min']))
  28. class Pattern:
  29. def __init__(self, criteria: list, action: str, must_match_all: bool = True):
  30. self.criteria = criteria
  31. self.action = action
  32. self.must_match_all = must_match_all
  33. def matches(self, message: Message) -> bool:
  34. for criterion in self.criteria:
  35. crit_matches = criterion.matches(message)
  36. if crit_matches and not self.must_match_all:
  37. return True
  38. if not crit_matches and self.must_match_all:
  39. return False
  40. return self.must_match_all
  41. @classmethod
  42. def decode(cls, val: dict):
  43. match_all = val.get('must_match_all')
  44. action = val.get('action')
  45. encoded_criteria = val.get('criteria')
  46. criteria = []
  47. for ec in encoded_criteria:
  48. criteria.append(Criterion.decode(ec))
  49. return Pattern(criteria, action, match_all if isinstance(match_all, bool) else True)
  50. class PatternCog(BaseCog):
  51. def __init__(self, bot):
  52. super().__init__(bot)
  53. def __patterns(self, guild: Guild) -> list:
  54. patterns = Storage.get_state_value(guild, 'pattern_patterns')
  55. if patterns is None:
  56. patterns_encoded = Storage.get_config_value(guild, 'pattern_patterns')
  57. if patterns_encoded:
  58. patterns = []
  59. for pe in patterns_encoded:
  60. patterns.append(Pattern.decode(pe))
  61. Storage.set_state_value(guild, 'pattern_patterns', patterns)
  62. return patterns
  63. @commands.Cog.listener()
  64. async def on_message(self, message: Message) -> None:
  65. if message.author is None or \
  66. message.author.bot or \
  67. message.channel is None or \
  68. message.guild is None or \
  69. message.content is None or \
  70. message.content == '':
  71. return
  72. if message.author.permissions_in(message.channel).ban_members:
  73. # Ignore mods
  74. return
  75. patterns = self.__patterns(message.guild)
  76. for pattern in patterns:
  77. if pattern.matches(message):
  78. text = None
  79. if pattern.action == 'delete':
  80. await message.delete()
  81. text = f'Message from {message.author.mention} matched ' + \
  82. 'banned pattern. Deleted.'
  83. self.log(message.guild, 'Message matched pattern. Deleted.')
  84. elif pattern.action == 'kick':
  85. await message.delete()
  86. await message.author.kick(reason='Rocketbot: Message matched banned pattern')
  87. text = f'Message from {message.author.mention} matched ' + \
  88. 'banned pattern. Message deleted and user kicked.'
  89. self.log(message.guild, 'Message matched pattern. Kicked user.')
  90. elif pattern.action == 'ban':
  91. await message.delete()
  92. await message.author.ban(reason='Rocketbot: Message matched banned pattern')
  93. text = f'Message from {message.author.mention} matched ' + \
  94. 'banned pattern. Message deleted and user banned.'
  95. self.log(message.guild, 'Message matched pattern. Banned user.')
  96. if text:
  97. m = BotMessage(message.guild,
  98. text = msg,
  99. type = BotMessage.TYPE_MOD_WARNING)
  100. m.quote = message.content
  101. await self.post_message(m)
  102. break
  103. """
  104. Expression language samples:
  105. content contains "poop"
  106. content contains "poop" and content contains "tinkle"
  107. joinage < 600s
  108. (content contains "this" and content contains "that") or content contains "whatever"
  109. <field> <op> <value>
  110. Fields:
  111. content
  112. author.id
  113. author.name
  114. author.joinage
  115. Ops:
  116. ==
  117. !=
  118. <
  119. >
  120. <=
  121. >=
  122. contains, !contains -- plain strings
  123. matches, !matches -- regexes
  124. Value types:
  125. timedelta (600, 600s, 10m, 5m30s)
  126. number
  127. string
  128. regex
  129. mention
  130. """
  131. @commands.group(
  132. brief='Manages message pattern matching',
  133. )
  134. @commands.has_permissions(ban_members=True)
  135. @commands.guild_only()
  136. async def patterns(self, context: commands.Context):
  137. 'Message pattern matching'
  138. if context.invoked_subcommand is None:
  139. await context.send_help()
  140. @patterns.command()
  141. async def addpattern(self, context: commands.Context, name: str, expression: str, *args):
  142. print(f'Pattern name: {name}')
  143. tokens = []
  144. tokens.append(expression)
  145. tokens += args
  146. print('Expression: ' + (' '.join(tokens)))