|
1 | 1 | import asyncio |
2 | 2 | import logging |
3 | | -import re |
4 | 3 | import os |
| 4 | +import re |
5 | 5 | import string |
6 | 6 | import typing |
7 | | -from types import SimpleNamespace as param |
8 | 7 | from datetime import datetime, timedelta |
| 8 | +from types import SimpleNamespace as param |
9 | 9 |
|
10 | 10 | import discord |
| 11 | +import isodate |
11 | 12 | from discord.ext.commands import MissingRequiredArgument, CommandError |
12 | 13 |
|
| 14 | +from core.time import human_timedelta |
13 | 15 | from core.utils import is_image_url, days, match_user_id |
14 | 16 | from core.utils import truncate, ignore, error |
15 | 17 |
|
@@ -39,6 +41,7 @@ def __init__( |
39 | 41 | self.genesis_message = None |
40 | 42 | self._ready_event = asyncio.Event() |
41 | 43 | self._close_task = None |
| 44 | + self._auto_close_task = None |
42 | 45 |
|
43 | 46 | def __repr__(self): |
44 | 47 | return ( |
@@ -231,6 +234,10 @@ async def _close( |
231 | 234 |
|
232 | 235 | await self.cancel_closure() |
233 | 236 |
|
| 237 | + # Cancel auto closing the thread if closed by any means. |
| 238 | + if self._auto_close_task: |
| 239 | + self._auto_close_task.cancel() |
| 240 | + |
234 | 241 | if str(self.id) in self.bot.config.subscriptions: |
235 | 242 | del self.bot.config.subscriptions[str(self.id)] |
236 | 243 |
|
@@ -349,6 +356,71 @@ async def _find_thread_message(channel, message_id): |
349 | 356 | if str(message_id) == str(embed.author.url).split("/")[-1]: |
350 | 357 | return msg |
351 | 358 |
|
| 359 | + async def _fetch_timeout( |
| 360 | + self |
| 361 | + ) -> typing.Union[None, isodate.duration.Duration, timedelta]: |
| 362 | + """ |
| 363 | + This grabs the timeout value for closing threads automatically |
| 364 | + from the ConfigManager and parses it for use internally. |
| 365 | +
|
| 366 | + :returns: None if no timeout is set. |
| 367 | + """ |
| 368 | + timeout = self.bot.config.get("thread_auto_close") |
| 369 | + if timeout is None: |
| 370 | + return timeout |
| 371 | + else: |
| 372 | + try: |
| 373 | + timeout = isodate.parse_duration(timeout) |
| 374 | + except isodate.ISO8601Error: |
| 375 | + logger.warning( |
| 376 | + "The auto_close_thread limit needs to be a " |
| 377 | + "ISO-8601 duration formatted duration string " |
| 378 | + 'greater than 0 days, not "%s".', |
| 379 | + str(timeout), |
| 380 | + ) |
| 381 | + del self.bot.config.cache["thread_auto_close"] |
| 382 | + await self.bot.config.update() |
| 383 | + timeout = None |
| 384 | + return timeout |
| 385 | + |
| 386 | + async def _restart_close_timer(self): |
| 387 | + """ |
| 388 | + This will create or restart a timer to automatically close this |
| 389 | + thread. |
| 390 | + """ |
| 391 | + timeout = await self._grab_timeout() |
| 392 | + |
| 393 | + # Exit if timeout was not set |
| 394 | + if timeout is None: |
| 395 | + return |
| 396 | + |
| 397 | + # Set timeout seconds |
| 398 | + seconds = timeout.total_seconds() |
| 399 | + # seconds = 20 # Uncomment to debug with just 20 seconds |
| 400 | + reset_time = datetime.utcnow() + timedelta(seconds=seconds) |
| 401 | + human_time = human_timedelta(dt=reset_time) |
| 402 | + |
| 403 | + # Grab message |
| 404 | + close_message = self.bot.config.get( |
| 405 | + "thread_auto_close_response", |
| 406 | + f"This thread has been closed automatically due to inactivity " |
| 407 | + f"after {human_time}.", |
| 408 | + ) |
| 409 | + time_marker_regex = "%t" |
| 410 | + if len(re.findall(time_marker_regex, close_message)) == 1: |
| 411 | + close_message = re.sub(time_marker_regex, str(human_time), close_message) |
| 412 | + elif len(re.findall(time_marker_regex, close_message)) > 1: |
| 413 | + logger.warning( |
| 414 | + "The thread_auto_close_response should only contain one" |
| 415 | + f" '{time_marker_regex}' to specify time." |
| 416 | + ) |
| 417 | + |
| 418 | + if self._auto_close_task: |
| 419 | + self._auto_close_task.cancel() |
| 420 | + self._auto_close_task = self.bot.loop.call_later( |
| 421 | + seconds, self._close_after, self.bot.user, False, True, close_message |
| 422 | + ) |
| 423 | + |
352 | 424 | async def edit_message(self, message_id: int, message: str) -> None: |
353 | 425 | recipient_msg, channel_msg = await asyncio.gather( |
354 | 426 | self._find_thread_message(self.recipient, message_id), |
@@ -435,6 +507,8 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None |
435 | 507 | ) |
436 | 508 | ) |
437 | 509 |
|
| 510 | + await self._restart_close_timer() |
| 511 | + |
438 | 512 | if self.close_task is not None: |
439 | 513 | # Cancel closing if a thread message is sent. |
440 | 514 | await self.cancel_closure() |
@@ -477,6 +551,10 @@ async def send( |
477 | 551 | if not from_mod and not note: |
478 | 552 | self.bot.loop.create_task(self.bot.api.append_log(message, self.channel.id)) |
479 | 553 |
|
| 554 | + # Cancel auto closing if we get a new message from user |
| 555 | + if self._auto_close_task: |
| 556 | + self._auto_close_task.cancel() |
| 557 | + |
480 | 558 | destination = destination or self.channel |
481 | 559 |
|
482 | 560 | author = message.author |
|
0 commit comments