Exemplo n.º 1
0
def marry(client, tokens, message, server, profile, msg, help=None):
    """
    # Marriage Help
    «Indicate that you are married to another player. Used to track `hunt together`
    results.»

    ## Usage
        • `rcd marry @<player>`
    ## Examples
    ```
    rcd marry @your_friend
    ```
    """
    partner = Profile.from_tag(tokens[-1], client, server, message)
    if not partner:
        return HelpMessage(marry.__doc__, title="Marriage Help")
    if partner.pk == profile.pk:
        return NormalMessage(
            "I'm afraid I can't let you marry yourself... you'll ruin my statistics!",
            title=":(",
            footer="and marriage is only about statistics...",
        )
    message = f"<@!{profile.pk}> and <@!{partner.pk}> got married!!?!? "
    message += random.choice(
        ["I give it like, 3 months, tops.", "What a time to be alive!", "It's a match made in heaven :heart_eyes:"]
    )
    profile.update(partner=partner)
    partner.update(parner=profile)
    return NormalMessage(message, title="Witness the Newlyweds :wedding:!")
Exemplo n.º 2
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")
        }
Exemplo n.º 3
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})",
        )
    }
Exemplo n.º 4
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!")}
Exemplo n.º 5
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!")}
Exemplo n.º 6
0
def ban(client, tokens, message, server, profile, msg, help=None):
    """
    # Ban Help

    Someone has been naughty...

    ## Usage
        • `rcd admin ban @player`
        • `rcd ban @player`
        • `rcd unban @player`
        • `rcd ban unban @player`
    """
    naughty = Profile.from_tag(tokens[-1], client, server, message)
    if not naughty:
        return HelpMessage(ban.__doc__)
    banned = "unban" not in tokens
    naughty.update(banned="unban" not in tokens)
    if banned:
        return NormalMessage(f"Okay, <@!{naughty.pk}> can no longer use `rcd` commands.", title=f"Player Banned :(")
    return NormalMessage(f"Okay, <@!{naughty.pk}> can use `rcd` commands!", title=f"Player Un-Banned :)")
Exemplo n.º 7
0
def _profile(client, tokens, message, server, profile, msg, help=None):
    """
    #Profile Help
    «When called without any arguments, e.g. `rcd profile` this will display profile-related information.
    Otherwise, it will treat your input as a profile related sub-command.»


    ## Sub Commands
        • `timezone`, `tz`: Set the timezone information for your profile
        • `timeformat`, `tf`: Set the date and time formatting for your profile
        • `multiplier`, `mp`: Reduce or increase your cooldown durations
        • `notify`, `n`: Set which cooldowns the bot will notify you for
        • `marry`: Indicate that you are married to another player

    ## Examples
        • `rcd profile` Displays your profile information
        • `rcd p tz <timezone>` Sets your timezone to the provided timezone.
        • `rcd p on` Enables notifications for your profile.
        • `rcd p notify hunt on` Turns on hunt notifications for your profile.
        • `rcd p hunt on` Turns on hunt notifications for your profile.
    """
    if help and len(tokens) == 1:
        return {"msg": HelpMessage(_profile.__doc__)}
    elif len(tokens) > 1:
        # allow other commands to be namespaced by profile if that's how the user calls it
        if tokens[1] in {
            *timezone.entry_tokens,
            *timeformat.entry_tokens,
            *multiplier.entry_tokens,
            *notify.entry_tokens,
            *marry.entry_tokens,
        }:
            return {"tokens": tokens[1:]}
        maybe_profile = Profile.from_tag(tokens[1], client, server, message)
        if not maybe_profile:
            return {"error": 1}
        profile = maybe_profile
    notifications = ""
    for k, v in model_to_dict(profile).items():
        if isinstance(v, bool):
            notifications += f"{':ballot_box_with_check:' if v else ':x:'} `{k:25}`\n"
    return {
        "msg": NormalMessage(
            "",
            fields=(
                ("Timezone", f"`{profile.timezone}`"),
                ("Time Format", f"`{profile.time_format}`"),
                ("Cooldown Multiplier", f"`{profile.cooldown_multiplier}`"),
                ("Married", f"To <@!{profile.partner_id}>" if profile.partner_id else "Nope."),
                ("Notifications Enabled", notifications),
            ),
        )
    }
Exemplo n.º 8
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)}
Exemplo n.º 9
0
def cd(client, tokens, message, server, profile, msg, help=None):
    """
    # EPIC Helper Cooldowns
    Display when your cooldowns are expected to be done.
    ## Usage
        • `rcd cd [<cooldown_types>]` shows all cooldowns
        • `rcd rd [<cooldown_types>]` shows ready cooldowns
    ## Examples
    ```
    rcd
    rcd cd
    rrd
    rcd rd
    rcd daily weekly
    ```
    """
    if help:
        return {"msg": HelpMessage(cd.__doc__)}

    if tokens[0] not in {"cd", "rd"}:
        # allow for implicit or default invocation of rcd
        tokens = ["cd", *tokens[1:]] if tokens[0] == "" else ["cd", *tokens]
    nickname = message.author.name
    cooldown_filter = lambda x: True  # show all filters by default

    if len(tokens) > 1:
        mentioned_profile = Profile.from_tag(tokens[-1], client, server, message)
        if mentioned_profile:
            profile = mentioned_profile
            cd_args = set(tokens[2:])
            nickname = profile.last_known_nickname
        else:
            cd_args = set(tokens[1:])
        # means there are cooldown type arguments to filter on
        if cd_args:
            cooldown_filter = lambda x: x in cd_args

    profile_tz = pytz.timezone(profile.timezone)
    now = datetime.datetime.now(tz=datetime.timezone.utc)
    default = datetime.datetime(1790, 1, 1, tzinfo=datetime.timezone.utc)
    msg, warn = "", False
    cooldowns = {
        _cd[0]: _cd[1]
        for _cd in CoolDown.objects.filter(profile_id=profile.pk).order_by("after").values_list("type", "after")
    }
    all_cooldown_types = set([c[0] for c in CoolDown.COOLDOWN_TYPE_CHOICES])
    if not profile.player_guild:
        all_cooldown_types = all_cooldown_types - {"guild"}
    else:
        warn = True if profile.player_guild.raid_dibbs else False
        cooldowns["guild"] = profile.player_guild.after if profile.player_guild.after else default
    selected_cooldown_types = sorted(
        filter(cooldown_filter, all_cooldown_types),
        key=lambda x: cooldowns[x] if x in cooldowns else default,
    )
    for cooldown_type in selected_cooldown_types:
        after = cooldowns.get(cooldown_type, None)
        if not after or after <= now:
            icon = ":warning:" if warn and cooldown_type == "guild" else ":white_check_mark:"
            warning = " (dibbs) " if warn and cooldown_type == "guild" else ""
            msg += f"{icon} `{cooldown_type + warning:15} {'Ready!':>20}` \n"
        elif tokens[0] == "cd":  # don't show if "rd" command
            cooldown_after = cooldowns[cooldown_type].astimezone(profile_tz)
            icon = ":warning:" if warn and cooldown_type == "guild" else ":clock2:"
            warning = " (dibbs) " if warn and cooldown_type == "guild" else ""
            msg += f"{icon} `{cooldown_type + warning:15} {cooldown_after.strftime(profile.time_format):>20}`\n"
    if not msg:
        msg = (
            "All commands on cooldown! (You may need to use `rpg cd` to populate your cooldowns for the first time.)\n"
        )
    event_info = ""
    active_events = Event.objects.active().values_list("event_name", flat=True)
    if active_events:
        event_info = ", ".join(active_events) + " event(s) currently active.\n"
    return NormalMessage(
        msg,
        title=f"**{nickname}'s** Cooldowns",
        footer=f"{event_info}Timezone is {profile.timezone}, change with rcd p tz.",
    )
Exemplo n.º 10
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)
Exemplo n.º 11
0
def notify(client, tokens, message, server, profile, msg, help=None):
    """
    «Manage your notification settings. Here you can specify which types of
    epic rpg commands you would like to receive reminders for. For example, you can
    enable or disable showing a reminder for when `rpg hunt` should be available. All reminders
    are enabled by default»

    ## Examples
        • `rcd notify on`, `rcd notify off`: Toggle the notification feature
        • `rcd notify hunt on`: Enable cd notifications for `rpg hunt`.
        • `rcd daily on`: Enable cd notifications for `rpg daily`.
        • `rcd n hunt off`: Enable notifications for `rpg hunt`
        • `rcd n weekly on`: Enable notifications for `rpg weekly`
        • `rcd n all off`: Disable all notification types (distinct from `rcd notify off`)

    ## Command Types
        • `all`
        • `daily`
        • `weekly`
        • `lootbox`
        • `vote`
        • `hunt`
        • `adventure`
        • `training`
        • `duel`
        • `quest`
        • `work` (chop, mine, fish, etc.)
        • `horse`
        • `arena`
        • `dungeon`
        • `guild`
        • `pet`
    """
    if help:
        return {"msg": HelpMessage(notify.__doc__)}

    toggle_all = False
    # allow implicit invocation of notify
    tokens = tokens[1:] if tokens[0] in {"notify", "n"} else tokens
    # make sure all passed tokens are valid cooldown type
    command_types, toggle = tokens[:-1], tokens[-1]
    for command_type in command_types:
        if command_type not in CoolDown.COOLDOWN_MAP:
            if command_type != "all":
                return {"error": 1}
            toggle_all = True

    # invocation of the toggle command
    if len(tokens) == 1:
        return {"tokens": [toggle]}

    on = toggle == "on"
    if toggle_all:
        kwargs = {command_name: on for command_name, _ in CoolDown.COOLDOWN_TYPE_CHOICES}
    else:
        kwargs = {command_type: on for command_type in command_types}
    profile.update(last_known_nickname=message.author.name, **kwargs)
    notification_type_string = ", ".join(kwargs.keys())
    if not profile.notify:
        return {
            "msg": NormalMessage(
                f"Notifications for `{notification_type_string}` are now {toggle} for **{message.author.name}** "
                "but you will need to turn on notifications before you can receive any. "
                "Try `rcd on` to start receiving notifications."
            )
        }
    return {
        "msg": SuccessMessage(
            f"Notifications for **{notification_type_string}** are now **{toggle}** for **{message.author.name}**."
        )
    }
Exemplo n.º 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")}