瀏覽代碼

Normalizing datetimes to utc. Fixing datatype conversions on guild settings

main
Rocketsoup 1 月之前
父節點
當前提交
258150e578

+ 0
- 1
rocketbot/bot.py 查看文件

@@ -83,7 +83,6 @@ def __create_bot():
83 83
 		return
84 84
 	bot_log(None, None, 'Creating bot...')
85 85
 	intents = Intents.default()
86
-	intents.messages = True
87 86
 	intents.message_content = True
88 87
 	intents.members = True
89 88
 	intents.presences = False

+ 2
- 2
rocketbot/botmessage.py 查看文件

@@ -9,7 +9,7 @@ from discord import Guild, Message, PartialEmoji, TextChannel
9 9
 
10 10
 from config import CONFIG
11 11
 from rocketbot.storage import ConfigKey, Storage
12
-from rocketbot.utils import bot_log
12
+from rocketbot.utils import bot_log, norm_datetime
13 13
 
14 14
 
15 15
 class BotMessageReaction:
@@ -165,7 +165,7 @@ class BotMessage:
165 165
 
166 166
 	def message_sent_at(self) -> Optional[datetime]:
167 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
168
+		return norm_datetime(self.__messages[0].created_at) if len(self.__messages) > 0 else None
169 169
 
170 170
 	def has_reactions(self) -> bool:
171 171
 		"""Whether this message has any reactions defined."""

+ 5
- 5
rocketbot/cogs/autokickcog.py 查看文件

@@ -1,4 +1,4 @@
1
-from datetime import datetime, timedelta
1
+from datetime import datetime, timedelta, timezone
2 2
 
3 3
 from discord import Guild, Member, Status
4 4
 from discord.ext import tasks
@@ -27,7 +27,7 @@ class AutoKickContext:
27 27
 class StatusCheckContext:
28 28
 	def __init__(self, member: Member):
29 29
 		self.member = member
30
-		self.joined_at = datetime.now()
30
+		self.joined_at = datetime.now(timezone.utc)
31 31
 
32 32
 class AutoKickCog(BaseCog, name='Auto Kick'):
33 33
 	"""
@@ -93,7 +93,7 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
93 93
 		"""Checks status of new members shortly after joining to see if they go offline"""
94 94
 		contexts = self.status_check_members.copy()
95 95
 		self.status_check_members = []
96
-		now = datetime.now()
96
+		now = datetime.now(timezone.utc)
97 97
 		# bot_log(guild=None, cog_class=None, message=f'Found {len(contexts)} members to check')
98 98
 		for c in contexts:
99 99
 			context: StatusCheckContext = c
@@ -118,10 +118,10 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
118 118
 			Storage.set_state_value(guild, self.STATE_KEY_RECENT_KICKS, recent_kicks)
119 119
 		context: AutoKickContext = recent_kicks.get(member.id)
120 120
 		if context is None:
121
-			context = AutoKickContext(member, datetime.now())
121
+			context = AutoKickContext(member, datetime.now(timezone.utc))
122 122
 			recent_kicks[member.id] = context
123 123
 		else:
124
-			context.record_kick(datetime.now())
124
+			context.record_kick(datetime.now(timezone.utc))
125 125
 		max_kick_count: int = self.get_guild_setting(guild, self.SETTING_BAN_COUNT)
126 126
 		disable_help = 'To disable this feature: `/disable autokick`.'
127 127
 		ban_help = 'To configure ban threshold: `/set autokick_bancount #` (0 to disable)'

+ 10
- 9
rocketbot/cogs/basecog.py 查看文件

@@ -24,7 +24,7 @@ from rocketbot.botmessage import BotMessage, BotMessageReaction
24 24
 from rocketbot.cogsetting import CogSetting
25 25
 from rocketbot.collections import AgeBoundDict
26 26
 from rocketbot.storage import Storage
27
-from rocketbot.utils import bot_log, dump_stacktrace
27
+from rocketbot.utils import bot_log, dump_stacktrace, norm_datetime
28 28
 
29 29
 
30 30
 class WarningContext:
@@ -129,9 +129,9 @@ class BaseCog(Cog):
129 129
 		"""
130 130
 		if guild:
131 131
 			key = f'{cls.__name__}.{setting.name}'
132
-			value = Storage.get_config_value(guild, key)
133
-			if value is not None:
134
-				return value
132
+			stored_value = Storage.get_config_value(guild, key)
133
+			if stored_value is not None:
134
+				return setting.to_native_value(stored_value)
135 135
 		if use_cog_default_if_not_set:
136 136
 			return setting.default_value
137 137
 		return None
@@ -150,7 +150,8 @@ class BaseCog(Cog):
150 150
 		"""
151 151
 		setting.validate_value(new_value)
152 152
 		key = f'{cls.__name__}.{setting.name}'
153
-		Storage.set_config_value(guild, key, new_value)
153
+		stored_value = setting.to_stored_value(new_value)
154
+		Storage.set_config_value(guild, key, stored_value)
154 155
 
155 156
 	# @commands.Cog.listener()
156 157
 	async def __on_ready(self):
@@ -183,7 +184,7 @@ class BaseCog(Cog):
183 184
 		if context is None:
184 185
 			return False
185 186
 		squelch_warning_seconds: int = CONFIG['squelch_warning_seconds']
186
-		return datetime.now() - context.last_warned < timedelta(seconds=squelch_warning_seconds)
187
+		return datetime.now(timezone.utc) - context.last_warned < timedelta(seconds=squelch_warning_seconds)
187 188
 
188 189
 	def record_warning(self, member: Member):
189 190
 		"""
@@ -198,10 +199,10 @@ class BaseCog(Cog):
198 199
 			Storage.set_state_value(member.guild, BaseCog.STATE_KEY_RECENT_WARNINGS, recent_warns)
199 200
 		context: WarningContext = recent_warns.get(member.id)
200 201
 		if context is None:
201
-			context = WarningContext(member, datetime.now())
202
+			context = WarningContext(member, datetime.now(timezone.utc))
202 203
 			recent_warns[member.id] = context
203 204
 		else:
204
-			context.last_warned = datetime.now()
205
+			context.last_warned = datetime.now(timezone.utc)
205 206
 
206 207
 	def record_warnings(self, members: list[Member]):
207 208
 		"""
@@ -219,7 +220,7 @@ class BaseCog(Cog):
219 220
 		if bm is None:
220 221
 			far_future = datetime.now(timezone.utc) + timedelta(days=1000)
221 222
 			bm = AgeBoundDict(timedelta(seconds=600),
222
-				lambda k, v : v.message_sent_at() or far_future)
223
+				lambda k, v : norm_datetime(v.message_sent_at()) or far_future)
223 224
 			Storage.set_state_value(guild, 'bot_messages', bm)
224 225
 		return bm
225 226
 

+ 14
- 14
rocketbot/cogs/crosspostcog.py 查看文件

@@ -2,7 +2,7 @@
2 2
 Cog for detecting spam messages posted in multiple channels.
3 3
 """
4 4
 import re
5
-from datetime import datetime, timedelta
5
+from datetime import datetime, timedelta, timezone
6 6
 from typing import Optional
7 7
 
8 8
 from discord import Member, Message, TextChannel
@@ -13,7 +13,7 @@ from config import CONFIG
13 13
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
14 14
 from rocketbot.collections import AgeBoundDict, AgeBoundList
15 15
 from rocketbot.storage import Storage
16
-from rocketbot.utils import str_from_timedelta
16
+from rocketbot.utils import norm_datetime, str_from_timedelta
17 17
 
18 18
 
19 19
 class SpamContext:
@@ -22,7 +22,7 @@ class SpamContext:
22 22
 	"""
23 23
 	def __init__(self, member: Member) -> None:
24 24
 		self.member: Member = member
25
-		self.age: datetime = datetime.now()
25
+		self.age: datetime = datetime.now(timezone.utc)
26 26
 		self.bot_message: Optional[BotMessage] = None
27 27
 		self.is_kicked: bool = False
28 28
 		self.is_banned: bool = False
@@ -152,21 +152,21 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
152 152
 			self.__trace(f"Hash for message #{m.id} by {m.author.name} is {h}\n\thash content: \"{to_hash}\"")
153 153
 			return h
154 154
 
155
-		min_length = self.get_guild_setting(message.guild, self.SETTING_MIN_LENGTH)
155
+		min_length: int = self.get_guild_setting(message.guild, self.SETTING_MIN_LENGTH)
156 156
 		if len(message.attachments) == 0 and len(message.content) < min_length:
157 157
 			# Message too short to count towards spam total
158 158
 			self.__trace(f"Message len {len(message.content)} < {min_length} configured min")
159 159
 			return
160 160
 
161 161
 		# Get config
162
-		max_age = timedelta(seconds=self.get_guild_setting(message.guild, self.SETTING_TIMESPAN))
162
+		max_age: timedelta = self.get_guild_setting(message.guild, self.SETTING_TIMESPAN)
163 163
 		warn_count: int = self.get_guild_setting(message.guild, self.SETTING_WARN_COUNT)
164 164
 		dupe_warn_count: int = self.get_guild_setting(message.guild, self.SETTING_DUPE_WARN_COUNT)
165 165
 
166 166
 		# Record message
167 167
 		recent_messages: AgeBoundList[Message, datetime, timedelta] = Storage.get_state_value(message.guild, self.STATE_KEY_RECENT_MESSAGES)
168 168
 		if recent_messages is None:
169
-			recent_messages = AgeBoundList(max_age, lambda index, message : message.created_at)
169
+			recent_messages = AgeBoundList(max_age, lambda index, message : norm_datetime(message.created_at))
170 170
 			Storage.set_state_value(message.guild, self.STATE_KEY_RECENT_MESSAGES, recent_messages)
171 171
 		recent_messages.max_age = max_age
172 172
 		recent_messages.append(message)
@@ -219,19 +219,19 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
219 219
 			self.log(message.guild,
220 220
 				f'\u0007{message.author.name} ({message.author.id}) ' + \
221 221
 				f'posted messages in {channel_count} channels.')
222
-		context.age = message.created_at
222
+		context.age = norm_datetime(message.created_at)
223 223
 		context.duplicate_count = max_duplicate_count
224 224
 		context.spam_messages.update(member_messages)
225 225
 		context.unique_channels.update(unique_channels)
226 226
 		await self.__update_from_context(context)
227 227
 
228 228
 	async def __update_from_context(self, context: SpamContext):
229
-		ban_count = self.get_guild_setting(context.member.guild, self.SETTING_BAN_COUNT)
230
-		dupe_ban_count = self.get_guild_setting(context.member.guild, self.SETTING_DUPE_BAN_COUNT)
229
+		ban_count: int = self.get_guild_setting(context.member.guild, self.SETTING_BAN_COUNT)
230
+		dupe_ban_count: int = self.get_guild_setting(context.member.guild, self.SETTING_DUPE_BAN_COUNT)
231 231
 		channel_count = len(context.unique_channels)
232 232
 		if channel_count >= ban_count or context.duplicate_count >= dupe_ban_count:
233 233
 			if not context.is_banned:
234
-				max_age = timedelta(seconds=self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN))
234
+				max_age: timedelta = self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN)
235 235
 				max_age_str = str_from_timedelta(max_age)
236 236
 				await context.member.ban(
237 237
 					reason=f'Rocketbot: Posted in {channel_count} channels within {max_age_str} ' + \
@@ -253,7 +253,7 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
253 253
 		channel_count = len(context.unique_channels)
254 254
 		deleted_count = len(context.deleted_messages)
255 255
 		duplicate_count = context.duplicate_count
256
-		max_age = timedelta(seconds=self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN))
256
+		max_age: timedelta = self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN)
257 257
 		max_age_str = str_from_timedelta(max_age)
258 258
 		message = context.bot_message
259 259
 		if message is None:
@@ -328,7 +328,7 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
328 328
 			self.__log_ban(context, reacted_by.name)
329 329
 
330 330
 	def __log_deletion(self, context: SpamContext, by_who: str) -> None:
331
-		max_age = timedelta(seconds=self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN))
331
+		max_age: timedelta = self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN)
332 332
 		max_age_str = str_from_timedelta(max_age)
333 333
 		channel_count = len(context.unique_channels)
334 334
 		duplicate_count = context.duplicate_count
@@ -338,7 +338,7 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
338 338
 				 f'({duplicate_count} identical). Deleted by {by_who}.')
339 339
 
340 340
 	def __log_kick(self, context: SpamContext, by_who: str) -> None:
341
-		max_age = timedelta(seconds=self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN))
341
+		max_age: timedelta = self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN)
342 342
 		max_age_str = str_from_timedelta(max_age)
343 343
 		channel_count = len(context.unique_channels)
344 344
 		duplicate_count = context.duplicate_count
@@ -348,7 +348,7 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
348 348
 				 f'({duplicate_count} identical). Kicked by {by_who}.')
349 349
 
350 350
 	def __log_ban(self, context: SpamContext, by_who: str) -> None:
351
-		max_age = timedelta(seconds=self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN))
351
+		max_age: timedelta = self.get_guild_setting(context.member.guild, self.SETTING_TIMESPAN)
352 352
 		max_age_str = str_from_timedelta(max_age)
353 353
 		channel_count = len(context.unique_channels)
354 354
 		duplicate_count = context.duplicate_count

+ 3
- 5
rocketbot/cogs/joinraidcog.py 查看文件

@@ -106,9 +106,8 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
106 106
 		guild: Guild = member.guild
107 107
 		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
108 108
 			return
109
-		min_count = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
110
-		seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
111
-		timespan: timedelta = timedelta(seconds=seconds)
109
+		min_count: int = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
110
+		timespan: timedelta = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
112 111
 
113 112
 		last_raid: JoinRaidContext = Storage.get_state_value(guild, self.STATE_KEY_LAST_RAID)
114 113
 		recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
@@ -155,8 +154,7 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
155 154
 
156 155
 	async def on_setting_updated(self, guild: Guild, setting: CogSetting) -> None:
157 156
 		if setting is self.SETTING_JOIN_TIME:
158
-			seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
159
-			timespan: timedelta = timedelta(seconds=seconds)
157
+			timespan: timedelta = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
160 158
 			recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild,
161 159
 				self.STATE_KEY_RECENT_JOINS)
162 160
 			if recent_joins:

+ 12
- 5
rocketbot/cogs/logcog.py 查看文件

@@ -30,7 +30,7 @@ from discord.utils import escape_markdown
30 30
 
31 31
 from rocketbot.cogs.basecog import BaseCog, BotMessage, CogSetting
32 32
 from rocketbot.storage import Storage
33
-from rocketbot.utils import dump_stacktrace
33
+from rocketbot.utils import dump_stacktrace, norm_datetime
34 34
 
35 35
 
36 36
 class BufferedMessageEditEvent:
@@ -336,7 +336,7 @@ class LoggingCog(BaseCog, name='Logging'):
336 336
 			pass  # not that interesting and probably noisy
337 337
 		if after.timed_out_until != before.timed_out_until:
338 338
 			if after.timed_out_until:
339
-				delta = after.timed_out_until - datetime.now()
339
+				delta = after.timed_out_until - datetime.now(timezone.utc)
340 340
 				changes.append(f'Timed out for `{delta}`')
341 341
 			elif before.timed_out_until:
342 342
 				changes.append('Timeout cleared')
@@ -426,7 +426,7 @@ class LoggingCog(BaseCog, name='Logging'):
426 426
 			guild = user.guild
427 427
 		else:
428 428
 			return None
429
-		now = datetime.now()
429
+		now = datetime.now(timezone.utc)
430 430
 		async for entry in guild.audit_logs():
431 431
 			age_seconds = now.timestamp() - entry.created_at.timestamp()
432 432
 			if entry.action == action and entry.target == user and age_seconds <= max_age:
@@ -493,7 +493,14 @@ class LoggingCog(BaseCog, name='Logging'):
493 493
 
494 494
 		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_message
495 495
 		"""
496
-		# print(f"Saw message {message.id} \"{message.content}\"")
496
+		# print(f"on_message:"
497
+		# 	f"\n\tid: {message.id}"
498
+		# 	f"\n\tauthor: {message.author.name}"
499
+		# 	f"\n\tchannel: {message.channel.name}"
500
+		# 	f"\n\tcontent: \"{message.content}\""
501
+		# 	f"\n\tattachments: x{len(message.attachments)}"
502
+		# 	f"\n\tembeds: x{len(message.embeds)}"
503
+		# 	f"\n\tcomponents: x{len(message.components)}")
497 504
 
498 505
 	@Cog.listener()
499 506
 	async def on_message_edit(self, before: Message, after: Message) -> None:
@@ -570,7 +577,7 @@ class LoggingCog(BaseCog, name='Logging'):
570 577
 		old_cutoff = timedelta(days=1)
571 578
 		now = datetime.now(timezone.utc)
572 579
 		for event in events:
573
-			if event.before is not None and (now - event.after.created_at) < old_cutoff:
580
+			if event.before is not None and (now - norm_datetime(event.after.created_at)) < old_cutoff:
574 581
 				simple_edits.append(event)
575 582
 			else:
576 583
 				complex_edits.append(event)

+ 2
- 2
rocketbot/cogs/patterncog.py 查看文件

@@ -22,7 +22,7 @@ from rocketbot.pattern import (
22 22
 	PatternStatement,
23 23
 )
24 24
 from rocketbot.storage import Storage
25
-from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
25
+from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace, norm_datetime
26 26
 
27 27
 
28 28
 class PatternContext:
@@ -179,7 +179,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
179 179
 		if last_matched is None:
180 180
 			last_matched = {}
181 181
 			Storage.set_state_value(guild, 'PatternCog.last_matched', last_matched)
182
-		last_matched[name] = time
182
+		last_matched[name] = norm_datetime(time)
183 183
 
184 184
 	@Cog.listener()
185 185
 	async def on_message(self, message: Message) -> None:

+ 4
- 5
rocketbot/cogs/urlspamcog.py 查看文件

@@ -12,7 +12,7 @@ from discord.utils import escape_markdown
12 12
 
13 13
 from config import CONFIG
14 14
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
15
-from rocketbot.utils import describe_timedelta
15
+from rocketbot.utils import describe_timedelta, norm_datetime
16 16
 
17 17
 
18 18
 class URLSpamContext:
@@ -140,13 +140,12 @@ class URLSpamCog(BaseCog, name='URL Spam'):
140 140
 
141 141
 	async def check_message_recency(self, message: Message):
142 142
 		"""Checks if the message was sent too recently by a new user"""
143
-		action = self.get_guild_setting(message.guild, self.SETTING_ACTION)
144
-		join_seconds = self.get_guild_setting(message.guild, self.SETTING_JOIN_AGE)
145
-		min_join_age = timedelta(seconds=join_seconds)
143
+		action: str = self.get_guild_setting(message.guild, self.SETTING_ACTION)
144
+		min_join_age: timedelta = self.get_guild_setting(message.guild, self.SETTING_JOIN_AGE)
146 145
 		if action == 'nothing':
147 146
 			self.__trace("Configured action is nothing")
148 147
 			return
149
-		join_age = message.created_at - message.author.joined_at
148
+		join_age = norm_datetime(message.created_at) - norm_datetime(message.author.joined_at)
150 149
 		join_age_str = describe_timedelta(join_age)
151 150
 		if join_age > min_join_age:
152 151
 			self.__trace("User has been member long enough")

+ 2
- 1
rocketbot/cogs/videopreviewcog.py 查看文件

@@ -151,7 +151,8 @@ class VideoPreviewCog(BaseCog, name='Video Link Previews'):
151 151
 
152 152
 	async def _wait_for_preview(self, message: Message, links: list[MessageLink]):
153 153
 		Self = VideoPreviewCog
154
-		await asyncio.sleep(self.get_guild_setting(message.guild, Self.SETTING_DELAY))
154
+		delay: timedelta = self.get_guild_setting(message.guild, Self.SETTING_DELAY)
155
+		await asyncio.sleep(delay.total_seconds())
155 156
 		# Look for embeds already showing the video
156 157
 		self.log(message.guild, "Checking message for embeds")
157 158
 		for embed in message.embeds:

+ 4
- 0
rocketbot/cogsetting.py 查看文件

@@ -129,12 +129,16 @@ class CogSetting:
129 129
 
130 130
 	def to_stored_value(self, native_value: Any) -> Any:
131 131
 		"""Converts a configuration value to a JSON-compatible datatype."""
132
+		if native_value is None:
133
+			return None
132 134
 		if self.datatype is timedelta:
133 135
 			return native_value.total_seconds()
134 136
 		return native_value
135 137
 
136 138
 	def to_native_value(self, stored_value: Any) -> Any:
137 139
 		"""Converts the stored JSON-compatible datatype to its actual value."""
140
+		if stored_value is None:
141
+			return None
138 142
 		if self.datatype is timedelta and isinstance(stored_value, (int, float)):
139 143
 			return timedelta(seconds=stored_value)
140 144
 		return stored_value

+ 3
- 2
rocketbot/pattern.py 查看文件

@@ -13,6 +13,7 @@ from discord.ext.commands import Context
13 13
 
14 14
 from rocketbot.utils import (
15 15
 	is_user_id,
16
+	norm_datetime,
16 17
 	str_from_quoted_str,
17 18
 	timedelta_from_str,
18 19
 	user_id_from_mention,
@@ -113,13 +114,13 @@ class PatternSimpleExpression(PatternExpression):
113 114
 		if self.field in (cls.FIELD_AUTHOR_ID, cls.ALIAS_FIELD_AUTHOR_ID):
114 115
 			return str(message.author.id)
115 116
 		if self.field == cls.FIELD_AUTHOR_JOINAGE:
116
-			return message.created_at - message.author.joined_at
117
+			return norm_datetime(message.created_at) - norm_datetime(message.author.joined_at)
117 118
 		if self.field == cls.FIELD_AUTHOR_NAME:
118 119
 			return message.author.name
119 120
 		if self.field == cls.FIELD_LAST_MATCHED:
120 121
 			long_ago = datetime(year=1900, month=1, day=1, hour=0, minute=0, second=0, tzinfo=timezone.utc)
121 122
 			last_matched = other_fields.get('last_matched') or long_ago
122
-			return message.created_at - last_matched
123
+			return norm_datetime(message.created_at) - norm_datetime(last_matched)
123 124
 		raise ValueError(f'Bad field name "{self.field}"')
124 125
 
125 126
 	def matches(self, message: Message, other_fields: dict[str, Any]) -> bool:

+ 2
- 1
rocketbot/storage.py 查看文件

@@ -10,6 +10,7 @@ from discord import Guild
10 10
 
11 11
 from config import CONFIG
12 12
 from rocketbot.collections import AgeBoundDict
13
+from rocketbot.utils import norm_datetime
13 14
 
14 15
 
15 16
 class ConfigKey:
@@ -143,7 +144,7 @@ class Storage:
143 144
 		if bm is None:
144 145
 			far_future = datetime.now(timezone.utc) + timedelta(days=1000)
145 146
 			bm = AgeBoundDict(timedelta(seconds=600),
146
-				lambda k, v : v.message_sent_at() or far_future)
147
+				lambda k, v : norm_datetime(v.message_sent_at()) or far_future)
147 148
 			Storage.set_state_value(guild, 'bot_messages', bm)
148 149
 		return bm
149 150
 

+ 6
- 1
rocketbot/utils.py 查看文件

@@ -4,7 +4,7 @@ General utility functions.
4 4
 import re
5 5
 import sys
6 6
 import traceback
7
-from datetime import datetime, timedelta
7
+from datetime import datetime, timedelta, timezone
8 8
 from typing import Any, Optional, Union
9 9
 
10 10
 import discord
@@ -206,6 +206,11 @@ def format_bytes(size: int) -> str:
206 206
 		return f"{size/gib:,.1f} GiB"
207 207
 	return f"{size/gib:,.0f} GiB"
208 208
 
209
+def norm_datetime(dt: datetime) -> datetime:
210
+	"""Converts a datetime to UTC for consistent comparison."""
211
+	# "Naive" datetimes (without a time zone) are assumed as system local time zone
212
+	return datetime.fromtimestamp(dt.timestamp(), timezone.utc)
213
+
209 214
 MOD_PERMISSIONS: Permissions = Permissions(Permissions.manage_messages.flag)
210 215
 
211 216
 class TimeDeltaTransformer(Transformer):

Loading…
取消
儲存