Exemple #1
0
def _register(client, tokens, message, server, profile, msg, help=None):
    """
    # EPIC Helper Registration Help
    «Register your server for use with Epic Reminder. This extra step is required to prevent general abuse of the bot.
    If you ask for a join code, I will probably give you one.»

    ## Usage
        • `rcd register|join <join_code>`

    ## Example
        • `rcd register asdf` attempts to register the server using the join code `asdf`
    """
    if help or len(tokens) == 1:
        return {"msg": HelpMessage(_register.__doc__)}
    if server:
        return {"msg": NormalMessage(f"{message.channel.guild.name} has already joined! Hello again!", title="Hi!")}
    if len(tokens) > 1:
        cmd, *args = tokens
    else:
        cmd, args = tokens[0], ()
    if not args:
        return {"msg": ErrorMessage("You must provide a join code to register.", title="Registration Error")}
    join_code = JoinCode.objects.filter(code=args[0], claimed=False).first()
    if not join_code:
        return {"msg": ErrorMessage("That is not a valid Join Code.", title="Invalid Join Code")}
    server = Server.objects.create(id=message.channel.guild.id, name=message.channel.guild.name, code=join_code)
    join_code.claim()
    return {"msg": SuccessMessage(f"Welcome {message.channel.guild.name}!", title="Welcome!")}
def handle_rcd_command(
    client, tokens, message, server, profile, msg, help=None, error=None, coro=None
) -> HandlerResult:
    _msg, _coro = msg, (None, ())
    if (error and not isinstance(error, str)) or not msg:
        original_tokens = tokenize(message.content[:250], preserve_case=True)
        _msg = ErrorMessage(f"`{' '.join(original_tokens)}` could not be parsed as a valid command.")
    elif error:
        _msg = ErrorMessage(error)
    if coro and inspect.iscoroutinefunction(coro[0]):
        _coro = coro
    return [_msg], _coro
Exemple #3
0
def multiplier(client, tokens, message, server, profile, msg, help=None):
    """
    # Multiplier Help
    «Set a multiplier on your cooldowns to extend or reduce their frequency.
    E.g. if you are a tier 3 donator, you may want to set your multiplier
    to `0.65`.»

    Multipliers must be from [0 to 10).

    ## Usage
        • `rcd multiplier|mp <multiplier>`
    ## Examples
    ```
    rcd multiplier 0.65
    rcd mp 0.85
    rcd mp default
    ```
    """
    command = tokens[0]
    if help or len(tokens) == 1:
        return {
            "msg": HelpMessage(
                multiplier.__doc__, fields=[("Info", f"Your current multiplier is `{profile.cooldown_multiplier}`")]
            )
        }
    elif len(tokens) != 2:
        return {
            "msg": ErrorMessage(
                f"Could not parse `rcd {command} {' '.join(tokens)}` as a valid multiplier "
                f"command; your input has more arguments that expected."
            )
        }
    if tokens[1] == "default":
        profile.cooldown_multiplier = None
    else:
        try:
            profile.cooldown_multiplier = decimal.Decimal(tokens[1])
        except:  # noqa
            return {
                "msg": ErrorMessage(
                    f"Could not parse `{tokens[1]}` as a valid multiplier; must be a decimal number or `default`."
                )
            }
    try:
        profile.full_clean()
    except ValidationError as e:
        return {"msg": ErrorMessage(f"Could not validate your multiplier; err={e.args[0]['cooldown_multiplier'][0]}.")}
    except Exception as e:
        return {"msg": ErrorMessage(f"Could not validate your multiplier for unknown reasons :(; err={e}.")}
    profile.save()
    return {"msg": SuccessMessage(f"Your Cooldown Multiplier is now `{tokens[1]}`.")}
Exemple #4
0
def stats(client, tokens, message, server, profile, msg, help=None):
    """
    # Stats Help
    This command shows the output of {long} stats that the helper bot has managed to collect.

    ## Usage
        • `rcd {long}|{short} [num_minutes] [@player=@you]`

    ## Examples
        • `rcd {long}` show your own {long} stats
        • `rcd {long} 3` for the last 3 minutes
        • `rcd {short} @player` show a player's {long} stats
    """
    minutes, _all = None, False
    long, short = stats.entry_tokens[tokens[0]]
    if help:
        return {"msg": HelpMessage(stats.__doc__.format(long=long, short=short))}
    if len(tokens) > 1:
        mentioned_profile = Profile.from_tag(tokens[-1], client, server, message)
        if mentioned_profile:
            tokens = tokens[:-1]
            profile = mentioned_profile
        elif tokens[-1] == "all":
            _all = True
            tokens = tokens[:-1]
        if re.match(r"^([0-9\* ]+)$", tokens[-1]):
            min_tokens, prod = tokens[-1].replace(" ", "").split("*"), 1
            minutes = functools.reduce(operator.mul, [int(t) for t in min_tokens], 1)
        if not mentioned_profile and not minutes and not _all:
            return {
                "msg": ErrorMessage(
                    f"`rcd {' '.join(tokens)}` is not valid invocation of `rcd {long}`. "
                    f"Example usage: `rcd {short} 5 @player`",
                    title="Stats Usage Error",
                )
            }
    uid = None if _all else profile.uid
    # show all for an individual regardless of which server,
    # but if _all=True then we want to restrict to current server
    server_id = server.id if _all else None
    name = server.name if _all else client.get_user(int(profile.uid)).name
    if long == "gambling":
        return {
            "msg": NormalMessage(
                "", fields=Gamble.objects.stats(uid, minutes, server_id), title=f"{name}'s Gambling Addiction"
            )
        }
    elif long == "hunts":
        return {
            "msg": NormalMessage("", fields=Hunt.objects.hunt_stats(uid, minutes, server_id), title=f"{name}'s Carnage")
        }
    elif long == "drops":
        return {
            "msg": NormalMessage("", fields=Hunt.objects.drop_stats(uid, minutes, server_id), title=f"{name}'s Drops")
        }
Exemple #5
0
def logs(client, tokens, message, server, profile, msg, help=None):
    """
    # Logs Help
    Ask for the future log value of your current inventory!

    «You may need to know how many logs you will have in A10 before you can decide whether
    you should progress to the next area. This command will tell you how many logs you
    will have in area 10 based on your current inventory.»

    The command assumes you are in A5 if no area is provided.

    ## Usage
        • `rcd logs [a{n}=a5] [@player=@you]`

    ## Examples
        • `rcd logs` assuming that I am in A5, how many logs am I gonna have in A10?
        • `rcd logs a7` now that I am in A7, how many logs am I gonna have in A10?
        • `rcd logs @kevin` how many logs is Kevin gonna have in area A10?

    """
    if help or not tokens:
        return {"msg": HelpMessage(logs.__doc__)}

    full_message, metadata = " ".join(tokens), {"area": 5}
    area_indicator = re.search(r" ([aA])?(\d{1,2})", full_message)
    if area_indicator:
        area = int(area_indicator.groups()[1])
        start, end = area_indicator.span()
        tokens, metadata["area"] = tokenize(f"{full_message[0:start]}{full_message[end:]}"), area
    if not 1 <= metadata["area"] <= 15:
        return {"msg": ErrorMessage("Only areas 1-15 are valid!", title="Logs Error")}

    mentioned_profile = Profile.from_tag(tokens[-1], client, server, message)
    if mentioned_profile:
        profile, metadata["snoop"] = mentioned_profile, profile.uid

    open_sentinels = list(Sentinel.objects.filter(trigger=0, profile=profile, action="logs"))
    len(open_sentinels) == 0 and Sentinel.objects.create(trigger=0, profile=profile, action="logs", metadata=metadata)
    for sentinel in open_sentinels:
        sentinel.metadata.get("snoop", -1) == metadata.get("snoop", -1) and sentinel.update(metadata=metadata)

    _area = f'Area {metadata["area"]}'
    if metadata.get("snoop", None):
        return {
            "msg": NormalMessage(
                "Busybody, eh? Okay, I'll check next time they open their inventory.", title=f"Snoop Lawgs ({_area})"
            )
        }
    return {
        "msg": NormalMessage(
            "Okay, the next time I see your inventory, I'll say how many logs you should have in Area 10.",
            title=f"Logs ({_area})",
        )
    }
Exemple #6
0
def dibbs(client, tokens, message, server, profile, msg, help=None):
    """
    # Dibbs Help
    «Call "dibbs" on the next guild raid. To register your guild with EPIC Rpg Helper, you can run
    `rcd list`.»

    ## Usage
        • `rcd dibbs|d[?] [undo]`

    ## Examples
        • `rcd dibbs` Call dibbs on next guild raid
        • `rcd dibbs undo` Undo your dibbs call
        • `rcd dibbs?` Find out if anyone has dibbs without claiming it
    """
    if help:
        return {"msg": HelpMessage(dibbs.__doc__)}

    if not profile.player_guild:
        return {"msg": ErrorMessage(":disappointed: You aren't part of a guild.")}
    tz, tf = pytz.timezone(profile.timezone), profile.time_format
    after_message = (
        f" at `{profile.player_guild.after.astimezone(tz).strftime(tf)}`" if profile.player_guild.after else ""
    )
    raid_dibbs = profile.player_guild.raid_dibbs
    if len(tokens) > 1 and tokens[1] == "undo":
        if not raid_dibbs or profile.uid != raid_dibbs.uid:
            return {
                "msg": NormalMessage(
                    f"Well, you never actually had dibbs, " f"but at least now everyone knows you didn't want it."
                )
            }
        profile.player_guild.update(raid_dibbs=None)
        return {"msg": SuccessMessage(f"Great! No more dibbs for you!", title="Undibbsed!")}

    if tokens[0][-1] == "?":
        if raid_dibbs:
            player_with_dibbs = client.get_user(int(raid_dibbs.uid))
            return {"msg": NormalMessage(f"**{player_with_dibbs}** has dibbs on the next guild raid{after_message}.")}
        else:
            return {"msg": NormalMessage(f"No one has dibbs on the next guild raid{after_message}.")}
    if not raid_dibbs:
        profile.player_guild.update(raid_dibbs=profile)
        return {
            "msg": SuccessMessage(f"Okay! You've got dibbs on the next guild raid{after_message}!", title="Dibbsed!")
        }
    elif raid_dibbs == profile:
        return {
            "msg": NormalMessage(f"You've already got dibbs on the next guild raid{after_message}!", title="Dibbsed!")
        }
    else:
        player_with_dibbs = client.get_user(int(raid_dibbs.uid))
        return {"msg": NormalMessage(f"Sorry, **{player_with_dibbs}** already has dibbs.", title="Not this time!")}
Exemple #7
0
def scrape(client, tokens, message, server, profile, msg, help=None):
    """
    # Help Scrape
    Scrape the contents of this channel for future reference.

    ## Usage
        • `rcd admin scrape [limit]`
    ## Examples:
        • `rcd admin scrape all`: Scrape all contents in all known channels
        • `rcd admin scrape 10`: Scrape last 10 messages from this channel
    """
    if help:
        return {"msg": HelpMessage(scrape.__doc__)}
    limit = None
    if len(tokens) > 1:
        if not tokens[-1].isdigit() and not tokens[-1] == "all":
            return {"msg": ErrorMessage(f"scrape limit must be an integer, got {tokens[-1]}.")}
        elif tokens[-1].isdigit():
            limit = tokens[-1]
    channels = [client.get_channel(c.id) for c in Channel.objects.all()] if "all" in tokens else None
    scope = "all" if not limit else f"last {limit}"
    if channels:

        async def _scrape_channels() -> HandlerResult:
            files, elapsed = await scrape_channels(channels, limit)
            newline = "\n"
            return [
                SuccessMessage(
                    f"Scrape completed in {elapsed} seconds.",
                    title="Scrape Completed.",
                    fields=(("Files", f"The following files were generated: ```\n{newline.join(files)}\n```"),),
                )
            ], (None, ())

        return {
            "msg": SuccessMessage(f"Beginning Scrape of {scope} messages in all known channels."),
            "coro": (_scrape_channels, ()),
        }
    else:

        async def _scrape_channel() -> HandlerResult:
            start = int(time.time())
            file = f"/tmp/{start}_{message.channel.id}_dump.json"
            file, elapsed = await scrape_channel(message.channel, start, file, limit)
            return [
                f"<@!{message.author.id}> Your scrape has completed " f"after {elapsed:,} seconds. Results in `{file}`."
            ], (None, ())

        return {
            "msg": SuccessMessage(f"Beginning Scrape of {scope} messages in this channel."),
            "coro": (_scrape_channel, ()),
        }
Exemple #8
0
def timezone(client, tokens, message, server, profile, msg, help=None):
    """
    # Timezone Help
    Set your timezone. This only effects the time displayed in `rcd cd`; notification functionality is not effected.

    ## Usage
        • `rcd timezone <timezone>`: Sets your timezone to the provided timezone
    ## Examples
        • `rcd timezone America/Chicago`: Sets your timezone to America/Chicago
        • `rcd tz default` Sets your timezone back to the default.
    """
    current_time = datetime.datetime.now().astimezone(pytz.timezone(profile.timezone))
    if help or len(tokens) == 1:
        return {
            "msg": HelpMessage(
                timezone.__doc__,
                fields=[
                    (
                        "Info",
                        f"Current time with your time format `{profile.time_format}` "
                        f"in your timezone `{profile.timezone}` is {current_time.strftime(profile.time_format)}. \n"
                        f"[Visit this page to see a list of timezones.](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)",
                    )
                ],
            )
        }
    if len(tokens) == 2:
        # need case sensitive tokens to get correct timezone name,
        # need case insensitive tokens to get relative position in of
        # tz|timezone token from user input
        tokens = tokenize(message.content[:250], preserve_case=True)
        tz = tokens[-1]
        if tz.lower() == "default":
            tz = Profile.DEFAULT_TIMEZONE
        elif tz not in set(map(lambda x: x[0], Profile.TIMEZONE_CHOICES)):
            return {"msg": ErrorMessage(f"{tz} is not a valid timezone.")}
        else:
            profile.update(timezone=tz)
            return {
                "msg": SuccessMessage(
                    f"**{message.author.name}'s** timezone has been set to **{tz}**.",
                    fields=[
                        (
                            "Info",
                            f"Current time with your time format `{profile.time_format}` "
                            f"in your timezone `{profile.timezone}` is {current_time.strftime(profile.time_format)}. ",
                        )
                    ],
                )
            }
Exemple #9
0
def event(client, tokens, message, server, profile, msg, help=None):
    """
    # Event Help
    Create and activate an event that has cooldown modifications. This will be active for all users.

    ## Usage
    • `rcd admin event upsert|show|delete "NAME" [param=value {param=value ...}]`
    • `rcd admin event show NAME`
    ## Example 1
    «The below command will create or update the event XMAS 2020 to start at `2020-12-01T00:00` UTC,
    end at `2020-01-01T23:55` UTC, and have cooldown adjustment for arena.»
    ```
    rcd event upsert "XMAS 2020" start=2020-12-01 end=2020-01-01T23:55 arena="12h 35s"
    ```

    ## Example 2
    «The below command will create a special event with flat multipliers to shorten cooldowns.»
    ```
    rcd event upsert "100k" start=2022-01-15 duration="100000s" lootbox=0.25 adventure=0.25 horse=0.25 dungeon=0.25 arena=0.25
    ```
    """
    if help or len(tokens) < 3 or tokens[1] not in {"upsert", "show", "delete"}:
        return {"msg": HelpMessage(event.__doc__)}
    if tokens[1] == "upsert":
        if len(tokens) > 3:
            _event = Event.parse_event(tokens[3:], tokens[2], tz=profile.timezone)
            try:
                _event.save()
            except Exception as e:
                return {"msg": ErrorMessage(f"Encountered error executing command` {' '.join(tokens)}`; err = {e}")}
        else:
            return {
                "msg": ErrorMessage(
                    f'`rcd admin event {tokens[1]} "{tokens[2]}"` could not be parsed as a valid command. '
                    "Did you provide all required arguments?"
                )
            }
    elif tokens[1] == "delete":
        _event = Event.parse_event([], tokens[2])
        try:
            _event.delete()
            return {"msg": SuccessMessage(f'Event "{tokens[2]}" successfully deleted.', title="Delete Success")}
        except Exception as e:
            return {"msg": ErrorMessage(f"Encountered error executing command` {' '.join(tokens)}`; err = {e}")}
    else:
        _event = Event.parse_event([], tokens[2], upsert=False, tz=profile.timezone)
    fields = [
        (
            "Effective (UTC)",
            f'{_event.start.strftime("%Y-%m-%dT%H:%M")} to {_event.end.strftime("%Y-%m-%dT%H:%M")}',
        ),
    ]
    if _event.cooldown_adjustments or _event.cooldown_multipliers:
        duration_format = "{:>1}d {:>02}h {:02}m {:02}s"
        if _event.cooldown_adjustments:
            output = ""
            for key in _event.cooldown_adjustments:
                delta = datetime.timedelta(seconds=_event.cooldown_adjustments[key])
                output += f"{key:12} => {duration_format.format(*to_human_readable(delta))}\n"
            fields.append(("Cooldown Adjustments", f"```{output}```"))
        if _event.cooldown_multipliers:
            output = ""
            for key, value in _event.cooldown_multipliers.items():
                output += f"{key:12} => {value:.2f}\n"
            fields.append(("Cooldown Multipliers", f"```{output}```"))

        output = ""
        for key in {*_event.cooldown_adjustments, *_event.cooldown_multipliers}:
            multiplier = _event.cooldown_multipliers.get(key, 1)
            duration = datetime.timedelta(
                seconds=_event.cooldown_adjustments.get(key, int(CoolDown.COOLDOWN_MAP[key].total_seconds()))
            )
            output += f"{key:12} => {duration_format.format(*to_human_readable(duration*multiplier))}\n"
        fields.append(("Event Cooldowns", f"```{output}```"))
    return {"msg": NormalMessage("", title=f'{tokens[1]} "{tokens[2]}"', fields=fields)}
Exemple #10
0
def timeformat(client, tokens, message, server, profile, msg, help=None):
    """
    # Time Format Help
    «Set the time format for the output of `rcd` using Python
    `strftime` notation. Defaults to `%I:%M:%S %p, %m/%d`. If
    you don't know what that means, see the linked resource below.»

    ## Usage
        • `rcd timeformat|tf "<format_string>"`
    ##Examples
        • `rcd timeformat "%I:%M:%S %p, %m/%d"` **Notice the quotes.** Very important!
        • `rcd tf "%Y-%m-%d %H:%M:%S"`
        • `rcd tf default` Restore your time format to default.
    """

    itokens = tokenize(message.content[:250], preserve_case=True)
    current_time = datetime.datetime.now().astimezone(pytz.timezone(profile.timezone)).strftime(profile.time_format)
    if help or len(tokens) == 1:
        return {
            "msg": HelpMessage(
                timeformat.__doc__,
                fields=[
                    (
                        "Info",
                        f"Current time with your time format `{profile.time_format}` "
                        f"in your timezone `{profile.timezone}` is {current_time}. \n"
                        "[Visit here for documentation on time format strings.]"
                        "(https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)",
                    )
                ],
            )
        }
    elif len(tokens) != 2:
        return {
            "msg": ErrorMessage(
                f"Could not parse {' '.join(itokens)} as a valid timeformat command; "
                "your input had more arguments than epected. "
                "Did you make sure to quote your format string?",
                title="Parse Error",
            )
        }
    # ensure tokens and itokens are the same lenth.
    time_format = itokens[-1]
    if len(time_format) > Profile.MAX_TIME_FORMAT_LENGTH:
        return {
            "msg": ErrorMessage(
                f"Your specified time format `{timeformat}` is too long. "
                f"It should have {len(time_format) - Profile.MAX_TIME_FORMAT_LENGTH} "
                "fewer characters.",
                title="Are you being naughty?",
            )
        }
    if time_format.lower() == "default":
        time_format = Profile.DEFAULT_TIME_FORMAT
    try:
        current_time = datetime.datetime.now().astimezone(pytz.timezone(profile.timezone)).strftime(time_format)
        profile.update(time_format=time_format)
        return {
            "msg": SuccessMessage(
                f"Great! You set your time format to `{time_format}`. " f"The current time is {current_time}",
                title="Good job!",
            )
        }
    except Exception as e:
        # I honestly am not sure if this is possible
        return {
            "msg": ErrorMessage(
                f"Great... Your time format `{time_format}` broke something; " f"err = {e}", "Oh boy Oh geez"
            )
        }
Exemple #11
0
def checklist(client, tokens, message, server, profile, msg, help=None):
    """
    # Checklist Help
    Use this command to determine what you should be doing in a particular area.

    ## Usage
        • `rcd cl <area>`: Shows what you should be doing in this area
        • `rcd checklist <area>`: Shows what you should be doing in this area

    ## Examples
    ```
    rcd cl a5
    rcd checklist 15
    ```
    """
    full_message = " ".join(tokens)
    area_indicator = re.search(r" ([aA])?(\d{1,2})", full_message)
    if help or not area_indicator:
        return HelpMessage(checklist.__doc__)
    area_id = int(area_indicator.groups()[1])
    area = Area.objects.prefetch_related("dungeons").filter(id=area_id).first()
    if not area:
        return ErrorMessage(f"No such area A{area_id}", title="Checklist Error")
    sections = []
    if area.activities and isinstance(area.activities, list):
        sections.append(("**Activities**", "\n".join(f"• {text}" for text in area.activities)))

    if area.ascended_activities and isinstance(area.ascended_activities, list):
        sections.append(("**Ascended Activities**", "\n".join(f"• {text}" for text in area.ascended_activities)))

    if area.trades and isinstance(area.trades, list):
        sections.append(("**Before Dungeon**", "\n".join(f"• {text}" for text in area.trades)))
    else:
        sections.append(("**Before Dungeon**", "No trades necessary."))

    for dungeon in area.dungeons.all():
        carry = f"{dungeon.carry} Defense" if dungeon.carry else "N/A"
        sections.append(
            (
                f"**{dungeon.name} Recommendations**",
                (
                    f"`{'Level':<7} => {dungeon.level or 'N/A':<30}`\n"
                    f"`{'Sword':<7} => {dungeon.sword or 'N/A':<30}`\n"
                    f"`{'Armor':<7} => {dungeon.armor or 'N/A':<30}`\n"
                    f"`{'Attack':<7} => {dungeon.attack or 'N/A':<30}`\n"
                    f"`{'Defense':<7} => {dungeon.defense or 'N/A':<30}`\n"
                    f"`{'HP':<7} => {dungeon.life or 'N/A':<30}`\n"
                    f"`{'Carry':<7} => {carry:<30}`"
                ),
            )
        )
        if dungeon.description:
            sections.append((f"{dungeon.name} Description", dungeon.description))

    if area.id > 10:
        sections.append(
            (
                "**Before Time Travel**",
                "\n".join(
                    f"• {text}"
                    for text in [
                        "completely ignore holiday items",
                        "dismantle all things (unless working on pr)",
                        "trade all to apple (to sell) if ascended, otherwise work on pr",
                        "sell all things (but not arena cookies)",
                        "sell sword & armor",
                    ]
                ),
            )
        )

    return NormalMessage("" if sections else "Sorry, nothing yet.", title=f"**{area.name} Checklist**", fields=sections)
Exemple #12
0
def info(client, tokens, message, server, profile, msg, help=None):
    """
    # Info Help
    Info about the bot and other supported topics.
    Note: This command is in early development and the invocation format is likely to change.

    ## Topics
    • `0`, `cooldown mechanics`: Detailed EPIC Rpg Helper cooldown mechanics
    • `1`, `default cooldowns`: EPIC Rpg's default cooldowns for all commands
    • `2`, `global cooldowns`: EPIC Rpg Helper's active global cooldowns
    • `3`, `my cooldowns`: My current calculated cooldown durations

    ## Usage
    • `rcd info|i <topic>|<topic number>`

    ## Examples
    ```
    rcd info 1
    rcd info default cooldowns
    ```
    """
    if help or len(tokens) == 1:
        return {"msg": HelpMessage(info.__doc__)}
    topic = " ".join(tokens[1:])
    if topic in {"bot", "0"}:
        return {
            "msg": NormalMessage(
                """
            # Cooldown Calculations
            Cooldowns are determined in various ways which depend on how EPIC Rpg responds to your commands.

            ## Normal Cooldown Calculation
            «If EPIC Rpg Helper determines that you have used a a valid command, it will attempt to calculate your new
            cooldown duration for that command, taking into account any active global events and any active
            multipliers you may have (see `rcd h multiplier`).»

            «In most cases, EPIC Rpg will respond with a standard cooldown response to tell you
            exactly how long it will be before you can use the command again. EPIC Rpg Helper will
            read this respond and schedule a notification based on it.»

            ## Group Cooldowns
            «For commands involving one or more people, EPIC Rpg Helper must also watch for EPIC Rpg reponses
            to determine whether a group command completed successfully and thus will go on cooldown as a result.»

            ## Pet Cooldowns
            «Because it is impossible to definitively determine who a pet cooldown response is for,
            EPIC Rpg Helper does not attempt to use the cooldown response to schedule a notification.
            Instead, it relies on the player to open their pet screen. It will schedule a notification for the
            pet which will return soonest according to the last pet screen it saw.»
            """
            )
        }

    cooldown_map = CoolDown.COOLDOWN_MAP.copy()
    format, output = "{:>1}d {:>02}h {:02}m {:02}s", ""
    if topic in {"default cooldowns", "1"}:
        title = "EPIC RPG Default Cooldowns"
        for key, delta in sorted(cooldown_map.items(), key=lambda x: x[1]):
            output += f"{key:12} => {format.format(*to_human_readable(delta))}\n"
        return {"msg": NormalMessage(f"```{output}```", title=title)}
    if topic in {"global cooldowns", "2"}:
        title = "Global Cooldowns, Including Event Data"
        cooldown_map, events = CoolDown.get_event_cooldown_map()
        for key, delta in sorted(cooldown_map.items(), key=lambda x: x[1]):
            output += f"{key:12} => {format.format(*to_human_readable(delta))}\n"
        event_info = ", ".join(events) + " event(s) currently active." if events else "No active events."
        return {"msg": NormalMessage(f"```{output}```", title=title, footer=event_info)}
    elif topic in {"my cooldowns", "3"}:
        title = "My Cooldowns, Including Event Data and Multipliers"
        cooldown_map, events = CoolDown.get_event_cooldown_map()
        mp = profile.cooldown_multiplier
        for key, delta in sorted(cooldown_map.items(), key=lambda x: x[1]):
            delta = CoolDown.apply_multiplier(mp, delta, key) if mp else delta
            output += f"{key:12} => {format.format(*to_human_readable(delta))}\n"
        event_info = ", ".join(events) + " event(s) currently active." if events else "No active events."
        return {"msg": NormalMessage(f"```{output}```", title=title, footer=event_info)}
    return {"msg": ErrorMessage(f"No such topic `{topic}`. ", title="Info Error")}