Parcourir la source

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

master
Rocketsoup il y a 2 mois
Parent
révision
c379e06c6a
6 fichiers modifiés avec 146 ajouts et 83 suppressions
  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 Voir le fichier

@@ -5,6 +5,7 @@ changes, and mods can perform actions on the message via emoji reactions.
5 5
 from typing import Any, Optional, Union
6 6
 
7 7
 from datetime import datetime
8
+
8 9
 from discord import Guild, Message, PartialEmoji, TextChannel
9 10
 
10 11
 from config import CONFIG
@@ -142,9 +143,9 @@ class BotMessage:
142 143
 		self.context: Optional[Any] = context
143 144
 		self.quote: Optional[str] = None
144 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 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 149
 		self.__reply_to: Optional[Message] = reply_to
149 150
 		self.__suppress_embeds = suppress_embeds
150 151
 		self.__reactions: list[BotMessageReaction] = []
@@ -155,18 +156,19 @@ class BotMessage:
155 156
 		continue returning False even after calling BaseCog.post_message if
156 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 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 170
 	def has_reactions(self) -> bool:
169
-		'Whether this message has any reactions defined.'
171
+		"""Whether this message has any reactions defined."""
170 172
 		return len(self.__reactions) > 0
171 173
 
172 174
 	async def set_text(self, new_text: str) -> None:
@@ -222,7 +224,7 @@ class BotMessage:
222 224
 		"""
223 225
 		for i, existing in enumerate(self.__reactions):
224 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 228
 					existing.emoji == reaction_or_emoji.emoji):
227 229
 				self.__reactions.pop(i)
228 230
 				await self.update_if_sent()
@@ -246,7 +248,7 @@ class BotMessage:
246 248
 		the guild, otherwise does nothing. Does not need to be called by
247 249
 		BaseCog subclasses.
248 250
 		"""
249
-		if self.__message:
251
+		if len(self.__messages) > 0:
250 252
 			await self.update()
251 253
 
252 254
 	async def update(self) -> None:
@@ -254,49 +256,66 @@ class BotMessage:
254 256
 		Sends or updates an already sent message based on BotMessage state.
255 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 301
 		emoji_to_remove = self.__posted_emoji.copy()
284 302
 		for reaction in self.__reactions:
285 303
 			if reaction.is_enabled:
286 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 306
 					self.__posted_emoji.add(reaction.emoji)
289 307
 				if reaction.emoji in emoji_to_remove:
290 308
 					emoji_to_remove.remove(reaction.emoji)
291 309
 		for emoji in emoji_to_remove:
292
-			await self.__message.clear_reaction(emoji)
310
+			await self.__messages[-1].clear_reaction(emoji)
293 311
 			if emoji in self.__posted_emoji:
294 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 320
 		s: str = ''
302 321
 
@@ -328,4 +347,33 @@ class BotMessage:
328 347
 				else:
329 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 Voir le fichier

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

+ 22
- 18
rocketbot/collections.py Voir le fichier

@@ -3,10 +3,14 @@ Subclasses of list, set, and dict with special behaviors.
3 3
 """
4 4
 
5 5
 from abc import ABCMeta, abstractmethod
6
+from typing import Generic, TypeVar
6 7
 
7 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 15
 	Abstract list with hooks for custom logic before and after mutation
12 16
 	operations.
@@ -86,7 +90,7 @@ class AbstractMutableList(list, metaclass=ABCMeta):
86 90
 		self.post_mutate()
87 91
 		return ret_val
88 92
 
89
-class AbstractMutableSet(set, metaclass=ABCMeta):
93
+class AbstractMutableSet(set[V], Generic[V], metaclass=ABCMeta):
90 94
 	"""
91 95
 	Abstract set with hooks for custom logic before and after mutation
92 96
 	operations.
@@ -171,7 +175,7 @@ class AbstractMutableSet(set, metaclass=ABCMeta):
171 175
 		self.post_mutate()
172 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 180
 	Abstract dict with hooks for custom logic before and after mutation
177 181
 	operations.
@@ -228,7 +232,7 @@ class AbstractMutableDict(dict, metaclass=ABCMeta):
228 232
 
229 233
 # Collections with limited number of elements
230 234
 
231
-class SizeBoundList(AbstractMutableList):
235
+class SizeBoundList(AbstractMutableList[V], Generic[V]):
232 236
 	"""
233 237
 	Subclass of `list` that enforces a maximum number of elements.
234 238
 
@@ -236,7 +240,7 @@ class SizeBoundList(AbstractMutableList):
236 240
 	`self.max_element_count`, then each element will be tested for its "age,"
237 241
 	and the "oldest" elements will be removed until the total size is back
238 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 244
 	the element with the smallest value.
241 245
 
242 246
 	The `element_age` lambda takes two arguments: the element index and the
@@ -286,7 +290,7 @@ class SizeBoundList(AbstractMutableList):
286 290
 	def copy(self):
287 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 295
 	Subclass of `set` that enforces a maximum number of elements.
292 296
 
@@ -294,7 +298,7 @@ class SizeBoundSet(AbstractMutableSet):
294 298
 	`self.max_element_count`, then each element will be tested for its "age,"
295 299
 	and the "oldest" elements will be removed until the total size is back
296 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 302
 	the element with the smallest value.
299 303
 
300 304
 	The `element_age` lambda takes one argument: the element value. It must
@@ -343,7 +347,7 @@ class SizeBoundSet(AbstractMutableSet):
343 347
 	def copy(self):
344 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 352
 	Subclass of `dict` that enforces a maximum number of elements.
349 353
 
@@ -351,7 +355,7 @@ class SizeBoundDict(AbstractMutableDict):
351 355
 	`self.max_element_count`, then each element will be tested for its "age,"
352 356
 	and the "oldest" elements will be removed until the total size is back
353 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 359
 	the element with the smallest value.
356 360
 
357 361
 	The `element_age` lambda takes two arguments: the key and the value of a
@@ -402,15 +406,15 @@ class SizeBoundDict(AbstractMutableDict):
402 406
 
403 407
 # Collections with limited age of elements
404 408
 
405
-class AgeBoundList(AbstractMutableList):
409
+class AgeBoundList(AbstractMutableList[V], Generic[V]):
406 410
 	"""
407 411
 	Subclass of `list` that enforces a maximum "age" of elements.
408 412
 
409 413
 	After each mutating operation, the minimum and maximum "age" of the elements
410 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 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 419
 	The `element_age` lambda takes two arguments: the element index and the
416 420
 	element value. It must return values that can be compared to one another
@@ -463,15 +467,15 @@ class AgeBoundList(AbstractMutableList):
463 467
 	def copy(self):
464 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 472
 	Subclass of `set` that enforces a maximum "age" of elements.
469 473
 
470 474
 	After each mutating operation, the minimum and maximum "age" of the elements
471 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 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 480
 	The `element_age` lambda takes one argument: the element value. It must
477 481
 	return values that can be compared to one another and be added and
@@ -524,15 +528,15 @@ class AgeBoundSet(AbstractMutableSet):
524 528
 	def copy(self):
525 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 533
 	Subclass of `dict` that enforces a maximum "age" of elements.
530 534
 
531 535
 	After each mutating operation, the minimum and maximum "age" of the elements
532 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 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 541
 	The `element_age` lambda takes two arguments: the key and value of a pair.
538 542
 	It must return values that can be compared to one another and be added and

+ 14
- 14
rocketbot/pattern.py Voir le fichier

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

+ 14
- 0
rocketbot/storage.py Voir le fichier

@@ -2,11 +2,14 @@
2 2
 Handles storage of persisted and non-persisted data for the bot.
3 3
 """
4 4
 import json
5
+from datetime import datetime, timezone, timedelta
5 6
 from os.path import exists
6 7
 from typing import Any, Optional
7 8
 from discord import Guild
8 9
 
9 10
 from config import CONFIG
11
+from rocketbot.collections import AgeBoundDict
12
+
10 13
 
11 14
 class ConfigKey:
12 15
 	"""
@@ -133,6 +136,17 @@ class Storage:
133 136
 		cls.__write_guild_config(guild, config)
134 137
 
135 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 150
 	def __write_guild_config(cls, guild: Guild, config: dict[str, Any]) -> None:
137 151
 		"""
138 152
 		Saves config for a guild to a JSON file on disk.

+ 2
- 2
rocketbot/utils.py Voir le fichier

@@ -125,11 +125,11 @@ def user_id_from_mention(mention: str) -> str:
125 125
 	raise ValueError(f'"{mention}" is not an @ user mention')
126 126
 
127 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 129
 	return f'<@!{user_id}>'
130 130
 
131 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 133
 	return f'<@&{role_id}>'
134 134
 
135 135
 def str_from_quoted_str(val: str) -> str:

Chargement…
Annuler
Enregistrer