Parcourir la source

Adding lastmatched so patterns can have cooldowns

master
Rocketsoup il y a 4 ans
Parent
révision
b4f5ebcc42
3 fichiers modifiés avec 49 ajouts et 23 suppressions
  1. 7
    6
      patterns.md
  2. 21
    1
      rocketbot/cogs/patterncog.py
  3. 21
    16
      rocketbot/pattern.py

+ 7
- 6
patterns.md Voir le fichier

51
 
51
 
52
 #### Fields
52
 #### Fields
53
 
53
 
54
-* `content.plain` - The plain text of the message. All markdown formatting is removed, and mentions look like the `@Username` text name that gets displayed.
55
-* `content.markdown` - The raw markdown of the message. This contains all markdown characters, and mentions are of the `<@!0000000>` form. Available operators: `==`, `!=`, `contains`, `!contains`, `matches`, `!matches`. Comparison value must be a quoted string.
56
 * `author` - Who sent the message. Available operators: `==`, `!=`. Comparison value must be a user mention (an @ that Discord will tab-complete for you).
54
 * `author` - Who sent the message. Available operators: `==`, `!=`. Comparison value must be a user mention (an @ that Discord will tab-complete for you).
57
 * `author.id` - The numeric ID of the user who sent the message. Available operators: `==`, `!=`. Comparison value must be a numeric user ID.
55
 * `author.id` - The numeric ID of the user who sent the message. Available operators: `==`, `!=`. Comparison value must be a numeric user ID.
58
-* `author.name` - The username of the author. Available operators: `==`, `!=`, `contains`, `!contains`, `containsword`, `!containsword`, `matches`, `!matches`. Comparison value must be a quoted string.
59
 * `author.joinage` - How much time has elapsed from when the author joined and when the message was sent. If the user has joined and left multiple times this is the most recent join time. Available operators: `==`, `!=`, `<`, `>`, `<=`, `>=`. Comparison value must be a timespan (see below)
56
 * `author.joinage` - How much time has elapsed from when the author joined and when the message was sent. If the user has joined and left multiple times this is the most recent join time. Available operators: `==`, `!=`, `<`, `>`, `<=`, `>=`. Comparison value must be a timespan (see below)
57
+* `author.name` - The username of the author. Available operators: `==`, `!=`, `contains`, `!contains`, `containsword`, `!containsword`, `matches`, `!matches`. Comparison value must be a quoted string.
58
+* `content.plain` - The plain text of the message. All markdown formatting is removed, and mentions look like the `@Username` text name that gets displayed.
59
+* `content.markdown` - The raw markdown of the message. This contains all markdown characters, and mentions are of the `<@!0000000>` form. Available operators: `==`, `!=`, `contains`, `!contains`, `matches`, `!matches`. Comparison value must be a quoted string.
60
+* `lastmatched` - How long ago this pattern matched a message. Used for imposing a cooldown, especially for autoresponses. If the pattern has never matched it will be treated as if it last ran 100 years ago for comparison purposes. Available operators: `==`, `!=`, `<`, `>`, `<=`, `>=`. Comparison value must be a timespan (see below).
60
 
61
 
61
 #### Operators
62
 #### Operators
62
 
63
 
139
 
140
 
140
 `<actions>` ::= `<action>` | `<action>` " " `<actions>`
141
 `<actions>` ::= `<action>` | `<action>` " " `<actions>`
141
 
142
 
142
-`<action>` ::= "ban" | "delete" | "kick" | "modwarn" | "reply " `<quoted_string>`
143
+`<action>` ::= "ban" | "delete" | "kick" | "modinfo" | "modwarn" | "reply " `<quoted_string>`
143
 
144
 
144
 `<quoted_string>` ::= '"' `<any>` '"'
145
 `<quoted_string>` ::= '"' `<any>` '"'
145
 
146
 
157
 
158
 
158
 `<or_expr>` ::= `<expression>` " or " `<expression>`
159
 `<or_expr>` ::= `<expression>` " or " `<expression>`
159
 
160
 
160
-`<field_name>` ::= "content.plain" | "content.markdown" | "author" | "author.id" | "author.name" | "author.joinage"
161
+`<field_name>` ::= "author" | "author.id" | "author.joinage" | "author.name" | "content.markdown" | "content.plain" | "lastmatched"
161
 
162
 
162
-`<op>` ::= "==" | "!=" | "<" | ">" | "<=" | ">=" | "contains" | "!contains" | "matches" | "!matches"
163
+`<op>` ::= "==" | "!=" | "<" | ">" | "<=" | ">=" | "contains" | "!contains" | "containsword" | "!containsword" | "matches" | "!matches"
163
 
164
 
164
 `<value>` ::= `<int>` | `<float>` | `<quoted_string>` | `<timespan>` | `<mention>`
165
 `<value>` ::= `<int>` | `<float>` | `<quoted_string>` | `<timespan>` | `<mention>`
165
 
166
 

+ 21
- 1
rocketbot/cogs/patterncog.py Voir le fichier

2
 Cog for matching messages against guild-configurable criteria and taking
2
 Cog for matching messages against guild-configurable criteria and taking
3
 automated actions on them.
3
 automated actions on them.
4
 """
4
 """
5
+from datetime import datetime
5
 from discord import Guild, Member, Message, utils as discordutils
6
 from discord import Guild, Member, Message, utils as discordutils
6
 from discord.ext import commands
7
 from discord.ext import commands
7
 
8
 
62
 		to_save: list[dict] = list(map(PatternStatement.to_json, patterns.values()))
63
 		to_save: list[dict] = list(map(PatternStatement.to_json, patterns.values()))
63
 		cls.set_guild_setting(guild, cls.SETTING_PATTERNS, to_save)
64
 		cls.set_guild_setting(guild, cls.SETTING_PATTERNS, to_save)
64
 
65
 
66
+	@classmethod
67
+	def __get_last_matched(cls, guild: Guild, name: str) -> datetime:
68
+		last_matched: dict[name, datetime] = Storage.get_state_value(guild, 'PatternCog.last_matched')
69
+		if last_matched:
70
+			return last_matched.get(name)
71
+		return None
72
+
73
+	@classmethod
74
+	def __set_last_matched(cls, guild: Guild, name: str, time: datetime) -> None:
75
+		last_matched: dict[name, datetime] = Storage.get_state_value(guild, 'PatternCog.last_matched')
76
+		if last_matched is None:
77
+			last_matched = {}
78
+			Storage.set_state_value(guild, 'PatternCog.last_matched', last_matched)
79
+		last_matched[name] = time
80
+
65
 	@commands.Cog.listener()
81
 	@commands.Cog.listener()
66
 	async def on_message(self, message: Message) -> None:
82
 	async def on_message(self, message: Message) -> None:
67
 		'Event listener'
83
 		'Event listener'
78
 
94
 
79
 		patterns = self.__get_patterns(message.guild)
95
 		patterns = self.__get_patterns(message.guild)
80
 		for statement in sorted(patterns.values(), key=lambda s : s.priority, reverse=True):
96
 		for statement in sorted(patterns.values(), key=lambda s : s.priority, reverse=True):
81
-			if statement.expression.matches(message):
97
+			other_fields = {
98
+				'last_matched': self.__get_last_matched(message.guild, statement.name),
99
+			}
100
+			if statement.expression.matches(message, other_fields):
101
+				self.__set_last_matched(message.guild, statement.name, message.created_at)
82
 				await self.__trigger_actions(message, statement)
102
 				await self.__trigger_actions(message, statement)
83
 				break
103
 				break
84
 
104
 

+ 21
- 16
rocketbot/pattern.py Voir le fichier

4
 """
4
 """
5
 import re
5
 import re
6
 from abc import ABCMeta, abstractmethod
6
 from abc import ABCMeta, abstractmethod
7
+from datetime import datetime
7
 from typing import Any
8
 from typing import Any
8
 
9
 
9
 from discord import Message, utils as discordutils
10
 from discord import Message, utils as discordutils
42
 		pass
43
 		pass
43
 
44
 
44
 	@abstractmethod
45
 	@abstractmethod
45
-	def matches(self, message: Message) -> bool:
46
+	def matches(self, message: Message, other_fields: dict[str: Any]) -> bool:
46
 		"""
47
 		"""
47
-		Whether a message matches this expression.
48
+		Whether a message matches this expression. other_fields are additional
49
+		fields that can be queried not contained in the message itself.
48
 		"""
50
 		"""
49
 		return False
51
 		return False
50
 
52
 
59
 		self.operator = operator
61
 		self.operator = operator
60
 		self.value = value
62
 		self.value = value
61
 
63
 
62
-	def __field_value(self, message: Message) -> Any:
64
+	def __field_value(self, message: Message, other_fields: dict[str: Any]) -> Any:
63
 		if self.field in ('content.markdown', 'content'):
65
 		if self.field in ('content.markdown', 'content'):
64
 			return message.content
66
 			return message.content
65
 		if self.field == 'content.plain':
67
 		if self.field == 'content.plain':
72
 			return message.created_at - message.author.joined_at
74
 			return message.created_at - message.author.joined_at
73
 		if self.field == 'author.name':
75
 		if self.field == 'author.name':
74
 			return message.author.name
76
 			return message.author.name
75
-		else:
76
-			raise ValueError(f'Bad field name {self.field}')
77
-
78
-	def matches(self, message: Message) -> bool:
79
-		field_value = self.__field_value(message)
77
+		if self.field == 'lastmatched':
78
+			long_ago = datetime(year=1900, month=1, day=1, hour=0, minute=0, second=0)
79
+			last_matched = other_fields.get('last_matched') or long_ago
80
+			return message.created_at - last_matched
81
+		raise ValueError(f'Bad field name {self.field}')
82
+
83
+	def matches(self, message: Message, other_fields: dict[str, Any]) -> bool:
84
+		field_value = self.__field_value(message, other_fields)
80
 		if self.operator == '==':
85
 		if self.operator == '==':
81
 			if isinstance(field_value, str) and isinstance(self.value, str):
86
 			if isinstance(field_value, str) and isinstance(self.value, str):
82
 				return field_value.lower() == self.value.lower()
87
 				return field_value.lower() == self.value.lower()
116
 		self.operator = operator
121
 		self.operator = operator
117
 		self.operands = list(operands)
122
 		self.operands = list(operands)
118
 
123
 
119
-	def matches(self, message: Message) -> bool:
124
+	def matches(self, message: Message, other_fields: dict[str, Any]) -> bool:
120
 		if self.operator == '!':
125
 		if self.operator == '!':
121
-			return not self.operands[0].matches(message)
126
+			return not self.operands[0].matches(message, other_fields)
122
 		if self.operator == 'and':
127
 		if self.operator == 'and':
123
 			for op in self.operands:
128
 			for op in self.operands:
124
-				if not op.matches(message):
129
+				if not op.matches(message, other_fields):
125
 					return False
130
 					return False
126
 			return True
131
 			return True
127
 		if self.operator == 'or':
132
 		if self.operator == 'or':
128
 			for op in self.operands:
133
 			for op in self.operands:
129
-				if op.matches(message):
134
+				if op.matches(message, other_fields):
130
 					return True
135
 					return True
131
 			return False
136
 			return False
132
 		raise ValueError(f'Bad operator "{self.operator}"')
137
 		raise ValueError(f'Bad operator "{self.operator}"')
208
 	TYPE_TIMESPAN = 'timespan'
213
 	TYPE_TIMESPAN = 'timespan'
209
 
214
 
210
 	FIELD_TO_TYPE: dict[str, str] = {
215
 	FIELD_TO_TYPE: dict[str, str] = {
211
-		'content.plain': TYPE_TEXT,
212
-		'content.markdown': TYPE_TEXT,
213
 		'author': TYPE_MEMBER,
216
 		'author': TYPE_MEMBER,
214
 		'author.id': TYPE_ID,
217
 		'author.id': TYPE_ID,
215
-		'author.name': TYPE_TEXT,
216
 		'author.joinage': TYPE_TIMESPAN,
218
 		'author.joinage': TYPE_TIMESPAN,
217
-
219
+		'author.name': TYPE_TEXT,
218
 		'content': TYPE_TEXT, # deprecated, use content.markdown or content.plain
220
 		'content': TYPE_TEXT, # deprecated, use content.markdown or content.plain
221
+		'content.markdown': TYPE_TEXT,
222
+		'content.plain': TYPE_TEXT,
223
+		'lastmatched': TYPE_TIMESPAN,
219
 	}
224
 	}
220
 	DEPRECATED_FIELDS: set[str] = set([ 'content' ])
225
 	DEPRECATED_FIELDS: set[str] = set([ 'content' ])
221
 
226
 

Chargement…
Annuler
Enregistrer