Skip to content

Commit c30c9e9

Browse files
committed
refactor: use monotonic timer for auto unsooze task, add typing
1 parent 1ae6579 commit c30c9e9

2 files changed

Lines changed: 61 additions & 42 deletions

File tree

bot.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@
5858
except AttributeError:
5959
logger.error("Failed to use WindowsProactorEventLoopPolicy.", exc_info=True)
6060

61+
6162
class ModmailCommandContext(commands.Context["ModmailBot"]):
6263
thread: Optional[Thread]
64+
65+
6366
class ModmailBot(commands.Bot):
6467
def __init__(self):
6568
self.config = ConfigManager(self)
@@ -171,7 +174,7 @@ def hosting_method(self) -> HostingMethod:
171174
return HostingMethod.OTHER
172175

173176
def startup(self):
174-
""
177+
""""""
175178
logger.info(
176179
r"""
177180
____ __ ___ __ _ __
@@ -613,6 +616,7 @@ async def on_ready(self):
613616
for log in await self.api.get_open_logs():
614617
if log.get("channel_id") is None or self.get_channel(int(log["channel_id"])) is None:
615618
logger.debug("Unable to resolve thread with channel %s.", log["channel_id"])
619+
assert self.user
616620
log_data = await self.api.post_log(
617621
log["channel_id"],
618622
{
@@ -882,7 +886,7 @@ async def send_embed(title=None, desc=None):
882886

883887
return blocked
884888

885-
async def get_thread_cooldown(self, author: discord.Member):
889+
async def get_thread_cooldown(self, author: discord.Member | discord.User) -> typing.Optional[str]:
886890
thread_cooldown = self.config.get("thread_cooldown")
887891
now = discord.utils.utcnow()
888892

@@ -966,10 +970,13 @@ async def process_dm_modmail(self, message: discord.Message) -> None:
966970
return
967971
sent_emoji, blocked_emoji = await self.retrieve_emoji()
968972

973+
# TODO pretty sure these conditionals are completely incorrect and will never evaluate to true
974+
# The enum values do not exist and messages aren't even structured like this.
969975
# Handle forwarded messages (Discord forwards)
970976
# See: https://discord.com/developers/docs/resources/message#message-reference-content-attribution-forwards
971977
# 1. Multi-forward (message_snapshots)
972978
if hasattr(message, "flags") and getattr(message.flags, "has_snapshot", False):
979+
logger.debug("Received a forwarded message with snapshots from ")
973980
if hasattr(message, "message_snapshots") and message.message_snapshots:
974981
thread = await self.threads.find(recipient=message.author)
975982
if thread is None:
@@ -1038,9 +1045,12 @@ def __init__(self, original_message, forwarded_content):
10381045
message.content = "[Forwarded message with no content]"
10391046
# 2. Single-message forward (MessageType.forward)
10401047
elif getattr(message, "type", None) == getattr(discord.MessageType, "forward", None):
1048+
logger.debug(
1049+
"Received a forwarded message from",
1050+
)
10411051
# Check for message.reference and its type
1042-
ref = getattr(message, "reference", None)
1043-
if ref and getattr(ref, "type", None) == getattr(discord, "MessageReferenceType", None).forward:
1052+
ref = message.reference
1053+
if ref and ref.type == discord.MessageReferenceType.forward:
10441054
# Try to fetch the referenced message
10451055
ref_msg = None
10461056
try:
@@ -1081,7 +1091,8 @@ def __init__(self, original_message, forwarded_content):
10811091
"A new thread was blocked from %s due to disabled Modmail.", message.author
10821092
)
10831093
await self.add_reaction(message, blocked_emoji)
1084-
return await message.channel.send(embed=embed)
1094+
await message.channel.send(embed=embed)
1095+
return
10851096
thread = await self.threads.create(message.author, message=message)
10861097
else:
10871098
if self.config["dm_disabled"] == DMDisabled.ALL_THREADS:
@@ -1098,11 +1109,12 @@ def __init__(self, original_message, forwarded_content):
10981109
"A message was blocked from %s due to disabled Modmail.", message.author
10991110
)
11001111
await self.add_reaction(message, blocked_emoji)
1101-
return await message.channel.send(embed=embed)
1112+
await message.channel.send(embed=embed)
1113+
return
11021114

11031115
# Create a forwarded message wrapper to preserve forward info
11041116
class ForwardedMessage:
1105-
def __init__(self, original_message, ref_message):
1117+
def __init__(self, original_message: discord.Message, ref_message: discord.Message):
11061118
self.author = original_message.author
11071119
# Use the utility function to extract content or fallback to ref message content
11081120
extracted_content = extract_forwarded_content(original_message)
@@ -1148,7 +1160,7 @@ def __init__(self, original_message, ref_message):
11481160
thread
11491161
and thread.channel
11501162
and isinstance(thread.channel, discord.TextChannel)
1151-
and self.get_channel(getattr(thread.channel, "id", None)) is None
1163+
and self.get_channel(thread.channel.id) is None
11521164
):
11531165
logger.info(
11541166
"Stale thread detected for %s (channel deleted). Purging cache entry and creating new thread.",
@@ -1192,7 +1204,8 @@ def __init__(self, original_message, ref_message):
11921204
message.author,
11931205
)
11941206
await self.add_reaction(message, blocked_emoji)
1195-
return await message.channel.send(embed=embed)
1207+
await message.channel.send(embed=embed)
1208+
return
11961209

11971210
thread = await self.threads.create(message.author, message=message)
11981211
# If thread menu is enabled, thread creation is deferred until user selects an option.
@@ -1214,7 +1227,8 @@ def __init__(self, original_message, ref_message):
12141227
message.author,
12151228
)
12161229
await self.add_reaction(message, blocked_emoji)
1217-
return await message.channel.send(embed=embed)
1230+
await message.channel.send(embed=embed)
1231+
return
12181232

12191233
if not thread.cancelled:
12201234
try:

cogs/modmail.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import re
3+
import time
34
from datetime import datetime, timedelta, timezone
45
from itertools import batched
56
from typing import List, Literal, Optional, Tuple, Union
@@ -26,13 +27,13 @@
2627
class Modmail(commands.Cog):
2728
"""Commands directly related to Modmail functionality."""
2829

29-
def __init__(self, bot):
30+
def __init__(self, bot: ModmailBot):
3031
self.bot: ModmailBot = bot
3132
self._snoozed_cache = []
3233
self._auto_unsnooze_task = self.bot.loop.create_task(self.auto_unsnooze_task())
3334

3435
@staticmethod
35-
def _to_utc_datetime(value):
36+
def _to_utc_datetime(value) -> Optional[datetime]:
3637
if value is None:
3738
return None
3839
if isinstance(value, datetime):
@@ -47,21 +48,18 @@ def _to_utc_datetime(value):
4748

4849
async def auto_unsnooze_task(self):
4950
await self.bot.wait_until_ready()
50-
last_db_query = 0
51+
logger.debug("auto_unsnooze_task")
52+
last_db_query: int | float = 0
5153
while not self.bot.is_closed():
52-
now = datetime.now(timezone.utc)
54+
now = time.monotonic()
5355
try:
5456
# Query DB every 2 minutes
55-
if (now.timestamp() - last_db_query) > 120:
56-
snoozed_threads = await self.bot.api.logs.find(
57-
{
58-
"$or": [
59-
{"snooze_until": {"$gte": now}},
60-
]
61-
}
62-
).to_list(None)
57+
if (now - last_db_query) > 120:
58+
snoozed_threads = await self.bot.api.logs.find({"snoozed": True, "open": True}).to_list(
59+
None
60+
)
6361
self._snoozed_cache = snoozed_threads or []
64-
last_db_query = now.timestamp()
62+
last_db_query = time.monotonic()
6563
# Check cache every 10 seconds
6664
to_unsnooze = []
6765
for thread_data in list(self._snoozed_cache):
@@ -74,7 +72,7 @@ async def auto_unsnooze_task(self):
7472
dt = self._to_utc_datetime(snooze_until)
7573
if dt is None:
7674
continue
77-
if now >= dt:
75+
if datetime.now(timezone.utc) >= dt:
7876
to_unsnooze.append(thread_data)
7977
for thread_data in to_unsnooze:
8078
recipient = thread_data.get("recipient")
@@ -92,7 +90,7 @@ async def auto_unsnooze_task(self):
9290
if channel:
9391
await channel.send("⏰ This thread has been automatically unsnoozed.")
9492
except Exception as e:
95-
logger.info(
93+
logger.error(
9694
"Failed to notify channel after auto-unsnooze: %s",
9795
e,
9896
)
@@ -101,7 +99,7 @@ async def auto_unsnooze_task(self):
10199
logger.error(f"Error in auto_unsnooze_task: {e}")
102100
await asyncio.sleep(10)
103101

104-
def _resolve_user(self, user_str):
102+
def _resolve_user(self, user_str: str) -> Optional[int]:
105103
"""Helper to resolve a user from mention, ID, or username."""
106104
import re
107105

@@ -319,11 +317,11 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte
319317
"""
320318
Add a snippet.
321319
322-
Simply to add a snippet, do:
320+
Simply to add a snippet, do:
323321
`{prefix}snippet add hey hello there :)`
324322
then when you type `{prefix}hey`, "hello there :)" will get sent to the recipient.
325323
326-
To add a multi-word snippet name, use quotes:
324+
To add a multi-word snippet name, use quotes:
327325
`{prefix}snippet add "two word" this is a two word snippet.`
328326
"""
329327
if self._validate_snippet_name(name):
@@ -1640,14 +1638,17 @@ async def pareply(self, ctx: ModmailCommandContext, *, msg: str = ""):
16401638
async with safe_typing(ctx):
16411639
await ctx.thread.reply(ctx.message, msg, anonymous=True, plain=True)
16421640

1643-
1644-
async def _create_note(self, message: discord.Message, msg: str, thread: Thread, persistent: bool) -> Optional[discord.Message]:
1641+
async def _create_note(
1642+
self, message: discord.Message, msg: str, thread: Thread, persistent: bool
1643+
) -> Optional[discord.Message]:
16451644
message.content = msg
16461645
async with safe_typing(message.channel):
16471646
note_message = await thread.note(message, persistent)
16481647
await note_message.pin()
16491648
if persistent:
1650-
await self.bot.api.create_note(recipient=thread.recipient, message=message, message_id=note_message.id)
1649+
await self.bot.api.create_note(
1650+
recipient=thread.recipient, message=message, message_id=note_message.id
1651+
)
16511652
# Acknowledge and clean up the invoking command message
16521653
sent_emoji, _ = await self.bot.retrieve_emoji()
16531654
await self.bot.add_reaction(message, sent_emoji)
@@ -1829,7 +1830,9 @@ async def contact(
18291830
try:
18301831
await msg.delete(delay=10)
18311832
except (discord.Forbidden, discord.NotFound) as e:
1832-
logger.debug(f"Failed to delete message (likely already deleted or lacking permissions): {e}")
1833+
logger.debug(
1834+
f"Failed to delete message (likely already deleted or lacking permissions): {e}"
1835+
)
18331836
# Don't try to create a new thread - we just unsnoozed existing ones
18341837
return
18351838

@@ -2448,9 +2451,9 @@ async def snooze(self, ctx: ModmailCommandContext, *, duration: UserFriendlyTime
24482451
try:
24492452
logger.debug("Auto-creating snoozed category for move-based snoozing.")
24502453
# Hide category by default; only bot can view/manage
2451-
overwrites: dict[Union[discord.Role, discord.Member, discord.Object], discord.PermissionOverwrite] = {
2452-
self.bot.modmail_guild.default_role: discord.PermissionOverwrite(view_channel=False)
2453-
}
2454+
overwrites: dict[
2455+
Union[discord.Role, discord.Member, discord.Object], discord.PermissionOverwrite
2456+
] = {self.bot.modmail_guild.default_role: discord.PermissionOverwrite(view_channel=False)}
24542457
bot_member = self.bot.modmail_guild.me
24552458
if bot_member is not None:
24562459
overwrites[bot_member] = discord.PermissionOverwrite(
@@ -2573,7 +2576,7 @@ async def unsnooze(self, ctx: ModmailCommandContext, *, user: str | None = None)
25732576
assert self.bot.threads is not None
25742577

25752578
thread: Thread | None = None
2576-
2579+
25772580
user_obj = None
25782581
if user is not None:
25792582
user_id = self._resolve_user(user)
@@ -2607,7 +2610,9 @@ async def unsnooze(self, ctx: ModmailCommandContext, *, user: str | None = None)
26072610

26082611
# Manually fetch snooze_data if the thread object doesn't have it
26092612
if not thread.snooze_data:
2610-
log_entry = await self.bot.api.logs.find_one({"recipient.id": str(thread.id), "snoozed": True, "_id": str(thread.key)})
2613+
log_entry = await self.bot.api.logs.find_one(
2614+
{"recipient.id": str(thread.id), "snoozed": True, "_id": str(thread.key)}
2615+
)
26112616
if log_entry:
26122617
thread.snooze_data = log_entry.get("snooze_data")
26132618
else:
@@ -2637,7 +2642,6 @@ async def snoozed(self, ctx: commands.Context):
26372642
await ctx.send("No threads are currently snoozed.")
26382643
return
26392644

2640-
26412645
# TODO this should really be a
26422646
lines = []
26432647
now = datetime.now(timezone.utc)
@@ -2667,9 +2671,7 @@ async def snoozed(self, ctx: commands.Context):
26672671
until_dt = since_dt + timedelta(seconds=int(duration))
26682672
until_str = f"<t:{int(until_dt.timestamp())}:R>"
26692673
except (ValueError, TypeError) as e:
2670-
logger.warning(
2671-
f"Invalid until time for {user_id}: {since} + {duration} ({e})"
2672-
)
2674+
logger.warning(f"Invalid until time for {user_id}: {since} + {duration} ({e})")
26732675

26742676
lines.append(f"- {user} (`{user_id}`) since {since_str}, until {until_str}")
26752677

@@ -2680,8 +2682,11 @@ async def cog_load(self):
26802682

26812683
@tasks.loop(seconds=10)
26822684
async def snooze_auto_unsnooze(self):
2685+
logger.debug("snooze_auto_unsnooze")
26832686
now = datetime.now(timezone.utc)
2684-
snoozed = await self.bot.api.logs.find({"snoozed": True, "open": True, "snooze_until": {"$lte": now}}).to_list(None)
2687+
snoozed = await self.bot.api.logs.find(
2688+
{"snoozed": True, "open": True, "snooze_until": {"$lte": now}}
2689+
).to_list(None)
26852690
for entry in snoozed:
26862691
snooze_until = entry.get("snooze_until")
26872692
if snooze_until:

0 commit comments

Comments
 (0)