Skip to content

Commit a5b24ef

Browse files
committed
This introduces thread_auto_close and thread_auto_close_response. Both these can be used to configure modmail to auto close a thread after some time has passed.
`thread_auto_close_response` has a `%t` variable that can be used to insert human friendly time into the closing message.
1 parent 9d279e6 commit a5b24ef

File tree

2 files changed

+84
-3
lines changed

2 files changed

+84
-3
lines changed

core/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class ConfigManager:
3535
"blocked_emoji",
3636
"close_emoji",
3737
"disable_recipient_thread_close",
38+
"thread_auto_close",
39+
"thread_auto_close_response",
3840
"thread_creation_response",
3941
"thread_creation_footer",
4042
"thread_creation_title",
@@ -89,7 +91,7 @@ class ConfigManager:
8991

9092
colors = {"mod_color", "recipient_color", "main_color"}
9193

92-
time_deltas = {"account_age", "guild_age"}
94+
time_deltas = {"account_age", "guild_age", "thread_auto_close"}
9395

9496
valid_keys = allowed_to_change_in_command | internal_keys | protected_keys
9597

core/thread.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import asyncio
22
import logging
3-
import re
43
import os
4+
import re
55
import string
66
import typing
7-
from types import SimpleNamespace as param
87
from datetime import datetime, timedelta
8+
from types import SimpleNamespace as param
99

1010
import discord
11+
import isodate
1112
from discord.ext.commands import MissingRequiredArgument, CommandError
1213

14+
from core.time import human_timedelta
1315
from core.utils import is_image_url, days, match_user_id
1416
from core.utils import truncate, ignore, error
1517

@@ -39,6 +41,7 @@ def __init__(
3941
self.genesis_message = None
4042
self._ready_event = asyncio.Event()
4143
self._close_task = None
44+
self._auto_close_task = None
4245

4346
def __repr__(self):
4447
return (
@@ -218,6 +221,10 @@ async def _close(
218221

219222
await self.cancel_closure()
220223

224+
# Cancel auto closing the thread if closed by any means.
225+
if self._auto_close_task:
226+
self._auto_close_task.cancel()
227+
221228
if str(self.id) in self.bot.config.subscriptions:
222229
del self.bot.config.subscriptions[str(self.id)]
223230

@@ -336,6 +343,72 @@ async def _find_thread_message(channel, message_id):
336343
if str(message_id) == str(embed.author.url).split("/")[-1]:
337344
return msg
338345

346+
async def _grab_timeout(
347+
self
348+
) -> typing.Union[None, isodate.duration.Duration, timedelta]:
349+
"""
350+
This grabs the timeout value for closing threads automatically
351+
from the ConfigManager and parses it for use internally.
352+
353+
:returns: None if no timeout is set.
354+
"""
355+
timeout = self.bot.config.get("thread_auto_close")
356+
if timeout is None:
357+
return timeout
358+
else:
359+
try:
360+
timeout = isodate.parse_duration(timeout)
361+
except isodate.ISO8601Error:
362+
logger.warning(
363+
"The auto_close_thread limit needs to be a "
364+
"ISO-8601 duration formatted duration string "
365+
'greater than 0 days, not "%s".',
366+
str(timeout),
367+
)
368+
del self.bot.config.cache["thread_auto_close"]
369+
await self.bot.config.update()
370+
timeout = None
371+
return timeout
372+
373+
async def _restart_close_timer(self):
374+
"""
375+
This will create or restart a timer to automatically close this
376+
thread.
377+
"""
378+
timeout = await self._grab_timeout()
379+
380+
# Exit if timeout was not set
381+
if timeout is None:
382+
return
383+
384+
# Set timeout seconds
385+
seconds = timeout.total_seconds()
386+
# seconds = 20 # Uncomment to debug with just 20 seconds
387+
reset_time = datetime.utcnow() + timedelta(seconds=seconds)
388+
human_time = human_timedelta(dt=reset_time)
389+
390+
# Grab message
391+
close_message = self.bot.config.get(
392+
"thread_auto_close_response",
393+
f"This thread has been closed automatically after no response from"
394+
f" you for {human_time}."
395+
)
396+
time_marker_regex = "%t"
397+
if len(re.findall(time_marker_regex, close_message)) == 1:
398+
close_message = re.sub(time_marker_regex, str(human_time), close_message)
399+
elif len(re.findall(time_marker_regex, close_message)) > 1:
400+
logger.warning(
401+
"The thread_auto_close_response should only contain one"
402+
f" '{time_marker_regex}' to specify time."
403+
)
404+
405+
if self._auto_close_task:
406+
self._auto_close_task.cancel()
407+
self._auto_close_task = self.bot.loop.call_later(
408+
seconds, self._close_after, self.bot.user, False, True,
409+
close_message
410+
)
411+
339412
async def edit_message(self, message_id: int, message: str) -> None:
340413
recipient_msg, channel_msg = await asyncio.gather(
341414
self._find_thread_message(self.recipient, message_id),
@@ -422,6 +495,8 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None
422495
)
423496
)
424497

498+
await self._restart_close_timer()
499+
425500
if self.close_task is not None:
426501
# Cancel closing if a thread message is sent.
427502
await self.cancel_closure()
@@ -464,6 +539,10 @@ async def send(
464539
if not from_mod and not note:
465540
self.bot.loop.create_task(self.bot.api.append_log(message, self.channel.id))
466541

542+
# Cancel auto closing if we get a new message from user
543+
if self._auto_close_task:
544+
self._auto_close_task.cancel()
545+
467546
destination = destination or self.channel
468547

469548
author = message.author

0 commit comments

Comments
 (0)