Experimental Discord bot written in Python
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

patterncog.py 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. """
  2. Cog for matching messages against guild-configurable criteria and taking
  3. automated actions on them.
  4. """
  5. from discord import Guild, Member, Message, utils as discordutils
  6. from discord.ext import commands
  7. from config import CONFIG
  8. from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction
  9. from rocketbot.cogsetting import CogSetting
  10. from rocketbot.pattern import PatternCompiler, PatternDeprecationError, \
  11. PatternError, PatternStatement
  12. from rocketbot.storage import Storage
  13. class PatternContext:
  14. """
  15. Data about a message that has matched a configured statement and what
  16. actions have been carried out.
  17. """
  18. def __init__(self, message: Message, statement: PatternStatement):
  19. self.message = message
  20. self.statement = statement
  21. self.is_deleted = False
  22. self.is_kicked = False
  23. self.is_banned = False
  24. class PatternCog(BaseCog, name='Pattern Matching'):
  25. """
  26. Highly flexible cog for performing various actions on messages that match
  27. various critera. Patterns can be defined by mods for each guild.
  28. """
  29. SETTING_PATTERNS = CogSetting('patterns', None)
  30. def __get_patterns(self, guild: Guild) -> dict[str, PatternStatement]:
  31. """
  32. Returns a name -> PatternStatement lookup for the guild.
  33. """
  34. patterns: dict[str, PatternStatement] = Storage.get_state_value(guild,
  35. 'PatternCog.patterns')
  36. if patterns is None:
  37. jsons: list[dict] = self.get_guild_setting(guild, self.SETTING_PATTERNS)
  38. pattern_list: list[PatternStatement] = []
  39. for json in jsons:
  40. try:
  41. ps = PatternStatement.from_json(json)
  42. pattern_list.append(ps)
  43. try:
  44. ps.check_deprecations()
  45. except PatternDeprecationError as e:
  46. self.log(guild, f'Pattern {ps.name}: {e}')
  47. except PatternError as e:
  48. self.log(guild, f'Error decoding pattern "{json["name"]}": {e}')
  49. patterns = { p.name:p for p in pattern_list}
  50. Storage.set_state_value(guild, 'PatternCog.patterns', patterns)
  51. return patterns
  52. @classmethod
  53. def __save_patterns(cls,
  54. guild: Guild,
  55. patterns: dict[str, PatternStatement]) -> None:
  56. to_save: list[dict] = list(map(PatternStatement.to_json, patterns.values()))
  57. cls.set_guild_setting(guild, cls.SETTING_PATTERNS, to_save)
  58. @commands.Cog.listener()
  59. async def on_message(self, message: Message) -> None:
  60. 'Event listener'
  61. if message.author is None or \
  62. message.author.bot or \
  63. message.channel is None or \
  64. message.guild is None or \
  65. message.content is None or \
  66. message.content == '':
  67. return
  68. if message.author.permissions_in(message.channel).ban_members:
  69. # Ignore mods
  70. return
  71. patterns = self.__get_patterns(message.guild)
  72. for statement in patterns.values():
  73. if statement.expression.matches(message):
  74. await self.__trigger_actions(message, statement)
  75. break
  76. async def __trigger_actions(self,
  77. message: Message,
  78. statement: PatternStatement) -> None:
  79. context = PatternContext(message, statement)
  80. should_post_message = False
  81. message_type: int = BotMessage.TYPE_DEFAULT
  82. action_descriptions = []
  83. self.log(message.guild, f'Message from {message.author.name} matched ' + \
  84. f'pattern "{statement.name}"')
  85. for action in statement.actions:
  86. if action.action == 'ban':
  87. await message.author.ban(
  88. reason='Rocketbot: Message matched custom pattern named ' + \
  89. f'"{statement.name}"',
  90. delete_message_days=0)
  91. context.is_banned = True
  92. context.is_kicked = True
  93. action_descriptions.append('Author banned')
  94. self.log(message.guild, f'{message.author.name} banned')
  95. elif action.action == 'delete':
  96. await message.delete()
  97. context.is_deleted = True
  98. action_descriptions.append('Message deleted')
  99. self.log(message.guild, f'{message.author.name}\'s message deleted')
  100. elif action.action == 'kick':
  101. await message.author.kick(
  102. reason='Rocketbot: Message matched custom pattern named ' + \
  103. f'"{statement.name}"')
  104. context.is_kicked = True
  105. action_descriptions.append('Author kicked')
  106. self.log(message.guild, f'{message.author.name} kicked')
  107. elif action.action == 'modinfo':
  108. should_post_message = True
  109. message_type = BotMessage.TYPE_INFO
  110. action_descriptions.append('Message logged')
  111. elif action.action == 'modwarn':
  112. should_post_message = True
  113. message_type = BotMessage.TYPE_MOD_WARNING
  114. action_descriptions.append('Mods alerted')
  115. elif action.action == 'reply':
  116. await message.reply(
  117. f'{action.arguments[0]}',
  118. mention_author=False)
  119. action_descriptions.append('Autoreplied')
  120. self.log(message.guild, f'autoreplied to {message.author.name}')
  121. if should_post_message:
  122. bm = BotMessage(
  123. message.guild,
  124. f'User {message.author.name} tripped custom pattern ' + \
  125. f'`{statement.name}`.\n\nAutomatic actions taken:\n• ' + \
  126. ('\n• '.join(action_descriptions)),
  127. type=message_type,
  128. context=context)
  129. bm.quote = discordutils.remove_markdown(message.clean_content)
  130. await bm.set_reactions(BotMessageReaction.standard_set(
  131. did_delete=context.is_deleted,
  132. did_kick=context.is_kicked,
  133. did_ban=context.is_banned))
  134. await self.post_message(bm)
  135. async def on_mod_react(self,
  136. bot_message: BotMessage,
  137. reaction: BotMessageReaction,
  138. reacted_by: Member) -> None:
  139. context: PatternContext = bot_message.context
  140. if reaction.emoji == CONFIG['trash_emoji']:
  141. await context.message.delete()
  142. context.is_deleted = True
  143. elif reaction.emoji == CONFIG['kick_emoji']:
  144. await context.message.author.kick(
  145. reason='Rocketbot: Message matched custom pattern named ' + \
  146. f'"{context.statement.name}". Kicked by {reacted_by.name}.')
  147. context.is_kicked = True
  148. elif reaction.emoji == CONFIG['ban_emoji']:
  149. await context.message.author.ban(
  150. reason='Rocketbot: Message matched custom pattern named ' + \
  151. f'"{context.statement.name}". Banned by {reacted_by.name}.',
  152. delete_message_days=1)
  153. context.is_banned = True
  154. await bot_message.set_reactions(BotMessageReaction.standard_set(
  155. did_delete=context.is_deleted,
  156. did_kick=context.is_kicked,
  157. did_ban=context.is_banned))
  158. @commands.group(
  159. brief='Manages message pattern matching',
  160. )
  161. @commands.has_permissions(ban_members=True)
  162. @commands.guild_only()
  163. async def pattern(self, context: commands.Context):
  164. 'Message pattern matching command group'
  165. if context.invoked_subcommand is None:
  166. await context.send_help()
  167. @pattern.command(
  168. brief='Adds a custom pattern',
  169. description='Adds a custom pattern. Patterns use a simplified ' + \
  170. 'expression language. Full documentation found here: ' + \
  171. 'https://git.rixafrix.com/ialbert/python-app-rocketbot/src/' + \
  172. 'branch/master/patterns.md',
  173. usage='<pattern_name> <expression...>',
  174. ignore_extra=True
  175. )
  176. async def add(self, context: commands.Context, name: str):
  177. 'Command handler'
  178. pattern_str = PatternCompiler.expression_str_from_context(context, name)
  179. try:
  180. statement = PatternCompiler.parse_statement(name, pattern_str)
  181. statement.check_deprecations()
  182. patterns = self.__get_patterns(context.guild)
  183. patterns[name] = statement
  184. self.__save_patterns(context.guild, patterns)
  185. await context.message.reply(
  186. f'{CONFIG["success_emoji"]} Pattern `{name}` added.',
  187. mention_author=False)
  188. except PatternError as e:
  189. await context.message.reply(
  190. f'{CONFIG["failure_emoji"]} Error parsing statement. {e}',
  191. mention_author=False)
  192. @pattern.command(
  193. brief='Removes a custom pattern',
  194. usage='<pattern_name>'
  195. )
  196. async def remove(self, context: commands.Context, name: str):
  197. 'Command handler'
  198. patterns = self.__get_patterns(context.guild)
  199. if patterns.get(name) is not None:
  200. del patterns[name]
  201. self.__save_patterns(context.guild, patterns)
  202. await context.message.reply(
  203. f'{CONFIG["success_emoji"]} Pattern `{name}` deleted.',
  204. mention_author=False)
  205. else:
  206. await context.message.reply(
  207. f'{CONFIG["failure_emoji"]} No pattern named `{name}`.',
  208. mention_author=False)
  209. @pattern.command(
  210. brief='Lists all patterns'
  211. )
  212. async def list(self, context: commands.Context) -> None:
  213. 'Command handler'
  214. patterns = self.__get_patterns(context.guild)
  215. if len(patterns) == 0:
  216. await context.message.reply('No patterns defined.', mention_author=False)
  217. return
  218. msg = ''
  219. for name, statement in sorted(patterns.items()):
  220. msg += f'Pattern `{name}`:\n```\n{statement.original}\n```\n'
  221. await context.message.reply(msg, mention_author=False)