NabDev/NabBot

View on GitHub
cogs/utils/converter.py

Summary

Maintainability
A
0 mins
Test Coverage
#  Copyright 2019 Allan Galarza
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import functools
import re

import discord
from discord.ext import commands

from cogs.utils.context import NabCtx

TIBIA_CASH_PATTERN = re.compile(r'(\d*\.?\d*)k*$')


class InsensitiveMember(commands.IDConverter):
    """Converts to a :class:`discord.Member` or :class:`discord.User` object.

    This class replicates :class:`discord.ext.commands.MemberConverter`, but the lookup is case insensitive.

    Lookup order:
    1. By ID.
    2. By mention.
    3. By name (case insensitive)."""
    async def convert(self, ctx: NabCtx, argument):
        member = ctx.bot.get_member(argument, ctx.guild)
        if member is None:
            raise commands.BadArgument('Member "{}" not found.'.format(argument))
        return member


class ChannelOrMember(commands.Converter):
    """Converts to a TextChannel or Member object."""
    async def convert(self, ctx, argument):
        try:
            return await commands.TextChannelConverter().convert(ctx, argument)
        except commands.BadArgument:
            return await InsensitiveMember().convert(ctx, argument)


class InsensitiveRole(commands.IDConverter):
    """Convert to a :class:`discord.Role`. object.

    This class replicates :class:`discord.ext.commands.RoleConverter`, but the lookup is case insensitive.

    Lookup order:
    1. By ID.
    2. By mention.
    3. By name (case insensitive)."""

    async def convert(self, ctx, argument) -> discord.Role:
        argument = argument.replace("\"", "")
        guild = ctx.guild
        if not guild:
            raise commands.NoPrivateMessage()

        match = self._get_id_match(argument) or re.match(r'<@&([0-9]+)>$', argument)
        if match:
            result = guild.get_role(int(match.group(1)))
        else:
            result = discord.utils.find(lambda r: r.name.lower() == argument.lower(), guild.roles)
        if result is None:
            raise commands.BadArgument('Role "{}" not found.'.format(argument))
        return result


class BadTime(commands.BadArgument):
    pass


class BadStamina(commands.BadArgument):
    pass


class TimeString:
    def __init__(self, argument: str):
        compiled = re.compile(r"(?:(?P<days>\d+)d)?(?:(?P<hours>\d+)h)?(?:(?P<minutes>\d+)m)?(?:(?P<seconds>\d+)s)?")
        self.original = argument.strip()
        match = compiled.match(self.original)
        if match is None or not match.group(0):
            raise BadTime("That's not a valid time, try something like this: `1d7h` or `4h20m`")

        self.seconds = 0
        days = match.group('days')
        if days is not None:
            self.seconds += int(days) * 86400
        hours = match.group('hours')
        if hours is not None:
            self.seconds += int(hours) * 3600
        minutes = match.group('minutes')
        if minutes is not None:
            self.seconds += int(minutes) * 60
        seconds = match.group('seconds')
        if seconds is not None:
            self.seconds += int(seconds)

        if self.seconds < 0:
            raise BadTime("I can't go back in time.")

        if self.seconds > (60*60*24*60):
            raise BadTime("That's a bit too far in the future... Try less than 60 days.")


stamina_pattern = re.compile(r"(\d{1,2}):(\d{1,2})")


@functools.total_ordering
class Stamina:
    def __init__(self, argument):
        match = stamina_pattern.match(argument)
        if not match:
            raise BadStamina("Invalid stamina format, expected: `hh:mm`")
        self.hours = int(match.group(1))
        self.minutes = int(match.group(2))
        if self.minutes >= 60:
            raise BadStamina("Invalid stamina, minutes can't be 60 or greater.")
        if self.hours > 42:
            raise BadStamina("Invalid stamina, can't have more than 42 hours.")

    @property
    def seconds(self):
        return ((self.hours*60) + self.minutes) * 60

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.seconds == other.seconds
        return False

    def __lt__(self, other):
        return self.seconds < other.seconds


class TibiaNumber(int):
    """Parses numbers allowing the use of 'k' as a thousand suffix.

    The output is an integer, so decimals will be truncated after multiplying.

    Examples:
        24k -> 24000
        1.2kk -> 1200000
        3435k -> 3435000
        1.4 -> 1
    """
    def __new__(cls, argument):
        try:
            return super().__new__(int, argument)
        except ValueError:
            argument = argument.replace(",", "").strip().lower()
            m = TIBIA_CASH_PATTERN.match(argument)
            if not m or not m.group(1):
                raise commands.BadArgument(f"`{argument}` is not a valid number.")
            num = float(m.group(1))
            k_count = argument.count("k")
            num *= pow(1000, k_count)
            return int(num)