diff --git a/redbot/core/events.py b/redbot/core/events.py index dfbc79a811b..a5ef5240452 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -4,6 +4,7 @@ import datetime import logging import traceback +import asyncio from datetime import timedelta from typing import List @@ -15,7 +16,7 @@ from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands from .data_manager import storage_type -from .utils.chat_formatting import inline, bordered, format_perms_list +from .utils.chat_formatting import inline, bordered, format_perms_list, humanize_timedelta from .utils import fuzzy_command_search, format_fuzzy_results log = logging.getLogger("red") @@ -242,8 +243,22 @@ async def on_command_error(ctx, error): elif isinstance(error, commands.NoPrivateMessage): await ctx.send("That command is not available in DMs.") elif isinstance(error, commands.CommandOnCooldown): + if error.retry_after < 1: + async with ctx.typing(): + # the sleep here is so that commands using this for ratelimit purposes + # are not made more lenient than intended, while still being + # more convienient for the user than redoing it less than a second later. + await asyncio.sleep(error.retry_after) + await ctx.bot.invoke(ctx) + # done this way so checks still occur if there are other + # failures possible than just cooldown. + # do not change to ctx.reinvoke() + return + await ctx.send( - "This command is on cooldown. Try again in {:.2f}s".format(error.retry_after) + "This command is on cooldown. Try again in {}.".format( + humanize_timedelta(seconds=error.retry_after) + ) ) else: log.exception(type(error).__name__, exc_info=error) diff --git a/redbot/core/utils/chat_formatting.py b/redbot/core/utils/chat_formatting.py index f0fb8aa2141..23d4a5e4e5e 100644 --- a/redbot/core/utils/chat_formatting.py +++ b/redbot/core/utils/chat_formatting.py @@ -1,5 +1,6 @@ import itertools -from typing import Sequence, Iterator, List +import datetime +from typing import Sequence, Iterator, List, Optional import discord @@ -204,7 +205,7 @@ def pagify( priority: bool = False, escape_mass_mentions: bool = True, shorten_by: int = 8, - page_length: int = 2000 + page_length: int = 2000, ) -> Iterator[str]: """Generate multiple pages from the given text. @@ -386,3 +387,37 @@ def format_perms_list(perms: discord.Permissions) -> str: perm_name = '"' + perm.replace("_", " ").title() + '"' perm_names.append(perm_name) return humanize_list(perm_names).replace("Guild", "Server") + + +def humanize_timedelta( + *, timedelta: Optional[datetime.timedelta] = None, seconds: Optional[int] = None +) -> str: + """ + Get a human timedelta representation + """ + + try: + obj = seconds or timedelta.total_seconds() + except AttributeError: + raise ValueError("You must provide either a timedelta or a number of seconds") + + seconds = int(obj) + periods = [ + (_("year"), _("years"), 60 * 60 * 24 * 365), + (_("month"), _("months"), 60 * 60 * 24 * 30), + (_("day"), _("days"), 60 * 60 * 24), + (_("hour"), _("hours"), 60 * 60), + (_("minute"), _("minutes"), 60), + (_("second"), _("seconds"), 1), + ] + + strings = [] + for period_name, plural_period_name, period_seconds in periods: + if seconds >= period_seconds: + period_value, seconds = divmod(seconds, period_seconds) + if period_value == 0: + continue + unit = plural_period_name if period_value > 1 else period_name + strings.append(f"{period_value} {unit}") + + return ", ".join(strings)