Просмотр исходного кода

Bot messages broken up into multiple smaller ones when long. More linter fixes.

master
Rocketsoup 2 месяцев назад
Родитель
Сommit
c379e06c6a
6 измененных файлов: 146 добавлений и 83 удалений
  1. 91
    43
      rocketbot/botmessage.py
  2. 3
    6
      rocketbot/cogs/basecog.py
  3. 22
    18
      rocketbot/collections.py
  4. 14
    14
      rocketbot/pattern.py
  5. 14
    0
      rocketbot/storage.py
  6. 2
    2
      rocketbot/utils.py

+ 91
- 43
rocketbot/botmessage.py Просмотреть файл

5
 from typing import Any, Optional, Union
5
 from typing import Any, Optional, Union
6
 
6
 
7
 from datetime import datetime
7
 from datetime import datetime
8
+
8
 from discord import Guild, Message, PartialEmoji, TextChannel
9
 from discord import Guild, Message, PartialEmoji, TextChannel
9
 
10
 
10
 from config import CONFIG
11
 from config import CONFIG
142
 		self.context: Optional[Any] = context
143
 		self.context: Optional[Any] = context
143
 		self.quote: Optional[str] = None
144
 		self.quote: Optional[str] = None
144
 		self.source_cog = None  # Set by `BaseCog.post_message()`
145
 		self.source_cog = None  # Set by `BaseCog.post_message()`
145
-		self.__posted_text: str = None  # last text posted, to test for changes
146
+		self.__posted_text: list[str] = []  # last text posted, to test for changes
146
 		self.__posted_emoji: set[str] = set()  # last emoji list posted
147
 		self.__posted_emoji: set[str] = set()  # last emoji list posted
147
-		self.__message: Optional[Message] = None  # set once the message has been posted
148
+		self.__messages: list[Message] = []  # set once the message has been posted
148
 		self.__reply_to: Optional[Message] = reply_to
149
 		self.__reply_to: Optional[Message] = reply_to
149
 		self.__suppress_embeds = suppress_embeds
150
 		self.__suppress_embeds = suppress_embeds
150
 		self.__reactions: list[BotMessageReaction] = []
151
 		self.__reactions: list[BotMessageReaction] = []
155
 		continue returning False even after calling BaseCog.post_message if
156
 		continue returning False even after calling BaseCog.post_message if
156
 		the guild has no configured warning channel.
157
 		the guild has no configured warning channel.
157
 		"""
158
 		"""
158
-		return self.__message is not None
159
+		return len(self.__messages) > 0
159
 
160
 
160
-	def message_id(self) -> Optional[int]:
161
-		'Returns the Message id or None if not sent.'
162
-		return self.__message.id if self.__message else None
161
+	def message_ids(self) -> list[int]:
162
+		"""Returns the ids of all actual Messages sent. One bot message may be
163
+		broken into multiple Discord messages."""
164
+		return [ m.id for m in self.__messages ]
163
 
165
 
164
 	def message_sent_at(self) -> Optional[datetime]:
166
 	def message_sent_at(self) -> Optional[datetime]:
165
-		'Returns when the message was sent or None if not sent.'
166
-		return self.__message.created_at if self.__message else None
167
+		"""Returns when the message was sent or None if not sent."""
168
+		return self.__messages[0].created_at if len(self.__messages) > 0 else None
167
 
169
 
168
 	def has_reactions(self) -> bool:
170
 	def has_reactions(self) -> bool:
169
-		'Whether this message has any reactions defined.'
171
+		"""Whether this message has any reactions defined."""
170
 		return len(self.__reactions) > 0
172
 		return len(self.__reactions) > 0
171
 
173
 
172
 	async def set_text(self, new_text: str) -> None:
174
 	async def set_text(self, new_text: str) -> None:
222
 		"""
224
 		"""
223
 		for i, existing in enumerate(self.__reactions):
225
 		for i, existing in enumerate(self.__reactions):
224
 			if (isinstance(reaction_or_emoji, str) and existing.emoji == reaction_or_emoji) or \
226
 			if (isinstance(reaction_or_emoji, str) and existing.emoji == reaction_or_emoji) or \
225
-				(isinstance(reaction_or_emoji, BotMessageReaction) and \
227
+				(isinstance(reaction_or_emoji, BotMessageReaction) and
226
 					existing.emoji == reaction_or_emoji.emoji):
228
 					existing.emoji == reaction_or_emoji.emoji):
227
 				self.__reactions.pop(i)
229
 				self.__reactions.pop(i)
228
 				await self.update_if_sent()
230
 				await self.update_if_sent()
246
 		the guild, otherwise does nothing. Does not need to be called by
248
 		the guild, otherwise does nothing. Does not need to be called by
247
 		BaseCog subclasses.
249
 		BaseCog subclasses.
248
 		"""
250
 		"""
249
-		if self.__message:
251
+		if len(self.__messages) > 0:
250
 			await self.update()
252
 			await self.update()
251
 
253
 
252
 	async def update(self) -> None:
254
 	async def update(self) -> None:
254
 		Sends or updates an already sent message based on BotMessage state.
256
 		Sends or updates an already sent message based on BotMessage state.
255
 		Does not need to be called by BaseCog subclasses.
257
 		Does not need to be called by BaseCog subclasses.
256
 		"""
258
 		"""
257
-		content: str = self.__formatted_message()
258
-		if self.__message:
259
-			if content != self.__posted_text:
260
-				await self.__message.edit(content=content)
261
-				self.__posted_text = content
262
-		else:
263
-			if self.__reply_to:
264
-				self.__message = await self.__reply_to.reply(content=content, mention_author=False)
265
-				self.__posted_text = content
266
-			else:
267
-				channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
268
-				if channel_id is None:
269
-					bot_log(self.guild, \
270
-						type(self.source_cog) if self.source_cog else None, \
271
-						'\u0007No warning channel set! No warning issued.')
272
-					return
273
-				channel: TextChannel = self.guild.get_channel(channel_id)
274
-				if channel is None:
275
-					channel = await self.guild.fetch_channel(channel_id)
276
-				if channel is None:
277
-					bot_log(self.guild, \
278
-						type(self.source_cog) if self.source_cog else None, \
279
-						f'\u0007Configured warning channel does not exist for guild {self.guild.name} ({self.guild.id})!')
280
-					return
281
-				self.__message = await channel.send(content=content, suppress_embeds=self.__suppress_embeds)
282
-				self.__posted_text = content
259
+		message_bodies: list[str] = self.__formatted_message()
260
+		if len(self.__messages) > 0:
261
+			if message_bodies != self.__posted_text:
262
+				while len(self.__messages) > len(message_bodies):
263
+					last_message = self.__messages.pop(-1)
264
+					await last_message.delete()
265
+					del Storage.get_bot_messages(self.guild)[last_message.id]
266
+				for i in range(min(len(message_bodies), len(self.__messages))):
267
+					await self.__messages[i].edit(content=message_bodies[i])
268
+				while len(self.__messages) < len(message_bodies):
269
+					body = message_bodies[len(self.__messages)]
270
+					message = await self.__messages[0].channel.send(content=body, suppress_embeds=self.__suppress_embeds)
271
+					Storage.get_bot_messages(self.guild)[message.id] = self
272
+					self.__messages.append(message)
273
+				self.__posted_text = message_bodies
274
+		else: # No messages posted yet
275
+			channel: Optional[TextChannel] = None
276
+			for index, body in enumerate(message_bodies):
277
+				if index == 0 and self.__reply_to:
278
+					message = await self.__reply_to.reply(content=body, mention_author=False)
279
+					Storage.get_bot_messages(self.guild)[message.id] = self
280
+					self.__messages.append(message)
281
+					channel = self.__reply_to.channel
282
+				else:
283
+					if channel is None:
284
+						channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
285
+						if channel_id is None:
286
+							bot_log(self.guild,
287
+								type(self.source_cog) if self.source_cog else None,
288
+								'\u0007No warning channel set! No warning issued.')
289
+							return
290
+						channel: TextChannel = self.guild.get_channel(channel_id) or await self.guild.fetch_channel(channel_id)
291
+						if channel is None:
292
+							bot_log(self.guild,
293
+								type(self.source_cog) if self.source_cog else None,
294
+								f'\u0007Configured warning channel does not exist for guild {self.guild.name} ({self.guild.id})!')
295
+							return
296
+					message = await channel.send(content=body, suppress_embeds=self.__suppress_embeds)
297
+					Storage.get_bot_messages(self.guild)[message.id] = self
298
+					self.__messages.append(message)
299
+			self.__posted_text = message_bodies
300
+
283
 		emoji_to_remove = self.__posted_emoji.copy()
301
 		emoji_to_remove = self.__posted_emoji.copy()
284
 		for reaction in self.__reactions:
302
 		for reaction in self.__reactions:
285
 			if reaction.is_enabled:
303
 			if reaction.is_enabled:
286
 				if reaction.emoji not in self.__posted_emoji:
304
 				if reaction.emoji not in self.__posted_emoji:
287
-					await self.__message.add_reaction(reaction.emoji)
305
+					await self.__messages[-1].add_reaction(reaction.emoji)
288
 					self.__posted_emoji.add(reaction.emoji)
306
 					self.__posted_emoji.add(reaction.emoji)
289
 				if reaction.emoji in emoji_to_remove:
307
 				if reaction.emoji in emoji_to_remove:
290
 					emoji_to_remove.remove(reaction.emoji)
308
 					emoji_to_remove.remove(reaction.emoji)
291
 		for emoji in emoji_to_remove:
309
 		for emoji in emoji_to_remove:
292
-			await self.__message.clear_reaction(emoji)
310
+			await self.__messages[-1].clear_reaction(emoji)
293
 			if emoji in self.__posted_emoji:
311
 			if emoji in self.__posted_emoji:
294
 				self.__posted_emoji.remove(emoji)
312
 				self.__posted_emoji.remove(emoji)
295
 
313
 
296
-	def __formatted_message(self) -> str:
314
+	def __formatted_message(self) -> list[str]:
297
 		"""
315
 		"""
298
-		Composes the entire message markdown from components. Includes the main
299
-		message, quoted text, summary of available reactions, etc.
316
+		Composes the entire message Markdown from components. Includes the main
317
+		message, quoted text, summary of available reactions, etc. Returned as
318
+		array of message bodies small enough to fit in a Discord message.
300
 		"""
319
 		"""
301
 		s: str = ''
320
 		s: str = ''
302
 
321
 
328
 				else:
347
 				else:
329
 					s += f'\n     {reaction.description}'
348
 					s += f'\n     {reaction.description}'
330
 
349
 
331
-		return s
350
+		# API complains if *request* is >2000. Unsure how much of that payload is overhead, so will define a max length
351
+		# conservatively of 85-90% of that.
352
+		ideal_message_length = 1700
353
+		max_message_length = 1800
354
+		# First time cutting up message
355
+		bodies = [s]
356
+		while len(bodies[-1]) > max_message_length:
357
+			# Try to cut at last newline before length limit, otherwise last space, otherwise hard cut at limit
358
+			last_body = bodies.pop(-1)
359
+			cut_before = ideal_message_length
360
+			cut_after = ideal_message_length
361
+			last_newline_index = last_body.rfind('\n', max_message_length // 2, max_message_length)
362
+			if last_newline_index >= 0:
363
+				cut_before = last_newline_index
364
+				cut_after = last_newline_index + 1
365
+			else:
366
+				last_space_index = last_body.rfind(' ', max_message_length // 2, max_message_length)
367
+				if last_space_index >= 0:
368
+					cut_before = last_space_index
369
+					cut_after = last_space_index + 1
370
+			body = last_body[:cut_before].strip()
371
+			remainder = last_body[cut_after:].strip()
372
+			while body.endswith('\n>'):
373
+				body = body[:-2]
374
+			while remainder.endswith('\n>'):
375
+				remainder = remainder[:-2]
376
+			bodies.append(body)
377
+			bodies.append(remainder)
378
+
379
+		return bodies

+ 3
- 6
rocketbot/cogs/basecog.py Просмотреть файл

157
 	# Bot message handling
157
 	# Bot message handling
158
 
158
 
159
 	@classmethod
159
 	@classmethod
160
-	def __bot_messages(cls, guild: Guild) -> AgeBoundDict:
160
+	def __bot_messages(cls, guild: Guild) -> AgeBoundDict[int, BotMessage]:
161
 		bm = Storage.get_state_value(guild, 'bot_messages')
161
 		bm = Storage.get_state_value(guild, 'bot_messages')
162
 		if bm is None:
162
 		if bm is None:
163
 			far_future = datetime.now(timezone.utc) + timedelta(days=1000)
163
 			far_future = datetime.now(timezone.utc) + timedelta(days=1000)
174
 		"""
174
 		"""
175
 		message.source_cog = self
175
 		message.source_cog = self
176
 		await message.update()
176
 		await message.update()
177
-		if message.has_reactions() and message.is_sent():
178
-			guild_messages = self.__bot_messages(message.guild)
179
-			guild_messages[message.message_id()] = message
180
 		return message.is_sent()
177
 		return message.is_sent()
181
 
178
 
182
 	@commands.Cog.listener()
179
 	@commands.Cog.listener()
183
 	async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
180
 	async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
184
-		'Event handler'
181
+		"""Event handler"""
185
 		# Avoid any unnecessary requests. Gets called for every reaction
182
 		# Avoid any unnecessary requests. Gets called for every reaction
186
 		# multiplied by every active cog.
183
 		# multiplied by every active cog.
187
 		if payload.user_id == self.bot.user.id:
184
 		if payload.user_id == self.bot.user.id:
193
 			# Possibly a DM
190
 			# Possibly a DM
194
 			return
191
 			return
195
 
192
 
196
-		guild_messages = self.__bot_messages(guild)
193
+		guild_messages: dict[int, BotMessage] = Storage.get_bot_messages(guild)
197
 		bot_message = guild_messages.get(payload.message_id)
194
 		bot_message = guild_messages.get(payload.message_id)
198
 		if bot_message is None:
195
 		if bot_message is None:
199
 			# Unknown message (expired or was never tracked)
196
 			# Unknown message (expired or was never tracked)

+ 22
- 18
rocketbot/collections.py Просмотреть файл

3
 """
3
 """
4
 
4
 
5
 from abc import ABCMeta, abstractmethod
5
 from abc import ABCMeta, abstractmethod
6
+from typing import Generic, TypeVar
6
 
7
 
7
 # Abstract collections
8
 # Abstract collections
8
 
9
 
9
-class AbstractMutableList(list, metaclass=ABCMeta):
10
+K = TypeVar('K')
11
+V = TypeVar('V')
12
+
13
+class AbstractMutableList(list[V], Generic[V], metaclass=ABCMeta):
10
 	"""
14
 	"""
11
 	Abstract list with hooks for custom logic before and after mutation
15
 	Abstract list with hooks for custom logic before and after mutation
12
 	operations.
16
 	operations.
86
 		self.post_mutate()
90
 		self.post_mutate()
87
 		return ret_val
91
 		return ret_val
88
 
92
 
89
-class AbstractMutableSet(set, metaclass=ABCMeta):
93
+class AbstractMutableSet(set[V], Generic[V], metaclass=ABCMeta):
90
 	"""
94
 	"""
91
 	Abstract set with hooks for custom logic before and after mutation
95
 	Abstract set with hooks for custom logic before and after mutation
92
 	operations.
96
 	operations.
171
 		self.post_mutate()
175
 		self.post_mutate()
172
 		return ret_val
176
 		return ret_val
173
 
177
 
174
-class AbstractMutableDict(dict, metaclass=ABCMeta):
178
+class AbstractMutableDict(dict[K, V], Generic[K, V], metaclass=ABCMeta):
175
 	"""
179
 	"""
176
 	Abstract dict with hooks for custom logic before and after mutation
180
 	Abstract dict with hooks for custom logic before and after mutation
177
 	operations.
181
 	operations.
228
 
232
 
229
 # Collections with limited number of elements
233
 # Collections with limited number of elements
230
 
234
 
231
-class SizeBoundList(AbstractMutableList):
235
+class SizeBoundList(AbstractMutableList[V], Generic[V]):
232
 	"""
236
 	"""
233
 	Subclass of `list` that enforces a maximum number of elements.
237
 	Subclass of `list` that enforces a maximum number of elements.
234
 
238
 
236
 	`self.max_element_count`, then each element will be tested for its "age,"
240
 	`self.max_element_count`, then each element will be tested for its "age,"
237
 	and the "oldest" elements will be removed until the total size is back
241
 	and the "oldest" elements will be removed until the total size is back
238
 	within the limit. "Age" is determined via a provided lambda function. It is
242
 	within the limit. "Age" is determined via a provided lambda function. It is
239
-	a value with arbitrary numeric value that is used comparitively to find
243
+	a value with arbitrary numeric value that is used comparatively to find
240
 	the element with the smallest value.
244
 	the element with the smallest value.
241
 
245
 
242
 	The `element_age` lambda takes two arguments: the element index and the
246
 	The `element_age` lambda takes two arguments: the element index and the
286
 	def copy(self):
290
 	def copy(self):
287
 		return SizeBoundList(self.max_element_count, self.element_age, super())
291
 		return SizeBoundList(self.max_element_count, self.element_age, super())
288
 
292
 
289
-class SizeBoundSet(AbstractMutableSet):
293
+class SizeBoundSet(AbstractMutableSet[V], Generic[V]):
290
 	"""
294
 	"""
291
 	Subclass of `set` that enforces a maximum number of elements.
295
 	Subclass of `set` that enforces a maximum number of elements.
292
 
296
 
294
 	`self.max_element_count`, then each element will be tested for its "age,"
298
 	`self.max_element_count`, then each element will be tested for its "age,"
295
 	and the "oldest" elements will be removed until the total size is back
299
 	and the "oldest" elements will be removed until the total size is back
296
 	within the limit. "Age" is determined via a provided lambda function. It is
300
 	within the limit. "Age" is determined via a provided lambda function. It is
297
-	a value with arbitrary numeric value that is used comparitively to find
301
+	a value with arbitrary numeric value that is used comparatively to find
298
 	the element with the smallest value.
302
 	the element with the smallest value.
299
 
303
 
300
 	The `element_age` lambda takes one argument: the element value. It must
304
 	The `element_age` lambda takes one argument: the element value. It must
343
 	def copy(self):
347
 	def copy(self):
344
 		return SizeBoundSet(self.max_element_count, self.element_age, super())
348
 		return SizeBoundSet(self.max_element_count, self.element_age, super())
345
 
349
 
346
-class SizeBoundDict(AbstractMutableDict):
350
+class SizeBoundDict(AbstractMutableDict[K, V], Generic[K, V]):
347
 	"""
351
 	"""
348
 	Subclass of `dict` that enforces a maximum number of elements.
352
 	Subclass of `dict` that enforces a maximum number of elements.
349
 
353
 
351
 	`self.max_element_count`, then each element will be tested for its "age,"
355
 	`self.max_element_count`, then each element will be tested for its "age,"
352
 	and the "oldest" elements will be removed until the total size is back
356
 	and the "oldest" elements will be removed until the total size is back
353
 	within the limit. "Age" is determined via a provided lambda function. It is
357
 	within the limit. "Age" is determined via a provided lambda function. It is
354
-	a value with arbitrary numeric value that is used comparitively to find
358
+	a value with arbitrary numeric value that is used comparatively to find
355
 	the element with the smallest value.
359
 	the element with the smallest value.
356
 
360
 
357
 	The `element_age` lambda takes two arguments: the key and the value of a
361
 	The `element_age` lambda takes two arguments: the key and the value of a
402
 
406
 
403
 # Collections with limited age of elements
407
 # Collections with limited age of elements
404
 
408
 
405
-class AgeBoundList(AbstractMutableList):
409
+class AgeBoundList(AbstractMutableList[V], Generic[V]):
406
 	"""
410
 	"""
407
 	Subclass of `list` that enforces a maximum "age" of elements.
411
 	Subclass of `list` that enforces a maximum "age" of elements.
408
 
412
 
409
 	After each mutating operation, the minimum and maximum "age" of the elements
413
 	After each mutating operation, the minimum and maximum "age" of the elements
410
 	are determined. If the span between the newest and oldest exceeds
414
 	are determined. If the span between the newest and oldest exceeds
411
-	`self.max_age` then then the oldest elements will be purged until that is
415
+	`self.max_age` then the oldest elements will be purged until that is
412
 	no longer the case. "Age" is determined via a provided lambda function. It
416
 	no longer the case. "Age" is determined via a provided lambda function. It
413
-	is a value with arbitrary numeric value that is used comparitively.
417
+	is a value with arbitrary numeric value that is used comparatively.
414
 
418
 
415
 	The `element_age` lambda takes two arguments: the element index and the
419
 	The `element_age` lambda takes two arguments: the element index and the
416
 	element value. It must return values that can be compared to one another
420
 	element value. It must return values that can be compared to one another
463
 	def copy(self):
467
 	def copy(self):
464
 		return AgeBoundList(self.max_age, self.element_age, super())
468
 		return AgeBoundList(self.max_age, self.element_age, super())
465
 
469
 
466
-class AgeBoundSet(AbstractMutableSet):
470
+class AgeBoundSet(AbstractMutableSet[V], Generic[V]):
467
 	"""
471
 	"""
468
 	Subclass of `set` that enforces a maximum "age" of elements.
472
 	Subclass of `set` that enforces a maximum "age" of elements.
469
 
473
 
470
 	After each mutating operation, the minimum and maximum "age" of the elements
474
 	After each mutating operation, the minimum and maximum "age" of the elements
471
 	are determined. If the span between the newest and oldest exceeds
475
 	are determined. If the span between the newest and oldest exceeds
472
-	`self.max_age` then then the oldest elements will be purged until that is
476
+	`self.max_age` then the oldest elements will be purged until that is
473
 	no longer the case. "Age" is determined via a provided lambda function. It
477
 	no longer the case. "Age" is determined via a provided lambda function. It
474
-	is a value with arbitrary numeric value that is used comparitively.
478
+	is a value with arbitrary numeric value that is used comparatively.
475
 
479
 
476
 	The `element_age` lambda takes one argument: the element value. It must
480
 	The `element_age` lambda takes one argument: the element value. It must
477
 	return values that can be compared to one another and be added and
481
 	return values that can be compared to one another and be added and
524
 	def copy(self):
528
 	def copy(self):
525
 		return AgeBoundSet(self.max_age, self.element_age, super())
529
 		return AgeBoundSet(self.max_age, self.element_age, super())
526
 
530
 
527
-class AgeBoundDict(AbstractMutableDict):
531
+class AgeBoundDict(AbstractMutableDict[K, V], Generic[K, V]):
528
 	"""
532
 	"""
529
 	Subclass of `dict` that enforces a maximum "age" of elements.
533
 	Subclass of `dict` that enforces a maximum "age" of elements.
530
 
534
 
531
 	After each mutating operation, the minimum and maximum "age" of the elements
535
 	After each mutating operation, the minimum and maximum "age" of the elements
532
 	are determined. If the span between the newest and oldest exceeds
536
 	are determined. If the span between the newest and oldest exceeds
533
-	`self.max_age` then then the oldest elements will be purged until that is
537
+	`self.max_age` then the oldest elements will be purged until that is
534
 	no longer the case. "Age" is determined via a provided lambda function. It
538
 	no longer the case. "Age" is determined via a provided lambda function. It
535
-	is a value with arbitrary numeric value that is used comparitively.
539
+	is a value with arbitrary numeric value that is used comparatively.
536
 
540
 
537
 	The `element_age` lambda takes two arguments: the key and value of a pair.
541
 	The `element_age` lambda takes two arguments: the key and value of a pair.
538
 	It must return values that can be compared to one another and be added and
542
 	It must return values that can be compared to one another and be added and

+ 14
- 14
rocketbot/pattern.py Просмотреть файл

5
 import re
5
 import re
6
 from abc import ABCMeta, abstractmethod
6
 from abc import ABCMeta, abstractmethod
7
 from datetime import datetime, timezone
7
 from datetime import datetime, timezone
8
-from typing import Any
8
+from typing import Any, Union
9
 
9
 
10
 from discord import Message, utils as discordutils
10
 from discord import Message, utils as discordutils
11
 from discord.ext.commands import Context
11
 from discord.ext.commands import Context
61
 		self.operator: str = operator
61
 		self.operator: str = operator
62
 		self.value: Any = value
62
 		self.value: Any = value
63
 
63
 
64
-	def __field_value(self, message: Message, other_fields: dict[str: Any]) -> Any:
64
+	def __field_value(self, message: Message, other_fields: dict[str, Any]) -> Any:
65
 		if self.field in ('content.markdown', 'content'):
65
 		if self.field in ('content.markdown', 'content'):
66
 			return message.content
66
 			return message.content
67
 		if self.field == 'content.plain':
67
 		if self.field == 'content.plain':
68
-			return discordutils.remove_markdown(message.clean_content)
68
+			return discordutils.remove_markdown(message.clean_content())
69
 		if self.field == 'author':
69
 		if self.field == 'author':
70
 			return str(message.author.id)
70
 			return str(message.author.id)
71
 		if self.field == 'author.id':
71
 		if self.field == 'author.id':
222
 		'content.plain': TYPE_TEXT,
222
 		'content.plain': TYPE_TEXT,
223
 		'lastmatched': TYPE_TIMESPAN,
223
 		'lastmatched': TYPE_TIMESPAN,
224
 	}
224
 	}
225
-	DEPRECATED_FIELDS: set[str] = set([ 'content' ])
225
+	DEPRECATED_FIELDS: set[str] = { 'content' }
226
 
226
 
227
 	ACTION_TO_ARGS: dict[str, list[str]] = {
227
 	ACTION_TO_ARGS: dict[str, list[str]] = {
228
 		'ban': [],
228
 		'ban': [],
233
 		'reply': [ TYPE_TEXT ],
233
 		'reply': [ TYPE_TEXT ],
234
 	}
234
 	}
235
 
235
 
236
-	OPERATORS_IDENTITY: set[str] = set([ '==', '!=' ])
237
-	OPERATORS_COMPARISON: set[str] = set([ '<', '>', '<=', '>=' ])
236
+	OPERATORS_IDENTITY: set[str] = { '==', '!=' }
237
+	OPERATORS_COMPARISON: set[str] = { '<', '>', '<=', '>=' }
238
 	OPERATORS_NUMERIC: set[str] = OPERATORS_IDENTITY | OPERATORS_COMPARISON
238
 	OPERATORS_NUMERIC: set[str] = OPERATORS_IDENTITY | OPERATORS_COMPARISON
239
-	OPERATORS_TEXT: set[str] = OPERATORS_IDENTITY | set([
239
+	OPERATORS_TEXT: set[str] = OPERATORS_IDENTITY | {
240
 		'contains', '!contains',
240
 		'contains', '!contains',
241
 		'containsword', '!containsword',
241
 		'containsword', '!containsword',
242
 		'matches', '!matches',
242
 		'matches', '!matches',
243
-	])
243
+	}
244
 	OPERATORS_ALL: set[str] = OPERATORS_IDENTITY | OPERATORS_COMPARISON | OPERATORS_TEXT
244
 	OPERATORS_ALL: set[str] = OPERATORS_IDENTITY | OPERATORS_COMPARISON | OPERATORS_TEXT
245
 
245
 
246
 	TYPE_TO_OPERATORS: dict[str, set[str]] = {
246
 	TYPE_TO_OPERATORS: dict[str, set[str]] = {
297
 		Converts a message filter statement into a list of tokens.
297
 		Converts a message filter statement into a list of tokens.
298
 		"""
298
 		"""
299
 		tokens: list[str] = []
299
 		tokens: list[str] = []
300
-		in_quote: bool = False
300
+		in_quote: Union[bool, str] = False
301
 		in_escape: bool = False
301
 		in_escape: bool = False
302
-		all_token_types: set[str] = set([ 'sym', 'op', 'val' ])
302
+		all_token_types: set[str] = { 'sym', 'op', 'val' }
303
 		possible_token_types: set[str] = set(all_token_types)
303
 		possible_token_types: set[str] = set(all_token_types)
304
 		current_token: str = ''
304
 		current_token: str = ''
305
 		for ch in statement:
305
 		for ch in statement:
398
 			token = tokens[token_index]
398
 			token = tokens[token_index]
399
 			if token == 'if':
399
 			if token == 'if':
400
 				if len(current_action_tokens) > 0:
400
 				if len(current_action_tokens) > 0:
401
-					a = PatternAction(current_action_tokens[0], \
401
+					a = PatternAction(current_action_tokens[0],
402
 						current_action_tokens[1:])
402
 						current_action_tokens[1:])
403
 					cls.__validate_action(a)
403
 					cls.__validate_action(a)
404
 					actions.append(a)
404
 					actions.append(a)
407
 			elif token == ',':
407
 			elif token == ',':
408
 				if len(current_action_tokens) < 1:
408
 				if len(current_action_tokens) < 1:
409
 					raise PatternError('Unexpected ,')
409
 					raise PatternError('Unexpected ,')
410
-				a = PatternAction(current_action_tokens[0], \
410
+				a = PatternAction(current_action_tokens[0],
411
 					current_action_tokens[1:])
411
 					current_action_tokens[1:])
412
 				cls.__validate_action(a)
412
 				cls.__validate_action(a)
413
 				actions.append(a)
413
 				actions.append(a)
460
 					return (subexpressions[0], token_index)
460
 					return (subexpressions[0], token_index)
461
 				return (PatternCompoundExpression(last_compound_operator,
461
 				return (PatternCompoundExpression(last_compound_operator,
462
 					subexpressions), token_index)
462
 					subexpressions), token_index)
463
-			if tokens[token_index] in set(["and", "or"]):
463
+			if tokens[token_index] in { "and", "or" }:
464
 				compound_operator = tokens[token_index]
464
 				compound_operator = tokens[token_index]
465
 				if last_compound_operator and \
465
 				if last_compound_operator and \
466
 						compound_operator != last_compound_operator:
466
 						compound_operator != last_compound_operator:
471
 				last_compound_operator = compound_operator
471
 				last_compound_operator = compound_operator
472
 				token_index += 1
472
 				token_index += 1
473
 			if tokens[token_index] == '!':
473
 			if tokens[token_index] == '!':
474
-				(exp, next_index) = cls.__read_expression(tokens, \
474
+				(exp, next_index) = cls.__read_expression(tokens,
475
 						token_index + 1, depth + 1, one_subexpression=True)
475
 						token_index + 1, depth + 1, one_subexpression=True)
476
 				subexpressions.append(PatternCompoundExpression('!', [exp]))
476
 				subexpressions.append(PatternCompoundExpression('!', [exp]))
477
 				token_index = next_index
477
 				token_index = next_index

+ 14
- 0
rocketbot/storage.py Просмотреть файл

2
 Handles storage of persisted and non-persisted data for the bot.
2
 Handles storage of persisted and non-persisted data for the bot.
3
 """
3
 """
4
 import json
4
 import json
5
+from datetime import datetime, timezone, timedelta
5
 from os.path import exists
6
 from os.path import exists
6
 from typing import Any, Optional
7
 from typing import Any, Optional
7
 from discord import Guild
8
 from discord import Guild
8
 
9
 
9
 from config import CONFIG
10
 from config import CONFIG
11
+from rocketbot.collections import AgeBoundDict
12
+
10
 
13
 
11
 class ConfigKey:
14
 class ConfigKey:
12
 	"""
15
 	"""
133
 		cls.__write_guild_config(guild, config)
136
 		cls.__write_guild_config(guild, config)
134
 
137
 
135
 	@classmethod
138
 	@classmethod
139
+	def get_bot_messages(cls, guild: Guild) -> AgeBoundDict:
140
+		"""Returns all the bot messages for a guild."""
141
+		bm = cls.get_state_value(guild, 'bot_messages')
142
+		if bm is None:
143
+			far_future = datetime.now(timezone.utc) + timedelta(days=1000)
144
+			bm = AgeBoundDict(timedelta(seconds=600),
145
+				lambda k, v : v.message_sent_at() or far_future)
146
+			Storage.set_state_value(guild, 'bot_messages', bm)
147
+		return bm
148
+
149
+	@classmethod
136
 	def __write_guild_config(cls, guild: Guild, config: dict[str, Any]) -> None:
150
 	def __write_guild_config(cls, guild: Guild, config: dict[str, Any]) -> None:
137
 		"""
151
 		"""
138
 		Saves config for a guild to a JSON file on disk.
152
 		Saves config for a guild to a JSON file on disk.

+ 2
- 2
rocketbot/utils.py Просмотреть файл

125
 	raise ValueError(f'"{mention}" is not an @ user mention')
125
 	raise ValueError(f'"{mention}" is not an @ user mention')
126
 
126
 
127
 def mention_from_user_id(user_id: Union[str, int]) -> str:
127
 def mention_from_user_id(user_id: Union[str, int]) -> str:
128
-	"""Returns a markdown user mention from a user id."""
128
+	"""Returns a Markdown user mention from a user id."""
129
 	return f'<@!{user_id}>'
129
 	return f'<@!{user_id}>'
130
 
130
 
131
 def mention_from_role_id(role_id: Union[str, int]) -> str:
131
 def mention_from_role_id(role_id: Union[str, int]) -> str:
132
-	"""Returns a markdown role mention from a role id."""
132
+	"""Returns a Markdown role mention from a role id."""
133
 	return f'<@&{role_id}>'
133
 	return f'<@&{role_id}>'
134
 
134
 
135
 def str_from_quoted_str(val: str) -> str:
135
 def str_from_quoted_str(val: str) -> str:

Загрузка…
Отмена
Сохранить