예제 #1
0
파일: skip.py 프로젝트: tctree333/Bird-ID
class Skip(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    # Skip command - no args
    @commands.command(help="- Skip the current bird to get a new one",
                      aliases=["sk"])
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def skip(self, ctx):
        logger.info("command: skip")

        currentBird = database.hget(f"channel:{ctx.channel.id}",
                                    "bird").decode("utf-8")
        database.hset(f"channel:{ctx.channel.id}", "bird", "")
        database.hset(f"channel:{ctx.channel.id}", "answered", "1")
        if currentBird != "":  # check if there is bird
            url = get_wiki_url(ctx, currentBird)
            await ctx.send(f"Ok, skipping {currentBird.lower()}")
            await ctx.send(url)  # sends wiki page

            streak_increment(ctx, None)  # reset streak

            if database.exists(f"race.data:{ctx.channel.id}"):
                if Filter.from_int(
                        int(
                            database.hget(f"race.data:{ctx.channel.id}",
                                          "filter"))).vc:
                    await voice_functions.stop(ctx, silent=True)

                media = database.hget(f"race.data:{ctx.channel.id}",
                                      "media").decode("utf-8")

                limit = int(
                    database.hget(f"race.data:{ctx.channel.id}", "limit"))
                first = database.zrevrange(f"race.scores:{ctx.channel.id}", 0,
                                           0, True)[0]
                if int(first[1]) >= limit:
                    logger.info("race ending")
                    race = self.bot.get_cog("Race")
                    await race.stop_race_(ctx)
                else:
                    logger.info(f"auto sending next bird {media}")
                    filter_int, taxon, state = database.hmget(
                        f"race.data:{ctx.channel.id}",
                        ["filter", "taxon", "state"])
                    birds = self.bot.get_cog("Birds")
                    await birds.send_bird_(
                        ctx,
                        media,
                        Filter.from_int(int(filter_int)),
                        taxon.decode("utf-8"),
                        state.decode("utf-8"),
                    )
        else:
            await ctx.send("You need to ask for a bird first!")
예제 #2
0
파일: hint.py 프로젝트: tctree333/Bird-ID
class Hint(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    # give hint
    @commands.command(help="- Gives first letter of current bird", aliases=["h"])
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    async def hint(self, ctx):
        logger.info("command: hint")

        currentBird = database.hget(f"channel:{ctx.channel.id}", "bird").decode("utf-8")
        if currentBird != "":  # check if there is bird
            await ctx.send(f"The first letter is {currentBird[0]}")
        else:
            await ctx.send("You need to ask for a bird first!")
예제 #3
0
class COVID(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.update_covid()

    @staticmethod
    def _request(endpoint, params=None):
        url = "https://coronavirus-tracker-api.herokuapp.com"
        response = requests.get(url + endpoint, params)
        response.raise_for_status()
        return response.json()

    def getLocations(self, rank_by: str = None, rank_amount: int = None):
        data = None
        world = [
            item for item in self._request("/v2/locations", {"source": "jhu"})
            ["locations"] if not (
                item["country_code"] == "US" and "," in set(item["province"]))
        ]
        usa = self._request("/v2/locations", {"source": "csbs"})["locations"]
        for item in usa:
            item["province"] = f"{item['county']} County, {item['province']}"
        data = world + usa
        ranking_criteria = ["confirmed", "deaths", "recovered"]
        if rank_by is not None:
            if rank_by not in ranking_criteria:
                raise ValueError(
                    "Invalid ranking criteria. Expected one of: %s" %
                    ranking_criteria)
            ranked = sorted(data,
                            key=lambda i: i["latest"][rank_by],
                            reverse=True)
            if rank_amount:
                data = ranked[:rank_amount]
            else:
                data = ranked
        return data

    def getLatest(self):
        data = self._request("/v2/latest")
        return data["latest"]

    def getCountryCode(self, country_code):
        if country_code == "US":
            data = self._request("/v2/locations", {
                "source": "csbs",
                "country_code": country_code
            })
        else:
            data = self._request("/v2/locations", {
                "source": "jhu",
                "country_code": country_code
            })
        if not data["locations"]:
            return None
        return data

    def getLocationById(self, country_id: int, us_county: bool = False):
        data = self._request(
            "/v2/locations/" + str(country_id),
            {"source": ("csbs" if us_county else "jhu")},
        )
        return data["location"]

    def update_covid(self):
        self.covid_location_ids = {
            f'{x["province"]}, {x["country_code"]}': x["id"]
            for x in self.getLocations()
        }

    @staticmethod
    def format_data(confirmed: int,
                    died: int,
                    recovered: int,
                    location="Global"):
        embed = discord.Embed(
            title="COVID-19 Data:",
            description="Latest data on the COVID-19 pandemic.",
            type="rich",
            colour=discord.Color.blurple(),
        )
        embed.set_author(name="Bird ID - An Ornithology Bot")
        data = (
            f"**Confirmed Cases:** `{confirmed}`\n" +
            f"**Deaths:** `{died}` {f'*({round((died/confirmed)*100, 1)}%)*' if confirmed != 0 else ''}\n"
            +
            f"**Recovered:** `{recovered}` {f'*({round((recovered/confirmed)*100, 1)}%)*' if confirmed != 0 else ''}\n"
        )
        embed.add_field(name=location, value=data, inline=False)
        return embed

    @staticmethod
    def format_leaderboard(data, ranked):
        embed = discord.Embed(
            title="COVID-19 Top:",
            description="Latest data on the COVID-19 pandemic.",
            type="rich",
            colour=discord.Color.blurple(),
        )
        embed.set_author(name="Bird ID - An Ornithology Bot")
        for item in data:
            c, d, r = item["latest"].values()
            location = f'{(item["province"] + ", " if item["province"] else "")}{item["country"]}'
            data = (f"**Confirmed Cases:** `{c}`\n" + f"**Deaths:** `{d}`\n" +
                    f"**Recovered:** `{r}`\n")
            embed.add_field(name=location, value=data, inline=False)
        return embed

    # give data
    @commands.group(
        brief="- Gives updated info on the COVID-19 pandemic.",
        help="- Gives updated info on the COVID-19 pandemic. " +
        "This fetches data from ExpDev07's Coronavirus tracker API, " +
        "which fetches data from Johns Hopkins, with county data from CSBS. " +
        "More info: (https://github.com/ExpDev07/coronavirus-tracker-api)",
        aliases=["corona", "coronavirus", "covid19"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.default))
    async def covid(self, ctx):
        if ctx.invoked_subcommand is None:
            logger.info("command: covid")
            location = await commands.clean_content(
                fix_channel_mentions=True,
                use_nicknames=True,
                escape_markdown=True).convert(
                    ctx, " ".join(ctx.message.content.split(" ")[1:]))

            if not location:
                c, d, r = self.getLatest().values()
                embed = self.format_data(c, d, r)
                await ctx.send(embed=embed)
                return

            if len(location) == 2:
                data = self.getCountryCode(location.upper())
                if data:
                    country = data["locations"][0]["country"]
                    await ctx.send(f"Fetching data for location `{country}`.")

                    c, d, r = data["latest"].values()
                    embed = self.format_data(c, d, r, country)
                    await ctx.send(embed=embed)
                    return

            location_matches = difflib.get_close_matches(
                location, self.covid_location_ids.keys(), n=1, cutoff=0.4)
            if location_matches:
                await ctx.send(
                    f"Fetching data for location `{location_matches[0]}`.")
                location_id = self.covid_location_ids[location_matches[0]]
                us_county = (location_matches[0].split(", ")[-1] == "US"
                             and location_matches[0].count(",") == 2)
                c, d, r = self.getLocationById(location_id,
                                               us_county)["latest"].values()
                embed = self.format_data(c, d, r, location_matches[0])
                await ctx.send(embed=embed)
                return

            await ctx.send(f"No location `{location}` found.")
            return

    # top countries
    @covid.command(
        brief="- Gets locations with the most cases",
        help="- Gets locations with the most cases. " +
        "This fetches data from ExpDev07's Coronavirus tracker API, " +
        "which fetches data from Johns Hopkins, with county data from CSBS. " +
        "More info: (https://github.com/ExpDev07/coronavirus-tracker-api)",
        aliases=["leaderboard"],
    )
    async def top(self, ctx, ranking: str = "confirmed", amt: int = 3):
        logger.info("command: covid top")

        if amt > 10:
            await ctx.send("**Invalid amount!** Defaulting to 10.")
            amt = 10
        if amt < 1:
            await ctx.send("**Invalid amount!** Defaulting to 1.")
            amt = 1

        if ranking in ("confirmed", "confirm", "cases", "c"):
            ranking = "confirmed"
        elif ranking in ("deaths", "death", "dead", "d"):
            ranking = "deaths"
        elif ranking in ("recovered", "recover", "better", "r"):
            ranking = "recovered"
        else:
            await ctx.send("Invalid argument!")
            return

        data = self.getLocations(ranking, amt)
        embed = self.format_leaderboard(data, ranking)

        await ctx.send(embed=embed)

    # update data
    @covid.command(
        brief="- Updates data.",
        help="- Updates data. " +
        "This fetches data from ExpDev07's Coronavirus tracker API, " +
        "which fetches data from Johns Hopkins, with county data from CSBS. " +
        "More info: (https://github.com/ExpDev07/coronavirus-tracker-api)",
    )
    @commands.check(CustomCooldown(3600.0, bucket=commands.BucketType.default))
    async def update(self, ctx):
        logger.info("command: update_covid")

        self.update_covid()

        await ctx.send("Ok, done!")
예제 #4
0
파일: check.py 프로젝트: tctree333/Bird-ID
class Check(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    # Check command - argument is the guess
    @commands.command(help="- Checks your answer.",
                      usage="guess",
                      aliases=["guess", "c"])
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.user))
    async def check(self, ctx, *, arg):
        logger.info("command: check")

        currentBird = database.hget(f"channel:{ctx.channel.id}",
                                    "bird").decode("utf-8")
        if currentBird == "":  # no bird
            await ctx.send("You must ask for a bird first!")
            return
        # if there is a bird, it checks answer
        sciBird = (await get_sciname(currentBird)).lower().replace("-", " ")
        arg = arg.lower().replace("-", " ")
        currentBird = currentBird.lower().replace("-", " ")
        alpha_code = alpha_codes.get(string.capwords(currentBird))
        logger.info("currentBird: " + currentBird)
        logger.info("arg: " + arg)

        bird_setup(ctx, currentBird)

        race_in_session = bool(database.exists(f"race.data:{ctx.channel.id}"))
        if race_in_session:
            logger.info("race in session")
            if database.hget(f"race.data:{ctx.channel.id}", "strict"):
                logger.info("strict spelling")
                correct = arg in (currentBird, sciBird)
            else:
                logger.info("spelling leniency")
                correct = spellcheck(arg, currentBird) or spellcheck(
                    arg, sciBird)

            if not correct and database.hget(f"race.data:{ctx.channel.id}",
                                             "alpha"):
                logger.info("checking alpha codes")
                correct = arg.upper() == alpha_code
        else:
            logger.info("no race")
            if database.hget(f"session.data:{ctx.author.id}", "strict"):
                logger.info("strict spelling")
                correct = arg in (currentBird, sciBird)
            else:
                logger.info("spelling leniency")
                correct = (spellcheck(arg, currentBird)
                           or spellcheck(arg, sciBird)
                           or arg.upper() == alpha_code)

        if correct:
            logger.info("correct")

            database.hset(f"channel:{ctx.channel.id}", "bird", "")
            database.hset(f"channel:{ctx.channel.id}", "answered", "1")

            session_increment(ctx, "correct", 1)
            streak_increment(ctx, 1)
            database.zincrby(f"correct.user:{ctx.author.id}", 1,
                             string.capwords(str(currentBird)))

            if (race_in_session and Filter.from_int(
                    int(database.hget(f"race.data:{ctx.channel.id}",
                                      "filter"))).vc):
                await voice_functions.stop(ctx, silent=True)

            await ctx.send(
                f"Correct! Good job! The bird was **{currentBird}**."
                if not race_in_session else
                f"**{ctx.author.mention}**, you are correct! The bird was **{currentBird}**."
            )
            url = get_wiki_url(ctx, currentBird)
            await ctx.send(url)
            score_increment(ctx, 1)
            if int(database.zscore("users:global",
                                   str(ctx.author.id))) in achievement:
                number = str(
                    int(database.zscore("users:global", str(ctx.author.id))))
                await ctx.send(
                    f"Wow! You have answered {number} birds correctly!")
                filename = f"bot/media/achievements/{number}.PNG"
                with open(filename, "rb") as img:
                    await ctx.send(file=discord.File(img, filename="award.png")
                                   )

            if race_in_session:
                media = database.hget(f"race.data:{ctx.channel.id}",
                                      "media").decode("utf-8")

                limit = int(
                    database.hget(f"race.data:{ctx.channel.id}", "limit"))
                first = database.zrevrange(f"race.scores:{ctx.channel.id}", 0,
                                           0, True)[0]
                if int(first[1]) >= limit:
                    logger.info("race ending")
                    race = self.bot.get_cog("Race")
                    await race.stop_race_(ctx)
                else:
                    logger.info(f"auto sending next bird {media}")
                    filter_int, taxon, state = database.hmget(
                        f"race.data:{ctx.channel.id}",
                        ["filter", "taxon", "state"])
                    birds = self.bot.get_cog("Birds")
                    await birds.send_bird_(
                        ctx,
                        media,
                        Filter.from_int(int(filter_int)),
                        taxon.decode("utf-8"),
                        state.decode("utf-8"),
                    )

        else:
            logger.info("incorrect")

            streak_increment(ctx, None)  # reset streak
            session_increment(ctx, "incorrect", 1)
            incorrect_increment(ctx, str(currentBird), 1)

            if race_in_session:
                await ctx.send("Sorry, that wasn't the right answer.")
            else:
                database.hset(f"channel:{ctx.channel.id}", "bird", "")
                database.hset(f"channel:{ctx.channel.id}", "answered", "1")
                await ctx.send("Sorry, the bird was actually **" +
                               currentBird + "**.")
                url = get_wiki_url(ctx, currentBird)
                await ctx.send(url)
예제 #5
0
class Race(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    async def _get_options(self, ctx):
        filter_int, state, media, limit, taxon, strict, alpha = database.hmget(
            f"race.data:{ctx.channel.id}",
            ["filter", "state", "media", "limit", "taxon", "strict", "alpha"],
        )
        filters = Filter.from_int(int(filter_int))
        options = (
            f"**Active Filters:** `{'`, `'.join(filters.display())}`\n"
            + f"**Special bird list:** {state.decode('utf-8') if state else 'None'}\n"
            + f"**Taxons:** {taxon.decode('utf-8') if taxon else 'None'}\n"
            + f"**Media Type:** {media.decode('utf-8')}\n"
            + f"**Amount to Win:** {limit.decode('utf-8')}\n"
            + f"**Strict Spelling:** {strict == b'strict'}\n"
            + f"**Alpha Codes:** {'Enabled' if alpha == b'alpha' else 'Disabled'}"
        )
        return options

    async def _send_stats(self, ctx, preamble):
        placings = 5
        database_key = f"race.scores:{ctx.channel.id}"
        if database.zcard(database_key) == 0:
            logger.info(f"no users in {database_key}")
            await ctx.send("There are no users in the database.")
            return

        if placings > database.zcard(database_key):
            placings = database.zcard(database_key)

        leaderboard_list = database.zrevrangebyscore(
            database_key, "+inf", "-inf", 0, placings, True
        )
        embed = discord.Embed(
            type="rich", colour=discord.Color.blurple(), title=preamble
        )
        embed.set_author(name="Bird ID - An Ornithology Bot")
        leaderboard = ""

        for i, stats in enumerate(leaderboard_list):
            if ctx.guild is not None:
                user = await fetch_get_user(int(stats[0]), ctx=ctx, member=True)
            else:
                user = None

            if user is None:
                user = await fetch_get_user(int(stats[0]), ctx=ctx, member=False)
                if user is None:
                    user_info = "**Deleted**"
                else:
                    user_info = f"**{esc(user.name)}#{user.discriminator}**"
            else:
                user_info = f"**{esc(user.name)}#{user.discriminator}** ({user.mention})"

            leaderboard += f"{i+1}. {user_info} - {int(stats[1])}\n"

        start = int(database.hget(f"race.data:{ctx.channel.id}", "start"))
        elapsed = str(datetime.timedelta(seconds=round(time.time()) - start))

        embed.add_field(
            name="Options", value=await self._get_options(ctx), inline=False
        )
        embed.add_field(
            name="Stats", value=f"**Race Duration:** `{elapsed}`", inline=False
        )
        embed.add_field(name="Leaderboard", value=leaderboard, inline=False)

        if ctx.author:
            if database.zscore(database_key, str(ctx.author.id)) is not None:
                placement = int(database.zrevrank(database_key, str(ctx.author.id))) + 1
                embed.add_field(
                    name="You:", value=f"You are #{placement}.", inline=False
                )
            else:
                embed.add_field(
                    name="You:", value="You haven't answered any correctly."
                )

        await ctx.send(embed=embed)

    async def stop_race_(self, ctx):
        if Filter.from_int(
            int(database.hget(f"race.data:{ctx.channel.id}", "filter"))
        ).vc:
            await voice_functions.disconnect(ctx, silent=True)
            database.delete(f"voice.server:{ctx.guild.id}")

        first = database.zrevrange(f"race.scores:{ctx.channel.id}", 0, 0, True)[0]
        if ctx.guild is not None:
            user = await fetch_get_user(int(first[0]), ctx=ctx, member=True)
        else:
            user = None

        if user is None:
            user = await fetch_get_user(int(first[0]), ctx=ctx, member=False)
            if user is None:
                user_info = "Deleted"
            else:
                user_info = f"{esc(user.name)}#{user.discriminator}"
        else:
            user_info = f"{esc(user.name)}#{user.discriminator} ({user.mention})"

        await ctx.send(
            f"**Congratulations, {user_info}!**\n"
            + f"You have won the race by correctly identifying `{int(first[1])}` birds. "
            + "*Way to go!*"
        )

        database.hset(f"race.data:{ctx.channel.id}", "stop", round(time.time()))

        await self._send_stats(ctx, "**Race stopped.**")
        database.delete(f"race.data:{ctx.channel.id}")
        database.delete(f"race.scores:{ctx.channel.id}")

        logger.info("race end: skipping last bird")
        database.hset(f"channel:{ctx.channel.id}", "bird", "")
        database.hset(f"channel:{ctx.channel.id}", "answered", "1")

    @commands.group(
        brief="- Base race command",
        help="- Base race command\n"
        + "Races allow you to compete with others to see who can ID a bird first. "
        + "Starting a race will keep all cooldowns the same, but automatically run "
        + "'b!bird' (or 'b!song') after every check. You will still need to use 'b!check' "
        + "to check your answer. Races are channel-specific, and anyone in that channel can play."
        + "Races end when a player is the first to correctly ID a set amount of birds. (default 10)",
    )
    @commands.guild_only()
    async def race(self, ctx):
        if ctx.invoked_subcommand is None:
            await ctx.send(
                "**Invalid subcommand passed.**\n*Valid Subcommands:* `start, view, stop`"
            )

    @race.command(
        brief="- Starts race",
        help="""- Starts race.
        Arguments passed will become the default arguments to 'b!bird', but some can be manually overwritten during use.
        Arguments can be passed in any taxon.
        However, having both females and juveniles are not supported.""",
        aliases=["st"],
        usage="[state] [filters] [taxon] [amount to win (default 10)]",
    )
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    async def start(self, ctx, *, args_str: str = ""):
        logger.info("command: start race")

        if not str(ctx.channel.name).startswith("racing"):
            logger.info("not race channel")
            await ctx.send(
                "**Sorry, racing is not available in this channel.**\n"
                + "*Set the channel name to start with `racing` to enable it.*"
            )
            return

        if database.exists(f"race.data:{ctx.channel.id}"):
            logger.info("already race")
            await ctx.send(
                "**There is already a race in session.** *Change settings/view stats with `b!race view`*"
            )
            return

        filters = Filter.parse(args_str, use_numbers=False)
        if filters.vc:
            if database.get(f"voice.server:{ctx.guild.id}") is not None:
                logger.info("already vc race")
                await ctx.send(
                    "**There is already a VC race in session in this server!**"
                )
                return
            client = await voice_functions.get_voice_client(ctx, connect=True)
            if client is None:
                return
            database.set(f"voice.server:{ctx.guild.id}", str(ctx.channel.id))

        args = args_str.split(" ")
        logger.info(f"args: {args}")

        taxon_args = set(taxons.keys()).intersection({arg.lower() for arg in args})
        if taxon_args:
            taxon = " ".join(taxon_args).strip()
        else:
            taxon = ""

        if "strict" in args:
            strict = "strict"
        else:
            strict = ""

        if "alpha" in args:
            alpha = "alpha"
        else:
            alpha = ""

        states_args = set(states.keys()).intersection({arg.upper() for arg in args})
        if states_args:
            if {"CUSTOM"}.issubset(states_args):
                if database.exists(
                    f"custom.list:{ctx.author.id}"
                ) and not database.exists(f"custom.confirm:{ctx.author.id}"):
                    states_args.discard("CUSTOM")
                    states_args.add(f"CUSTOM:{ctx.author.id}")
                else:
                    states_args.discard("CUSTOM")
                    await ctx.send(
                        "**You don't have a custom list set.**\n*Ignoring the argument.*"
                    )
            state = " ".join(states_args).strip()
        else:
            state = ""

        song = "song" in args or "songs" in args or "s" in args or filters.vc
        image = (
            "image" in args
            or "images" in args
            or "i" in args
            or "picture" in args
            or "pictures" in args
            or "p" in args
        )
        if song and image:
            await ctx.send(
                "**Songs and images are not yet supported.**\n*Please try again*"
            )
            return
        if song:
            media = "song"
        elif image:
            media = "image"
        else:
            media = "image"

        ints = []
        for n in args:
            try:
                ints.append(int(n))
            except ValueError:
                continue
        if ints:
            limit = int(ints[0])
        else:
            limit = 10

        if limit > 1000000:
            await ctx.send("**Sorry, the maximum amount to win is 1 million.**")
            limit = 1000000

        logger.info(
            f"adding filters: {filters}; state: {state}; media: {media}; limit: {limit}"
        )

        database.hset(
            f"race.data:{ctx.channel.id}",
            mapping={
                "start": round(time.time()),
                "stop": 0,
                "limit": limit,
                "filter": str(filters.to_int()),
                "state": state,
                "media": media,
                "taxon": taxon,
                "strict": strict,
                "alpha": alpha,
            },
        )

        database.zadd(f"race.scores:{ctx.channel.id}", {str(ctx.author.id): 0})
        await ctx.send(
            f"**Race started with options:**\n{await self._get_options(ctx)}"
        )

        media = database.hget(f"race.data:{ctx.channel.id}", "media").decode("utf-8")
        logger.info("clearing previous bird")
        database.hset(f"channel:{ctx.channel.id}", "bird", "")
        database.hset(f"channel:{ctx.channel.id}", "answered", "1")

        logger.info(f"auto sending next bird {media}")
        filter_int, taxon, state = database.hmget(
            f"race.data:{ctx.channel.id}", ["filter", "taxon", "state"]
        )
        birds = self.bot.get_cog("Birds")
        await birds.send_bird_(
            ctx,
            media,
            Filter.from_int(int(filter_int)),  # type: ignore
            taxon.decode("utf-8"),  # type: ignore
            state.decode("utf-8"),  # type: ignore
        )

    @race.command(
        brief="- Views race",
        help="- Views race.\nRaces allow you to compete with your friends to ID a certain bird first.",
    )
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    async def view(self, ctx):
        logger.info("command: view race")

        if database.exists(f"race.data:{ctx.channel.id}"):
            await self._send_stats(ctx, "**Race In Progress**")
        else:
            await ctx.send(
                "**There is no race in session.** *You can start one with `b!race start`*"
            )

    @race.command(help="- Stops race", aliases=["stp", "end"])
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    async def stop(self, ctx):
        logger.info("command: stop race")

        if database.exists(f"race.data:{ctx.channel.id}"):
            await self.stop_race_(ctx)
        else:
            await ctx.send(
                "**There is no race in session.** *You can start one with `b!race start`*"
            )
예제 #6
0
class Score(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @staticmethod
    def _server_total(ctx):
        logger.info("fetching server totals")
        channels = map(
            lambda x: x.decode("utf-8").split(":")[1],
            database.zrangebylex(
                "channels:global", f"[{ctx.guild.id}", f"({ctx.guild.id}\xff"
            ),
        )
        pipe = database.pipeline()  # use a pipeline to get all the scores
        for channel in channels:
            pipe.zscore("score:global", channel)
        scores = pipe.execute()
        return int(sum(scores))

    @staticmethod
    def _monthly_lb(category):
        logger.info("generating monthly leaderboard")
        if category == "scores":
            key = "daily.score"
        elif category == "missed":
            key = "daily.incorrect"
        else:
            raise GenericError("Invalid category", 990)

        today = datetime.datetime.now(datetime.timezone.utc).date()
        past_month = pd.date_range(  # pylint: disable=no-member
            today - datetime.timedelta(29), today
        ).date
        pipe = database.pipeline()
        for day in past_month:
            pipe.zrevrangebyscore(f"{key}:{day}", "+inf", "-inf", withscores=True)
        result = pipe.execute()
        totals = pd.Series(dtype="int64")
        for daily_score in result:
            daily_score = pd.Series(
                {
                    e[0]: e[1]
                    for e in map(
                        lambda x: (x[0].decode("utf-8"), int(x[1])), daily_score
                    )
                }
            )
            totals = totals.add(daily_score, fill_value=0)
        totals = totals.sort_values(ascending=False)
        return totals

    async def user_lb(self, ctx, title, page, database_key=None, data=None):
        if database_key is None and data is None:
            raise GenericError("database_key and data are both NoneType", 990)
        if database_key is not None and data is not None:
            raise GenericError("database_key and data are both set", 990)

        if page < 1:
            page = 1

        user_amount = (
            int(database.zcard(database_key))
            if database_key is not None
            else data.count()
        )
        page = (page * 10) - 10

        if user_amount == 0:
            logger.info(f"no users in {database_key}")
            await ctx.send("There are no users in the database.")
            return

        if page >= user_amount:
            page = user_amount - (user_amount % 10 if user_amount % 10 != 0 else 10)

        users_per_page = 10
        leaderboard_list = (
            database.zrevrangebyscore(
                database_key, "+inf", "-inf", page, users_per_page, True
            )
            if database_key is not None
            else data.iloc[page : page + users_per_page - 1].items()
        )

        embed = discord.Embed(type="rich", colour=discord.Color.blurple())
        embed.set_author(name="Bird ID - An Ornithology Bot")
        leaderboard = ""

        for i, stats in enumerate(leaderboard_list):
            if ctx.guild is not None:
                user = ctx.guild.get_member(int(stats[0]))
            else:
                user = None

            if user is None:
                user = self.bot.get_user(int(stats[0]))
                if user is None:
                    user = "******"
                else:
                    user = f"**{user.name}#{user.discriminator}**"
            else:
                user = f"**{user.name}#{user.discriminator}** ({user.mention})"

            leaderboard += f"{i+1+page}. {user} - {int(stats[1])}\n"

        embed.add_field(name=title, value=leaderboard, inline=False)

        user_score = (
            database.zscore(database_key, str(ctx.author.id))
            if database_key is not None
            else data.get(str(ctx.author.id))
        )

        if user_score is not None:
            if database_key is not None:
                placement = int(database.zrevrank(database_key, str(ctx.author.id))) + 1
                distance = int(
                    database.zrevrange(
                        database_key, placement - 2, placement - 2, True
                    )[0][1]
                ) - int(user_score)
            else:
                placement = int(data.rank(ascending=False)[str(ctx.author.id)])
                distance = int(data.iloc[placement - 2] - user_score)

            if placement == 1:
                embed.add_field(
                    name="You:",
                    value=f"You are #{placement} on the leaderboard.\nYou are in first place.",
                    inline=False,
                )
            elif distance == 0:
                embed.add_field(
                    name="You:",
                    value=f"You are #{placement} on the leaderboard.\nYou are tied with #{placement-1}",
                    inline=False,
                )
            else:
                embed.add_field(
                    name="You:",
                    value=f"You are #{placement} on the leaderboard.\nYou are {distance} away from #{placement-1}",
                    inline=False,
                )
        else:
            embed.add_field(name="You:", value="You haven't answered any correctly.")

        await ctx.send(embed=embed)

    # returns total number of correct answers so far
    @commands.command(
        brief="- Total correct answers in a channel or server",
        help="- Total correct answers in a channel or server. Defaults to channel.",
        usage="[server|s]",
    )
    @commands.check(CustomCooldown(8.0, bucket=commands.BucketType.channel))
    async def score(self, ctx, scope=""):
        logger.info("command: score")

        if scope in ("server", "s"):
            total_correct = self._server_total(ctx)
            await ctx.send(
                f"Wow, looks like a total of `{total_correct}` birds have been answered correctly in this **server**!\n"
                + "Good job everyone!"
            )
        else:
            total_correct = int(database.zscore("score:global", str(ctx.channel.id)))
            await ctx.send(
                f"Wow, looks like a total of `{total_correct}` birds have been answered correctly in this **channel**!\n"
                + "Good job everyone!"
            )

    # sends correct answers by a user
    @commands.command(
        brief="- How many correct answers given by a user",
        help="- Gives the amount of correct answers by a user.\n"
        + "Mention someone to get their score, don't mention anyone to get your score.",
        aliases=["us"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def userscore(
        self, ctx, *, user: typing.Optional[typing.Union[discord.Member, str]] = None
    ):
        logger.info("command: userscore")

        if user is not None:
            if isinstance(user, str):
                await ctx.send("Not a user!")
                return
            usera = user.id
            logger.info(usera)
            score = database.zscore("users:global", str(usera))
            if score is not None:
                score = int(score)
                user = f"<@{usera}>"
            else:
                await ctx.send("This user does not exist on our records!")
                return
        else:
            user = f"<@{ctx.author.id}>"
            score = int(database.zscore("users:global", str(ctx.author.id)))

        embed = discord.Embed(type="rich", colour=discord.Color.blurple())
        embed.set_author(name="Bird ID - An Ornithology Bot")
        embed.add_field(
            name="User Score:", value=f"{user} has answered correctly {score} times."
        )
        await ctx.send(embed=embed)

    # gives streak of a user
    @commands.group(
        help="- Gives your current/max streak",
        usage="[user]",
        aliases=["streaks", "stk"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def streak(self, ctx):
        if ctx.invoked_subcommand is not None:
            return
        logger.info("command: streak")
        args = " ".join(ctx.message.content.split(" ")[1:])
        if args:
            user = await commands.MemberConverter().convert(ctx, args)
        else:
            user = None

        if user is not None:
            if isinstance(user, str):
                await ctx.send("Not a user!")
                return
            usera = user.id
            logger.info(usera)
            streak = database.zscore("streak:global", str(usera))
            max_streak = database.zscore("streak.max:global", str(usera))
            if streak is not None and max_streak is not None:
                streak = int(streak)
                max_streak = int(max_streak)
                user = f"<@{usera}>"
            else:
                await ctx.send("This user does not exist on our records!")
                return
        else:
            user = f"<@{ctx.author.id}>"
            streak = int(database.zscore("streak:global", str(ctx.author.id)))
            max_streak = int(database.zscore("streak.max:global", str(ctx.author.id)))

        embed = discord.Embed(
            type="rich", colour=discord.Color.blurple(), title="**User Streaks**"
        )
        embed.set_author(name="Bird ID - An Ornithology Bot")
        current_streak = f"{user} has answered `{streak}` in a row!"
        max_streak = f"{user}'s max was `{max_streak}` in a row!"
        embed.add_field(name="**Current Streak**", value=current_streak, inline=False)
        embed.add_field(name="**Max Streak**", value=max_streak, inline=False)

        await ctx.send(embed=embed)

    # streak leaderboard - returns top streaks
    @streak.command(
        brief="- Top streaks",
        help="- Top streaks, either current (default) or max.",
        usage="[max|m] [page]",
        name="leaderboard",
        aliases=["lb"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def streak_leaderboard(self, ctx, scope="", page=1):
        logger.info("command: leaderboard")

        try:
            page = int(scope)
        except ValueError:
            if scope == "":
                scope = "current"
            scope = scope.lower()
        else:
            scope = "current"

        logger.info(f"scope: {scope}")
        logger.info(f"page: {page}")

        if not scope in ("current", "now", "c", "max", "m"):
            logger.info("invalid scope")
            await ctx.send(f"**{scope} is not a valid scope!**\n*Valid Scopes:* `max`")
            return

        if scope in ("max", "m"):
            database_key = "streak.max:global"
            scope = "max"
        else:
            database_key = "streak:global"
            scope = "current"

        await self.user_lb(
            ctx, f"Streak Leaderboard ({scope})", page, database_key, None
        )

    # leaderboard - returns top 1-10 users
    @commands.command(
        brief="- Top scores",
        help="- Top scores, either global, server, or monthly.",
        usage="[global|g  server|s  month|monthly|m] [page]",
        aliases=["lb"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def leaderboard(self, ctx, scope="", page=1):
        logger.info("command: leaderboard")

        try:
            page = int(scope)
        except ValueError:
            if scope == "":
                scope = "global"
            scope = scope.lower()
        else:
            scope = "global"

        logger.info(f"scope: {scope}")
        logger.info(f"page: {page}")

        if not scope in ("global", "server", "month", "monthly", "m", "g", "s"):
            logger.info("invalid scope")
            await ctx.send(
                f"**{scope} is not a valid scope!**\n*Valid Scopes:* `global, server, month`"
            )
            return

        if scope in ("server", "s"):
            if ctx.guild is not None:
                database_key = f"users.server:{ctx.guild.id}"
                scope = "server"
            else:
                logger.info("dm context")
                await ctx.send(
                    "**Server scopes are not available in DMs.**\n*Showing global leaderboard instead.*"
                )
                scope = "global"
                database_key = "users:global"
            data = None
        elif scope in ("month", "monthly", "m"):
            database_key = None
            scope = "Last 30 Days"
            data = self._monthly_lb("scores")
        else:
            database_key = "users:global"
            scope = "global"
            data = None

        await self.user_lb(ctx, f"Leaderboard ({scope})", page, database_key, data)

    # missed - returns top 1-10 missed birds
    @commands.command(
        brief="- Top incorrect birds",
        help="- Top incorrect birds, either global, server, personal, or monthly.",
        usage="[global|g  server|s  me|m  month|monthly|mo] [page]",
        aliases=["m"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def missed(self, ctx, scope="", page=1):
        logger.info("command: missed")

        try:
            page = int(scope)
        except ValueError:
            if scope == "":
                scope = "global"
                scope = scope.lower()
        else:
            scope = "global"

        logger.info(f"scope: {scope}")
        logger.info(f"page: {page}")

        if not scope in (
            "global",
            "server",
            "me",
            "month",
            "monthly",
            "mo",
            "g",
            "s",
            "m",
        ):
            logger.info("invalid scope")
            await ctx.send(
                f"**{scope} is not a valid scope!**\n*Valid Scopes:* `global, server, me, month`"
            )
            return

        if scope in ("server", "s"):
            data = None
            if ctx.guild is not None:
                database_key = f"incorrect.server:{ctx.guild.id}"
                scope = "server"
            else:
                logger.info("dm context")
                await ctx.send(
                    "**Server scopes are not available in DMs.**\n*Showing global leaderboard instead.*"
                )
                scope = "global"
                database_key = "incorrect:global"
        elif scope in ("me", "m"):
            data = None
            database_key = f"incorrect.user:{ctx.author.id}"
            scope = "me"
        elif scope in ("month", "monthly", "mo"):
            data = self._monthly_lb("missed")
            database_key = None
            scope = "Last 30 days"
        else:
            data = None
            database_key = "incorrect:global"
            scope = "global"

        await send_leaderboard(
            ctx, f"Top Missed Birds ({scope})", page, database_key, data
        )
예제 #7
0
class Birds(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    async def _send_next_race_media(self, ctx):
        if database.exists(f"race.data:{ctx.channel.id}"):
            if Filter.from_int(
                    int(database.hget(f"race.data:{ctx.channel.id}",
                                      "filter"))).vc:
                await voice_functions.stop(ctx, silent=True)

            media = database.hget(f"race.data:{ctx.channel.id}",
                                  "media").decode("utf-8")

            logger.info(f"auto sending next bird {media}")
            filter_int, taxon, state = database.hmget(
                f"race.data:{ctx.channel.id}", ["filter", "taxon", "state"])

            await self.send_bird_(
                ctx,
                media,
                Filter.from_int(int(filter_int)),
                taxon.decode("utf-8"),
                state.decode("utf-8"),
            )

    def error_handle(
        self,
        ctx,
        media_type: str,
        filters: Filter,
        taxon_str,
        role_str,
        retries,
    ):
        """Return a function to pass to send_bird() as on_error."""

        # pylint: disable=unused-argument
        async def inner(error):
            nonlocal retries

            # skip current bird
            database.hset(f"channel:{ctx.channel.id}", "bird", "")
            database.hset(f"channel:{ctx.channel.id}", "answered", "1")

            if retries >= 2:  # only retry twice
                await ctx.send("**Too many retries.**\n*Please try again.*")
                await self._send_next_race_media(ctx)
                return

            if isinstance(error, GenericError) and error.code == 100:
                retries += 1
                await ctx.send("**Retrying...**")
                await self.send_bird_(ctx, media_type, filters, taxon_str,
                                      role_str, retries)
            else:
                await ctx.send("*Please try again.*")
                await self._send_next_race_media(ctx)

        return inner

    @staticmethod
    def error_skip(ctx):
        async def inner(error):
            # pylint: disable=unused-argument

            # skip current bird
            database.hset(f"channel:{ctx.channel.id}", "bird", "")
            database.hset(f"channel:{ctx.channel.id}", "answered", "1")
            await ctx.send("*Please try again.*")

        return inner

    @staticmethod
    def increment_bird_frequency(ctx, bird):
        bird_setup(ctx, bird)
        database.zincrby("frequency.bird:global", 1, string.capwords(bird))

    async def send_bird_(
        self,
        ctx,
        media_type: Optional[str],
        filters: Filter,
        taxon_str: str = "",
        role_str: str = "",
        retries=0,
    ):
        media_type = ("images" if media_type in ("images", "image", "i",
                                                 "p") else
                      ("songs" if media_type in ("songs", "song", "s",
                                                 "a") else None))
        if not media_type:
            raise GenericError("Invalid media type", code=990)

        if media_type == "songs" and filters.vc:
            current_voice = database.get(f"voice.server:{ctx.guild.id}")
            if current_voice is not None and current_voice.decode(
                    "utf-8") != str(ctx.channel.id):
                logger.info("already vc race")
                await ctx.send("**The voice channel is currently in use!**")
                return

        if taxon_str:
            taxon = taxon_str.split(" ")
        else:
            taxon = []

        if role_str:
            roles = role_str.split(" ")
        else:
            roles = []

        logger.info(
            "bird: " +
            database.hget(f"channel:{ctx.channel.id}", "bird").decode("utf-8"))

        currently_in_race = bool(
            database.exists(f"race.data:{ctx.channel.id}"))

        answered = int(database.hget(f"channel:{ctx.channel.id}", "answered"))
        logger.info(f"answered: {answered}")
        # check to see if previous bird was answered
        if answered:  # if yes, give a new bird
            session_increment(ctx, "total", 1)

            logger.info(f"filters: {filters}; taxon: {taxon}; roles: {roles}")

            if not currently_in_race and retries == 0:
                await ctx.send(
                    "**Recognized arguments:** " +
                    f"*Active Filters*: `{'`, `'.join(filters.display())}`, " +
                    f"*Taxons*: `{'None' if taxon_str == '' else taxon_str}`, "
                    +
                    f"*Detected State*: `{'None' if role_str == '' else role_str}`"
                )

            find_custom_role = {
                i if i.startswith("CUSTOM:") else ""
                for i in roles
            }
            find_custom_role.discard("")
            if (database.exists(f"race.data:{ctx.channel.id}")
                    and len(find_custom_role) == 1):
                custom_role = find_custom_role.pop()
                roles.remove(custom_role)
                roles.append("CUSTOM")
                user_id = custom_role.split(":")[1]
                birds = build_id_list(user_id=user_id,
                                      taxon=taxon,
                                      state=roles,
                                      media=media_type)
            else:
                birds = build_id_list(user_id=ctx.author.id,
                                      taxon=taxon,
                                      state=roles,
                                      media=media_type)

            if not birds:
                logger.info("no birds for taxon/state")
                await ctx.send(
                    "**Sorry, no birds could be found for the taxon/state combo.**\n*Please try again*"
                )
                return

            currentBird = random.choice(birds)
            self.increment_bird_frequency(ctx, currentBird)

            prevB = database.hget(f"channel:{ctx.channel.id}",
                                  "prevB").decode("utf-8")
            while currentBird == prevB and len(birds) > 1:
                currentBird = random.choice(birds)
            database.hset(f"channel:{ctx.channel.id}", "prevB",
                          str(currentBird))
            database.hset(f"channel:{ctx.channel.id}", "bird",
                          str(currentBird))
            logger.info("currentBird: " + str(currentBird))
            database.hset(f"channel:{ctx.channel.id}", "answered", "0")
            await send_bird(
                ctx,
                currentBird,
                media_type,
                filters,
                on_error=self.error_handle(ctx, media_type, filters, taxon_str,
                                           role_str, retries),
                message=(
                    SONG_MESSAGE if media_type == "songs" else BIRD_MESSAGE)
                if not currently_in_race else "*Here you go!*",
            )
        else:  # if no, give the same bird
            await ctx.send(
                f"**Active Filters**: `{'`, `'.join(filters.display())}`")
            await send_bird(
                ctx,
                database.hget(f"channel:{ctx.channel.id}",
                              "bird").decode("utf-8"),
                media_type,
                filters,
                on_error=self.error_handle(ctx, media_type, filters, taxon_str,
                                           role_str, retries),
                message=(
                    SONG_MESSAGE if media_type == "songs" else BIRD_MESSAGE)
                if not currently_in_race else "*Here you go!*",
            )

    @staticmethod
    async def parse(ctx, args_str: str):
        """Parse arguments for options."""

        args = args_str.split(" ")
        logger.info(f"args: {args}")

        if not database.exists(f"race.data:{ctx.channel.id}"):
            roles = check_state_role(ctx)

            taxon_args = set(taxons.keys()).intersection(
                {arg.lower()
                 for arg in args})
            if taxon_args:
                taxon = " ".join(taxon_args).strip()
            else:
                taxon = ""

            state_args = set(states.keys()).intersection(
                {arg.upper()
                 for arg in args})
            if state_args:
                state = " ".join(state_args).strip()
            else:
                state = ""

            if database.exists(f"session.data:{ctx.author.id}"):
                logger.info("session parameters")

                if taxon_args:
                    current_taxons = set(
                        database.hget(f"session.data:{ctx.author.id}",
                                      "taxon").decode("utf-8").split(" "))
                    logger.info(f"toggle taxons: {taxon_args}")
                    logger.info(f"current taxons: {current_taxons}")
                    taxon_args.symmetric_difference_update(current_taxons)
                    taxon_args.discard("")
                    logger.info(f"new taxons: {taxon_args}")
                    taxon = " ".join(taxon_args).strip()
                else:
                    taxon = database.hget(f"session.data:{ctx.author.id}",
                                          "taxon").decode("utf-8")

                roles = (database.hget(f"session.data:{ctx.author.id}",
                                       "state").decode("utf-8").split(" "))
                if roles[0] == "":
                    roles = []
                if not roles:
                    logger.info("no session lists")
                    roles = check_state_role(ctx)

                session_filter = int(
                    database.hget(f"session.data:{ctx.author.id}", "filter"))
                filters = Filter.parse(args_str, defaults=False)
                if filters.vc:
                    filters.vc = False
                    await ctx.send("**The VC filter is not allowed inline!**")

                default_quality = Filter().quality
                if (Filter.from_int(session_filter).quality == default_quality
                        and filters.quality
                        and filters.quality != default_quality):
                    filters ^= Filter()  # clear defaults
                filters ^= session_filter
            else:
                filters = Filter.parse(args_str)
                if filters.vc:
                    filters.vc = False
                    await ctx.send("**The VC filter is not allowed inline!**")

            if state_args:
                logger.info(f"toggle states: {state_args}")
                logger.info(f"current states: {roles}")
                state_args.symmetric_difference_update(set(roles))
                state_args.discard("")
                logger.info(f"new states: {state_args}")
                state = " ".join(state_args).strip()
            else:
                state = " ".join(roles).strip()

            if "CUSTOM" in state.upper().split(" "):
                if not database.exists(f"custom.list:{ctx.author.id}"):
                    await ctx.send("**You don't have a custom list set!**")
                    state_list = state.split(" ")
                    state_list.remove("CUSTOM")
                    state = " ".join(state_list)
                elif database.exists(f"custom.confirm:{ctx.author.id}"):
                    await ctx.send(
                        "**Please verify or confirm your custom list before using!**"
                    )
                    state_list = state.split(" ")
                    state_list.remove("CUSTOM")
                    state = " ".join(state_list)

        else:
            logger.info("race parameters")

            race_filter = int(
                database.hget(f"race.data:{ctx.channel.id}", "filter"))
            filters = Filter.parse(args_str, defaults=False)
            if filters.vc:
                filters.vc = False
                await ctx.send("**The VC filter is not allowed inline!**")

            default_quality = Filter().quality
            if (Filter.from_int(race_filter).quality == default_quality
                    and filters.quality
                    and filters.quality != default_quality):
                filters ^= Filter()  # clear defaults
            filters ^= race_filter

            taxon = database.hget(f"race.data:{ctx.channel.id}",
                                  "taxon").decode("utf-8")
            state = database.hget(f"race.data:{ctx.channel.id}",
                                  "state").decode("utf-8")

        logger.info(
            f"args: filters: {filters}; taxon: {taxon}; state: {state}")

        return (filters, taxon, state)

    # Bird command - no args
    # help text
    @commands.command(
        help="- Sends a random bird image for you to ID",
        aliases=["b"],
        usage="[filters] [order/family] [state]",
    )
    # 5 second cooldown
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def bird(self, ctx, *, args_str: str = ""):
        logger.info("command: bird")

        filters, taxon, state = await self.parse(ctx, args_str)
        media = "images"
        if database.exists(f"race.data:{ctx.channel.id}"):
            media = database.hget(f"race.data:{ctx.channel.id}",
                                  "media").decode("utf-8")
        await self.send_bird_(ctx, media, filters, taxon, state)

    # picks a random bird call to send
    @commands.command(
        help="- Sends a random bird song for you to ID",
        aliases=["s"],
        usage="[filters] [order/family] [state]",
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def song(self, ctx, *, args_str: str = ""):
        logger.info("command: song")

        filters, taxon, state = await self.parse(ctx, args_str)
        media = "songs"
        if database.exists(f"race.data:{ctx.channel.id}"):
            media = database.hget(f"race.data:{ctx.channel.id}",
                                  "media").decode("utf-8")
        await self.send_bird_(ctx, media, filters, taxon, state)

    # goatsucker command - no args
    # just for fun, no real purpose
    @commands.command(help="- Sends a random goatsucker to ID", aliases=["gs"])
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def goatsucker(self, ctx):
        logger.info("command: goatsucker")

        if database.exists(f"race.data:{ctx.channel.id}"):
            await ctx.send("This command is disabled during races.")
            return

        answered = int(database.hget(f"channel:{ctx.channel.id}", "answered"))
        # check to see if previous bird was answered
        if answered:  # if yes, give a new bird
            session_increment(ctx, "total", 1)

            database.hset(f"channel:{ctx.channel.id}", "answered", "0")
            currentBird = random.choice(goatsuckers)
            self.increment_bird_frequency(ctx, currentBird)

            database.hset(f"channel:{ctx.channel.id}", "bird",
                          str(currentBird))
            logger.info("currentBird: " + str(currentBird))
            await send_bird(
                ctx,
                currentBird,
                "images",
                Filter(),
                on_error=self.error_skip(ctx),
                message=GS_MESSAGE,
            )
        else:  # if no, give the same bird
            await send_bird(
                ctx,
                database.hget(f"channel:{ctx.channel.id}",
                              "bird").decode("utf-8"),
                "images",
                Filter(),
                on_error=self.error_skip(ctx),
                message=GS_MESSAGE,
            )
예제 #8
0
class States(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    async def broken_send(self, ctx, message: str, between: str = ""):
        pages: list[str] = []
        temp_lines: list[str] = []
        temp_len = 0
        for line in message.splitlines(keepends=True):
            temp_lines.append(line)
            temp_len += len(line)
            if temp_len > 1700:
                temp_out = f"{between}{''.join(temp_lines)}{between}"
                pages.append(temp_out)
                temp_lines.clear()

        if temp_lines:
            temp_out = f"{between}{''.join(temp_lines)}{between}"
            pages.append(temp_out)

        for page in pages:
            await ctx.send(page)

    # set state role
    @commands.command(help="- Sets your state", name="set", aliases=["state"])
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    @commands.guild_only()
    @commands.bot_has_permissions(manage_roles=True)
    async def state(self, ctx, *, args):
        logger.info("command: state set")

        raw_roles = ctx.author.roles
        role_ids = [role.id for role in raw_roles]
        role_names = [role.name.lower() for role in ctx.author.roles]
        args = args.upper().split(" ")

        if "CUSTOM" in args and (
                not database.exists(f"custom.list:{ctx.author.id}")
                or database.exists(f"custom.confirm:{ctx.author.id}")):
            await ctx.send(
                "Sorry, you don't have a custom list! Use `b!custom` to set your custom list."
            )
            return

        added = []
        removed = []
        invalid = []
        for arg in args:
            if arg not in states:
                logger.info("invalid state")
                invalid.append(arg)

            # gets similarities
            elif not set(role_names).intersection(set(states[arg]["aliases"])):
                # need to add role (does not have role)
                logger.info("add roles")
                raw_roles = ctx.guild.roles
                guild_role_names = [role.name.lower() for role in raw_roles]
                guild_role_ids = [role.id for role in raw_roles]

                if states[arg]["aliases"][0].lower() in guild_role_names:
                    # guild has role
                    index = guild_role_names.index(
                        states[arg]["aliases"][0].lower())
                    role = ctx.guild.get_role(guild_role_ids[index])

                else:
                    # create role
                    logger.info("creating role")
                    role = await ctx.guild.create_role(
                        name=string.capwords(states[arg]["aliases"][0]),
                        permissions=discord.Permissions.none(),
                        hoist=False,
                        mentionable=False,
                        reason="Create state role for bird list",
                    )

                await ctx.author.add_roles(
                    role, reason="Set state role for bird list")
                added.append(role.name)

            else:
                # have roles already (there were similarities)
                logger.info("already has role, removing")
                index = role_names.index(states[arg]["aliases"][0].lower())
                role = ctx.guild.get_role(role_ids[index])
                await ctx.author.remove_roles(
                    role, reason="Remove state role for bird list")
                removed.append(role.name)

        await ctx.send((
            f"**Sorry,** `{'`, `'.join(invalid)}` **{'are' if len(invalid) > 1 else 'is'} not a valid state.**\n"
            + f"*Valid States:* `{'`, `'.join(states.keys())}`\n"
            if invalid else ""
        ) + (
            f"**Added the** `{'`, `'.join(added)}` **role{'s' if len(added) > 1 else ''}**\n"
            if added else ""
        ) + (
            f"**Removed the** `{'`, `'.join(removed)}` **role{'s' if len(removed) > 1 else ''}**\n"
            if removed else ""))

    # set custom bird list
    @commands.command(
        brief="- Sets your custom bird list",
        help="- Sets your custom bird list. " +
        "This command only works in DMs. Lists have a max size of 200 birds. "
        +
        "When verifying, the bot may incorrectly say it didn't find any images. "
        +
        "If this is the case and you have verified yourself by going to https://macaulaylibrary.org, "
        +
        "just try again later. You can use your custom list anywhere you would use "
        + "a state with the `CUSTOM` 'state'.",
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    @commands.dm_only()
    async def custom(self, ctx, *, args=""):
        logger.info("command: custom list set")

        args = args.lower().strip().split(" ")
        logger.info(f"parsed args: {args}")

        if ("replace" not in args and ctx.message.attachments
                and database.exists(f"custom.list:{ctx.author.id}")):
            await ctx.send(
                "Woah there. You already have a custom list. " +
                "To view its contents, use `b!custom view`. " +
                "If you want to replace your list, upload the file with `b!custom replace`."
            )
            return

        if "delete" in args and database.exists(
                f"custom.list:{ctx.author.id}"):
            if (database.exists(f"custom.confirm:{ctx.author.id}")
                    and database.get(f"custom.confirm:{ctx.author.id}").decode(
                        "utf-8") == "delete"):
                database.delete(f"custom.list:{ctx.author.id}",
                                f"custom.confirm:{ctx.author.id}")
                await ctx.send("Ok, your list was deleted.")
                return

            database.set(f"custom.confirm:{ctx.author.id}", "delete", ex=86400)
            await ctx.send(
                "Are you sure you want to permanently delete your list? " +
                "Use `b!custom delete` again within 24 hours to clear your custom list."
            )
            return

        if ("confirm" in args
                and database.exists(f"custom.confirm:{ctx.author.id}") and
                database.get(f"custom.confirm:{ctx.author.id}").decode("utf-8")
                == "confirm"):
            # list was validated by server and user, making permanent
            logger.info("user confirmed")
            database.persist(f"custom.list:{ctx.author.id}")
            database.delete(f"custom.confirm:{ctx.author.id}")
            database.set(f"custom.cooldown:{ctx.author.id}", 0, ex=86400)
            await ctx.send(
                "Ok, your custom bird list is now available. Use `b!custom view` "
                +
                "to view your list. You can change your list again in 24 hours."
            )
            return

        if ("validate" in args
                and database.exists(f"custom.confirm:{ctx.author.id}") and
                database.get(f"custom.confirm:{ctx.author.id}").decode("utf-8")
                == "valid"):
            # list was validated, now for user confirm
            logger.info("valid list, user needs to confirm")
            database.expire(f"custom.list:{ctx.author.id}", 86400)
            database.set(f"custom.confirm:{ctx.author.id}",
                         "confirm",
                         ex=86400)
            birdlist = "\n".join(
                bird.decode("utf-8")
                for bird in database.smembers(f"custom.list:{ctx.author.id}"))
            await ctx.send(
                f"**Please confirm the following list.** ({int(database.scard(f'custom.list:{ctx.author.id}'))} items)"
            )
            await self.broken_send(ctx, birdlist, between="```\n")
            await ctx.send(
                "Once you have looked over the list and are sure you want to add it, "
                +
                "please use `b!custom confirm` to have this list added as a custom list. "
                + "You have another 24 hours to confirm. " +
                "To start over, upload a new list with the message `b!custom replace`."
            )
            return

        if "view" in args:
            if not database.exists(f"custom.list:{ctx.author.id}"):
                await ctx.send(
                    "You don't have a custom list. To add a custom list, " +
                    "upload a txt file with a bird's name on each line to this DM "
                    + "and put `b!custom` in the **Add a Comment** section.")
                return
            birdlist = "\n".join(
                bird.decode("utf-8")
                for bird in database.smembers(f"custom.list:{ctx.author.id}"))
            birdlist = f"{birdlist}"
            await ctx.send(
                f"**Your Custom Bird List** ({int(database.scard(f'custom.list:{ctx.author.id}'))} items)"
            )
            await self.broken_send(ctx, birdlist, between="```\n")
            return

        if not database.exists(
                f"custom.list:{ctx.author.id}") or "replace" in args:
            # user inputted bird list, now validating
            start = time.perf_counter()
            if database.exists(f"custom.cooldown:{ctx.author.id}"):
                await ctx.send(
                    "Sorry, you'll have to wait 24 hours between changing lists."
                )
                return
            logger.info("reading received bird list")
            if not ctx.message.attachments:
                logger.info("no file detected")
                await ctx.send(
                    "Sorry, no file was detected. Upload your txt file and put `b!custom` in the **Add a Comment** section."
                )
                return
            decoded = await auto_decode(await
                                        ctx.message.attachments[0].read())
            if not decoded:
                logger.info("invalid character encoding")
                await ctx.send(
                    "Sorry, something went wrong. Are you sure this is a text file?"
                )
                return
            parsed_birdlist = set(
                map(lambda x: x.strip(),
                    decoded.strip().split("\n")))
            parsed_birdlist.discard("")
            parsed_birdlist = list(parsed_birdlist)
            if len(parsed_birdlist) > 200:
                logger.info("parsed birdlist too long")
                await ctx.send(
                    "Sorry, we're not supporting custom lists larger than 200 birds. Make sure there are no empty lines."
                )
                return
            logger.info("checking for invalid characters")
            char = re.compile(r"[^A-Za-z '\-\xC0-\xD6\xD8-\xF6\xF8-\xFF]")
            for item in parsed_birdlist:
                if len(item) > 1000:
                    logger.info("item too long")
                    await ctx.send(
                        f"Line starting with `{item[:100]}` exceeds 1000 characters."
                    )
                    return
                search = char.search(item)
                if search:
                    logger.info("invalid character detected")
                    await ctx.send(
                        f"An invalid character `{search.group()}` was detected. Only letters, spaces, hyphens, and apostrophes are allowed."
                    )
                    await ctx.send(
                        f"Error on line starting with `{item[:100]}`, position {search.span()[0]}"
                    )
                    return
            database.delete(f"custom.list:{ctx.author.id}",
                            f"custom.confirm:{ctx.author.id}")
            await self.validate(ctx, parsed_birdlist)
            elapsed = time.perf_counter() - start
            await ctx.send(
                f"**Finished validation in {round(elapsed//60)} minutes {round(elapsed%60, 4)} seconds.** {ctx.author.mention}"
            )
            logger.info(
                f"Finished validation in {round(elapsed//60)} minutes {round(elapsed%60, 4)} seconds."
            )
            return

        if database.exists(f"custom.confirm:{ctx.author.id}"):
            next_step = database.get(f"custom.confirm:{ctx.author.id}").decode(
                "utf-8")
            if next_step == "valid":
                await ctx.send(
                    "You need to validate your list. Use `b!custom validate` to do so. "
                    +
                    "You can also delete or replace your list with `b!custom [delete|replace]`"
                )
                return
            if next_step == "confirm":
                await ctx.send(
                    "You need to confirm your list. Use `b!custom confirm` to do so. "
                    +
                    "You can also delete or replace your list with `b!custom [delete|replace]`"
                )
                return
            if next_step == "delete":
                await ctx.send(
                    "You're in the process of deleting your list. Use `b!custom delete` to do so. "
                    + "You can also replace your list with `b!custom replace`")
                return
            capture_message(
                f"custom.confirm database invalid with {next_step}")
            await ctx.send(
                "Whoops, something went wrong. Please report this incident " +
                "in the support server below.\nhttps://discord.gg/2HbshwGjnm")
            return

        await ctx.send(
            "Use `b!custom view` to view your bird list or `b!custom replace` to replace your bird list."
        )

    async def validate(self, ctx, parsed_birdlist):
        validated_birdlist = []
        async with aiohttp.ClientSession() as session:
            logger.info("starting validation")
            await ctx.send(
                "**Validating bird list...**\n*This may take a while.*")
            invalid_output = []
            valid_output = []
            validity = []
            for x in range(0, len(parsed_birdlist), 10):
                validity += await asyncio.gather(
                    *(valid_bird(bird, session)
                      for bird in parsed_birdlist[x:x + 10]))
                logger.info("sleeping during validation...")
                await asyncio.sleep(5)
            logger.info("checking validation")
            for item in validity:
                if item[1]:
                    validated_birdlist.append(
                        string.capwords(
                            item[3].split(" - ")[0].strip().replace("-", " ")))
                    valid_output.append(
                        f"Item `{item[0]}`: Detected as **{item[3]}**\n")
                else:
                    invalid_output.append(
                        f"Item `{item[0]}`: **{item[2]}** {f'(Detected as *{item[3]}*)' if item[3] else ''}\n"
                    )
            logger.info("done validating")

        if valid_output:
            logger.info("sending validation success")
            valid_output = (
                "**Succeeded Items:** Please verify items were detected correctly.\n"
                + "".join(valid_output))
            await self.broken_send(ctx, valid_output)
        if invalid_output:
            logger.info("sending validation failure")
            invalid_output = "**FAILED ITEMS:** Please fix and resubmit.\n" + "".join(
                invalid_output)
            await self.broken_send(ctx, invalid_output)
            return False

        await ctx.send("**Saving bird list...**")
        database.sadd(f"custom.list:{ctx.author.id}", *validated_birdlist)
        database.expire(f"custom.list:{ctx.author.id}", 86400)
        database.set(f"custom.confirm:{ctx.author.id}", "valid", ex=86400)
        await ctx.send(
            "**Ok!** Your bird list has been temporarily saved. " +
            "Please use `b!custom validate` to view and confirm your bird list. "
            +
            "To start over, upload a new list with the message `b!custom replace`. "
            +
            "You have 24 hours to confirm before your bird list will automatically be deleted."
        )
        return True

    @state.error
    async def set_error(self, ctx, error):
        logger.info("state set error")
        if isinstance(error, commands.MissingRequiredArgument):
            await ctx.send(
                f"**Please enter your state.**\n*Valid States:* `{'`, `'.join(states.keys())}`"
            )
        else:
            await handle_error(ctx, error)
예제 #9
0
class Sessions(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    async def _get_options(self, ctx):
        filter_int, state, taxon, wiki, strict = database.hmget(
            f"session.data:{ctx.author.id}",
            ["filter", "state", "taxon", "wiki", "strict"],
        )
        filters = Filter.from_int(int(filter_int))
        options = textwrap.dedent(f"""\
            **Active Filters:** `{'`, `'.join(filters.display())}`
            **State bird list:** {state.decode('utf-8') if state else 'None'}
            **Bird taxon:** {taxon.decode('utf-8') if taxon else 'None'}
            **Wiki Embeds**: {wiki==b'wiki'}
            **Strict Spelling**: {strict==b'strict'}
            """)
        return options

    async def _get_stats(self, ctx):
        start, correct, incorrect, total = map(
            int,
            database.hmget(
                f"session.data:{ctx.author.id}",
                ["start", "correct", "incorrect", "total"],
            ),
        )
        elapsed = datetime.timedelta(seconds=round(time.time()) - start)
        try:
            accuracy = round(100 * (correct / (correct + incorrect)), 2)
        except ZeroDivisionError:
            accuracy = 0

        stats = textwrap.dedent(f"""\
            **Duration:** `{elapsed}`
            **# Correct:** {correct}
            **# Incorrect:** {incorrect}
            **Total Birds:** {total}
            **Accuracy:** {accuracy}%
            """)
        return stats

    async def _send_stats(self, ctx, preamble):
        database_key = f"session.incorrect:{ctx.author.id}"

        embed = discord.Embed(type="rich",
                              colour=discord.Color.blurple(),
                              title=preamble)
        embed.set_author(name="Bird ID - An Ornithology Bot")

        if database.zcard(database_key) != 0:
            leaderboard_list = database.zrevrangebyscore(
                database_key, "+inf", "-inf", 0, 5, True)
            leaderboard = ""

            for i, stats in enumerate(leaderboard_list):
                leaderboard += (
                    f"{i+1}. **{stats[0].decode('utf-8')}** - {int(stats[1])}\n"
                )
        else:
            logger.info(f"no birds in {database_key}")
            leaderboard = "**There are no missed birds.**"

        embed.add_field(name="Options",
                        value=await self._get_options(ctx),
                        inline=False)
        embed.add_field(name="Stats",
                        value=await self._get_stats(ctx),
                        inline=False)
        embed.add_field(name="Top Missed Birds",
                        value=leaderboard,
                        inline=False)

        await ctx.send(embed=embed)

    @commands.group(
        brief="- Base session command",
        help="- Base session command\n" +
        "Sessions will record your activity for an amount of time and " +
        "will give you stats on how your performance and " +
        "also set global variables such as black and white, " +
        "state specific bird lists, specific bird taxons, or bird age/sex. ",
        aliases=["ses", "sesh"],
    )
    async def session(self, ctx):
        if ctx.invoked_subcommand is None:
            await ctx.send(
                "**Invalid subcommand passed.**\n*Valid Subcommands:* `start, view, stop`"
            )

    # starts session
    @session.command(
        brief="- Starts session",
        help="""- Starts session.
        Arguments passed will become the default arguments to 'b!bird', but can be manually overwritten during use. 
        These settings can be changed at any time with 'b!session edit', and arguments can be passed in any order. 
        However, having both females and juveniles are not supported.""",
        aliases=["st"],
        usage="[state] [taxons] [filters]",
    )
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.user))
    async def start(self, ctx, *, args_str: str = ""):
        logger.info("command: start session")

        if database.exists(f"session.data:{ctx.author.id}"):
            logger.info("already session")
            await ctx.send(
                "**There is already a session running.** *Change settings/view stats with `b!session edit`*"
            )
            return

        filters = Filter.parse(args_str)

        args = args_str.lower().split(" ")
        logger.info(f"args: {args}")

        if "wiki" in args:
            wiki = ""
        else:
            wiki = "wiki"

        if "strict" in args:
            strict = "strict"
        else:
            strict = ""

        states_args = set(states.keys()).intersection(
            {arg.upper()
             for arg in args})
        if states_args:
            state = " ".join(states_args).strip()
        else:
            state = " ".join(check_state_role(ctx))

        taxon_args = set(taxons.keys()).intersection(
            {arg.lower()
             for arg in args})
        if taxon_args:
            taxon = " ".join(taxon_args).strip()
        else:
            taxon = ""

        logger.info(
            f"adding filters: {filters}; state: {state}; wiki: {wiki}; strict: {strict}"
        )

        database.hset(
            f"session.data:{ctx.author.id}",
            mapping={
                "start": round(time.time()),
                "stop": 0,
                "correct": 0,
                "incorrect": 0,
                "total": 0,
                "filter": str(filters.to_int()),
                "state": state,
                "taxon": taxon,
                "wiki": wiki,
                "strict": strict,
            },
        )
        await ctx.send(
            f"**Session started with options:**\n{await self._get_options(ctx)}"
        )

    # views session
    @session.command(
        brief="- Views session",
        help=
        "- Views session\nSessions will record your activity for an amount of time and "
        +
        "will give you stats on how your performance and also set global variables such as black and white, "
        + "state specific bird lists, specific bird taxons, or bird age/sex. ",
        aliases=["view"],
        usage="[state] [taxons] [filters]",
    )
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.user))
    async def edit(self, ctx, *, args_str: str = ""):
        logger.info("command: view session")

        if database.exists(f"session.data:{ctx.author.id}"):
            new_filter = Filter.parse(args_str, defaults=False)

            args = args_str.lower().split(" ")
            logger.info(f"args: {args}")

            new_filter ^= int(
                database.hget(f"session.data:{ctx.author.id}", "filter"))
            database.hset(f"session.data:{ctx.author.id}", "filter",
                          str(new_filter.to_int()))

            if "wiki" in args:
                if database.hget(f"session.data:{ctx.author.id}", "wiki"):
                    logger.info("enabling wiki embeds")
                    database.hset(f"session.data:{ctx.author.id}", "wiki", "")
                else:
                    logger.info("disabling wiki embeds")
                    database.hset(f"session.data:{ctx.author.id}", "wiki",
                                  "wiki")

            if "strict" in args:
                if database.hget(f"session.data:{ctx.author.id}", "strict"):
                    logger.info("disabling strict spelling")
                    database.hset(f"session.data:{ctx.author.id}", "strict",
                                  "")
                else:
                    logger.info("enabling strict spelling")
                    database.hset(f"session.data:{ctx.author.id}", "strict",
                                  "strict")

            states_args = set(states.keys()).intersection(
                {arg.upper()
                 for arg in args})
            if states_args:
                current_states = set(
                    database.hget(f"session.data:{ctx.author.id}",
                                  "state").decode("utf-8").split(" "))
                logger.info(f"toggle states: {states_args}")
                logger.info(f"current states: {current_states}")
                states_args.symmetric_difference_update(current_states)
                logger.info(f"new states: {states_args}")
                database.hset(
                    f"session.data:{ctx.author.id}",
                    "state",
                    " ".join(states_args).strip(),
                )

            taxon_args = set(taxons.keys()).intersection(
                {arg.lower()
                 for arg in args})
            if taxon_args:
                current_taxons = set(
                    database.hget(f"session.data:{ctx.author.id}",
                                  "taxon").decode("utf-8").split(" "))
                logger.info(f"toggle taxons: {taxon_args}")
                logger.info(f"current taxons: {current_taxons}")
                taxon_args.symmetric_difference_update(current_taxons)
                logger.info(f"new taxons: {taxon_args}")
                database.hset(
                    f"session.data:{ctx.author.id}",
                    "taxon",
                    " ".join(taxon_args).strip(),
                )

            await self._send_stats(ctx, "**Session started previously.**\n")
        else:
            await ctx.send(
                "**There is no session running.** *You can start one with `b!session start`*"
            )

    # stops session
    @session.command(help="- Stops session", aliases=["stp", "end"])
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.user))
    async def stop(self, ctx):
        logger.info("command: stop session")

        if database.exists(f"session.data:{ctx.author.id}"):
            database.hset(f"session.data:{ctx.author.id}", "stop",
                          round(time.time()))

            await self._send_stats(ctx, "**Session stopped.**\n")
            database.delete(f"session.data:{ctx.author.id}")
            database.delete(f"session.incorrect:{ctx.author.id}")
        else:
            await ctx.send(
                "**There is no session running.** *You can start one with `b!session start`*"
            )
예제 #10
0
class Meta(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    # bot info command - gives info on bot
    @commands.command(
        help="- Gives info on bot, support server invite, stats",
        aliases=["bot_info", "support"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def botinfo(self, ctx):
        logger.info("command: botinfo")

        embed = discord.Embed(type="rich", colour=discord.Color.blurple())
        embed.set_author(name="Bird ID - An Ornithology Bot")
        embed.add_field(
            name="Bot Info",
            value="This bot was created by EraserBird and person_v1.32 " +
            "for helping people practice bird identification for Science Olympiad.\n"
            + "**By adding this bot to a server, you are agreeing to our " +
            "[Privacy Policy](<https://github.com/tctree333/Bird-ID/blob/master/PRIVACY.md>) and "
            +
            "[Terms of Service](<https://github.com/tctree333/Bird-ID/blob/master/TERMS.md>)**.\n"
            +
            "Bird-ID is licensed under the [GNU GPL v3.0](<https://github.com/tctree333/Bird-ID/blob/master/LICENSE>).",
            inline=False,
        )
        embed.add_field(
            name="Support",
            value="If you are experiencing any issues, have feature requests, "
            +
            "or want to get updates on bot status, join our support server below.",
            inline=False,
        )
        embed.add_field(
            name="Stats",
            value=
            f"This bot can see {len(self.bot.users)} users and is in {len(self.bot.guilds)} servers. "
            + f"The WebSocket latency is {round((self.bot.latency*1000))} ms.",
            inline=False,
        )
        await ctx.send(embed=embed)
        await ctx.send("https://discord.gg/fXxYyDJ")

    # ping command - gives bot latency
    @commands.command(
        help="- Pings the bot and displays latency", )
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    async def ping(self, ctx):
        logger.info("command: ping")
        lat = round(self.bot.latency * 1000)
        logger.info(f"latency: {lat}")
        await ctx.send(f"**Pong!** The WebSocket latency is `{lat}` ms.")

    # invite command - sends invite link
    @commands.command(help="- Get the invite link for this bot")
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def invite(self, ctx):
        logger.info("command: invite")

        embed = discord.Embed(type="rich", colour=discord.Color.blurple())
        embed.set_author(name="Bird ID - An Ornithology Bot")
        embed.add_field(
            name="Invite",
            value=
            "To invite this bot to your own server, use the following invite links.\n"
            +
            "**Bird-ID:** https://discord.com/api/oauth2/authorize?client_id=601917808137338900&permissions=268486656&scope=bot\n"
            +
            "**Orni-Bot:** https://discord.com/api/oauth2/authorize?client_id=601755752410906644&permissions=268486656&scope=bot\n\n"
            +
            "**By adding this bot to a server, you are agreeing to our `Privacy Policy` and `Terms of Service`**.\n"
            +
            "<https://github.com/tctree333/Bird-ID/blob/master/PRIVACY.md>, <https://github.com/tctree333/Bird-ID/blob/master/TERMS.md>\n\n"
            +
            "Unfortunately, Orni-Bot is currently unavailable. For more information, visit our support server below.",
            inline=False,
        )
        await ctx.send(embed=embed)
        await ctx.send("https://discord.gg/fXxYyDJ")

    # ignore command - ignores a given channel
    @commands.command(
        brief="- Ignore all commands in a channel",
        help=
        "- Ignore all commands in a channel. The 'manage guild' permission is needed to use this command.",
    )
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    @commands.has_guild_permissions(manage_guild=True)
    async def ignore(self,
                     ctx,
                     channels: commands.Greedy[discord.TextChannel] = None):
        logger.info("command: invite")

        added = ""
        removed = ""
        if channels is not None:
            logger.info(f"ignored channels: {[c.name for c in channels]}")
            for channel in channels:
                if database.zscore("ignore:global", str(channel.id)) is None:
                    added += f"`#{channel.name}` (`{channel.category.name if channel.category else 'No Category'}`)\n"
                    database.zadd("ignore:global",
                                  {str(channel.id): ctx.guild.id})
                else:
                    removed += f"`#{channel.name}` (`{channel.category.name if channel.category else 'No Category'}`)\n"
                    database.zrem("ignore:global", str(channel.id))
        else:
            await ctx.send("**No valid channels were passed.**")

        ignored = "".join([
            f"`#{channel.name}` (`{channel.category.name if channel.category else 'No Category'}`)\n"
            for channel in map(
                lambda c: ctx.guild.get_channel(int(c)),
                database.zrangebyscore("ignore:global", ctx.guild.id -
                                       0.1, ctx.guild.id + 0.1),
            )
        ])

        await ctx.send(
            (f"**Ignoring:**\n{added}" if added else "") +
            (f"**Stopped ignoring:**\n{removed}" if removed else "") +
            (f"**Ignored Channels:**\n{ignored}" if ignored else
             "**No channels in this server are currently ignored.**"))

    # leave command - removes itself from guild
    @commands.command(
        brief="- Remove the bot from the guild",
        help=
        "- Remove the bot from the guild. The 'manage guild' permission is needed to use this command.",
        aliases=["kick"],
    )
    @commands.check(CustomCooldown(2.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    @commands.has_guild_permissions(manage_guild=True)
    async def leave(self, ctx, confirm: typing.Optional[bool] = False):
        logger.info("command: leave")

        if database.exists(f"leave:{ctx.guild.id}"):
            logger.info("confirming")
            if confirm:
                logger.info(f"confirmed. Leaving {ctx.guild}")
                database.delete(f"leave:{ctx.guild.id}")
                await ctx.send("**Ok, bye!**")
                await ctx.guild.leave()
                return
            logger.info("confirm failed. leave canceled")
            database.delete(f"leave:{ctx.guild.id}")
            await ctx.send("**Leave canceled.**")
            return

        logger.info("not confirmed")
        database.set(f"leave:{ctx.guild.id}", 0, ex=60)
        await ctx.send(
            "**Are you sure you want to remove me from the guild?**\n" +
            "Use `b!leave yes` to confirm, `b!leave no` to cancel. " +
            "You have 60 seconds to confirm before it will automatically cancel."
        )

    # ban command - prevents certain users from using the bot
    @commands.command(help="- ban command", hidden=True)
    @commands.is_owner()
    async def ban(self, ctx, *, user: typing.Optional[discord.Member] = None):
        logger.info("command: ban")
        if user is None:
            logger.info("no args")
            await ctx.send("Invalid User!")
            return
        logger.info(f"user-id: {user.id}")
        database.zadd("banned:global", {str(user.id): 0})
        await ctx.send(f"Ok, {user.name} cannot use the bot anymore!")

    # unban command - prevents certain users from using the bot
    @commands.command(help="- unban command", hidden=True)
    @commands.is_owner()
    async def unban(self,
                    ctx,
                    *,
                    user: typing.Optional[discord.Member] = None):
        logger.info("command: unban")
        if user is None:
            logger.info("no args")
            await ctx.send("Invalid User!")
            return
        logger.info(f"user-id: {user.id}")
        database.zrem("banned:global", str(user.id))
        await ctx.send(f"Ok, {user.name} can use the bot!")
예제 #11
0
class Other(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @staticmethod
    def broken_join(input_list: list[str],
                    max_size: int = MAX_MESSAGE) -> list[str]:
        pages: list[str] = []
        lines: list[str] = []
        block_length = 0
        for line in input_list:
            lines.append(line)
            block_length += len(line)
            if block_length > max_size:
                page = "\n".join(lines)
                pages.append(page)
                lines.clear()
                block_length = 0

        if lines:
            page = "\n".join(lines)
            pages.append(page)

        return pages

    # Info - Gives call+image of 1 bird
    @commands.command(
        brief="- Gives an image and call of a bird",
        help=
        "- Gives an image and call of a bird. The bird name must come before any options.",
        usage="[bird] [options]",
        aliases=["i"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def info(self, ctx, *, arg):
        logger.info("command: info")
        arg = arg.lower().strip()

        filters = Filter.parse(arg)
        if filters.vc:
            filters.vc = False
            await ctx.send("**The VC filter is not allowed here!**")

        options = filters.display()
        arg = arg.split(" ")

        bird = None

        if len(arg[0]) == 4:
            bird = alpha_codes.get(arg[0].upper())

        if not bird:
            for i in reversed(range(1, 6)):
                # try the first 5 words, then first 4, etc. looking for a match
                matches = get_close_matches(
                    string.capwords(" ".join(arg[:i]).replace("-", " ")),
                    birdListMaster + sciListMaster,
                    n=1,
                    cutoff=0.8,
                )
                if matches:
                    bird = matches[0]
                    break

        if not bird:
            await ctx.send("Bird not found. Are you sure it's on the list?")
            return

        delete = await ctx.send("Please wait a moment.")
        if options:
            await ctx.send(f"**Detected filters**: `{'`, `'.join(options)}`")

        an = "an" if bird.lower()[0] in ("a", "e", "i", "o", "u") else "a"
        await send_bird(ctx,
                        bird,
                        "images",
                        filters,
                        message=f"Here's {an} *{bird.lower()}* image!")
        await send_bird(ctx,
                        bird,
                        "songs",
                        filters,
                        message=f"Here's {an} *{bird.lower()}* song!")
        await delete.delete()
        return

    # Filter command - lists available Macaulay Library filters and aliases
    @commands.command(help="- Lists available Macaulay Library filters.",
                      aliases=["filter"])
    @commands.check(CustomCooldown(8.0, bucket=commands.BucketType.user))
    async def filters(self, ctx):
        logger.info("command: filters")
        filters = Filter.aliases()
        embed = discord.Embed(
            title="Media Filters",
            type="rich",
            description="Filters can be space-separated or comma-separated. " +
            "You can use any alias to set filters. " +
            "Please note media will only be shown if it " +
            "matches all the filters, so using filters can " +
            "greatly reduce the number of media returned.",
            color=discord.Color.green(),
        )
        embed.set_author(name="Bird ID - An Ornithology Bot")
        for title, subdict in filters.items():
            value = "".join((f"**{name.title()}**: `{'`, `'.join(aliases)}`\n"
                             for name, aliases in subdict.items()))
            embed.add_field(name=title.title(), value=value, inline=False)
        await ctx.send(embed=embed)

    # List command - argument is state/bird list
    @commands.command(help="- DMs the user with the appropriate bird list.",
                      name="list")
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def list_of_birds(self, ctx, state: str = "blank"):
        logger.info("command: list")

        state = state.upper()

        if state not in states:
            logger.info("invalid state")
            await ctx.send(
                f"**Sorry, `{state}` is not a valid state.**\n*Valid States:* `{', '.join(map(str, list(states.keys())))}`"
            )
            return

        state_birdlist = sorted(
            build_id_list(user_id=ctx.author.id, state=state, media="images"))
        state_songlist = sorted(
            build_id_list(user_id=ctx.author.id, state=state, media="songs"))

        birdLists = self.broken_join(state_birdlist)
        songLists = self.broken_join(state_songlist)

        if ctx.author.dm_channel is None:
            await ctx.author.create_dm()

        await ctx.author.dm_channel.send(f"**The {state} bird list:**")
        for birds in birdLists:
            await ctx.author.dm_channel.send(f"```\n{birds}```")

        await ctx.author.dm_channel.send(f"**The {state} bird songs:**")
        for birds in songLists:
            await ctx.author.dm_channel.send(f"```\n{birds}```")

        await ctx.send(
            f"The `{state}` bird list has **{len(state_birdlist)}** birds.\n" +
            f"The `{state}` bird list has **{len(state_songlist)}** songs.\n" +
            "*A full list of birds has been sent to you via DMs.*")

    # taxons command - argument is state/bird list
    @commands.command(
        help="- DMs the user with the appropriate bird list.",
        name="taxon",
        aliases=["taxons", "orders", "families", "order", "family"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def bird_taxons(self,
                          ctx,
                          taxon: str = "blank",
                          state: str = "NATS"):
        logger.info("command: taxons")

        taxon = taxon.lower()
        state = state.upper()

        if taxon not in taxons:
            logger.info("invalid taxon")
            await ctx.send(
                f"**Sorry, `{taxon}` is not a valid taxon.**\n*Valid taxons:* `{', '.join(map(str, list(taxons.keys())))}`"
            )
            return

        if state not in states:
            logger.info("invalid state")
            await ctx.send(
                f"**Sorry, `{state}` is not a valid state.**\n*Valid States:* `{', '.join(map(str, list(states.keys())))}`"
            )
            return

        bird_list = sorted(
            build_id_list(user_id=ctx.author.id,
                          taxon=taxon,
                          state=state,
                          media="images"))
        song_bird_list = sorted(
            build_id_list(user_id=ctx.author.id,
                          taxon=taxon,
                          state=state,
                          media="songs"))
        if not bird_list and not song_bird_list:
            logger.info("no birds for taxon/state")
            await ctx.send(
                "**Sorry, no birds could be found for the taxon/state combo.**\n*Please try again*"
            )
            return

        birdLists = self.broken_join(bird_list)
        songLists = self.broken_join(song_bird_list)

        if ctx.author.dm_channel is None:
            await ctx.author.create_dm()

        await ctx.author.dm_channel.send(
            f"**The `{taxon}` in the `{state}` bird list:**")
        for birds in birdLists:
            await ctx.author.dm_channel.send(f"```\n{birds}```")

        await ctx.author.dm_channel.send(
            f"**The `{taxon}` in the `{state}` bird songs:**")
        for birds in songLists:
            await ctx.author.dm_channel.send(f"```\n{birds}```")

        await ctx.send(
            f"The `{taxon}` in the `{state}` bird list has **{len(bird_list)}** birds.\n"
            +
            f"The `{taxon}` in the `{state}` bird list has **{len(song_bird_list)}** songs.\n"
            + "*A full list of birds has been sent to you via DMs.*")

    # Wiki command - argument is the wiki page
    @commands.command(help="- Fetch the wikipedia page for any given argument",
                      aliases=["wiki"])
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.user))
    async def wikipedia(self, ctx, *, arg):
        logger.info("command: wiki")

        arg = arg.capitalize()

        try:
            page = wikipedia.page(arg, auto_suggest=False)
        except (wikipedia.exceptions.DisambiguationError,
                wikipedia.exceptions.PageError):
            try:
                page = wikipedia.page(f"{arg} (bird)", auto_suggest=False)
            except (wikipedia.exceptions.DisambiguationError,
                    wikipedia.exceptions.PageError):
                # fall back to suggestion
                try:
                    page = wikipedia.page(arg)
                except wikipedia.exceptions.DisambiguationError:
                    await ctx.send(
                        "Sorry, that page was not found. Try being more specific."
                    )
                    return
                except wikipedia.exceptions.PageError:
                    await ctx.send("Sorry, that page was not found.")
                    return
        await ctx.send(page.url)

    # meme command - sends a random bird video/gif
    @commands.command(help="- Sends a funny bird video!")
    @commands.check(
        CustomCooldown(180.0, disable=True, bucket=commands.BucketType.user))
    async def meme(self, ctx):
        logger.info("command: meme")
        await ctx.send(random.choice(memeList))

    # Send command - for testing purposes only
    @commands.command(help="- send command", hidden=True, aliases=["sendas"])
    @commands.is_owner()
    async def send_as_bot(self, ctx, *, args):
        logger.info("command: send")
        logger.info(f"args: {args}")
        channel_id = int(args.split(" ")[0])
        message = args.strip(str(channel_id))
        channel = self.bot.get_channel(channel_id)
        await channel.send(message)
        await ctx.send("Ok, sent!")

    # Test command - for testing purposes only
    @commands.command(help="- test command", hidden=True)
    @commands.is_owner()
    async def cache(self, ctx):
        logger.info("command: cache stats")
        items = []
        with contextlib.suppress(FileNotFoundError):
            items += os.listdir("bot_files/cache/images/")
        with contextlib.suppress(FileNotFoundError):
            items += os.listdir("bot_files/cache/songs/")
        stats = {
            "sciname_cache": get_sciname.cache_info(),
            "taxon_cache": get_taxon.cache_info(),
            "num_downloaded_birds": len(items),
        }
        await ctx.send(f"```python\n{stats}```")

    # Test command - for testing purposes only
    @commands.command(help="- test command", hidden=True)
    @commands.is_owner()
    async def error(self, ctx):
        logger.info("command: error")
        await ctx.send(1 / 0)

    # Test command - for testing purposes only
    @commands.command(help="- test command", hidden=True)
    @commands.is_owner()
    async def test(
        self,
        ctx,
        *,
        user: typing.Optional[typing.Union[discord.Member, discord.User,
                                           str]] = None,
    ):
        logger.info("command: test")
        await ctx.send(f"```\nMembers Intent: {self.bot.intents.members}\n" +
                       f"Message Mentions: {ctx.message.mentions}\n" +
                       f"User: {user}\nType: {type(user)}```")
예제 #12
0
class Stats(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @staticmethod
    def generate_series(database_key):
        """Generates a pandas.Series from a Redis sorted set."""
        logger.info("generating series")
        data = database.zrevrangebyscore(database_key,
                                         "+inf",
                                         "-inf",
                                         withscores=True)
        return pd.Series({
            e[0]: e[1]
            for e in map(lambda x: (x[0].decode("utf-8"), int(x[1])), data)
        })

    @staticmethod
    def generate_dataframe(database_keys, titles, index=None):
        """Generates a pandas.DataFrame from multiple Redis sorted sets."""
        pipe = database.pipeline()
        for key in database_keys:
            pipe.zrevrangebyscore(key, "+inf", "-inf", withscores=True)
        result = pipe.execute()
        index = {v[0].decode("utf-8")
                 for r in result for v in r}.union(set(index if index else {}))
        df = pd.DataFrame(index=index)
        for title, item in zip(titles, result):
            df.insert(
                len(df.columns),
                title,
                pd.Series({
                    e[0]: e[1]
                    for e in map(lambda x: (x[0].decode("utf-8"), int(x[1])),
                                 item)
                }),
            )
        df = df.fillna(value=0).astype(int)
        return df

    async def convert_users(self, df):
        """Converts discord user ids in DataFrames or Series indexes to usernames."""
        current_ids = df.index
        new_index = []
        for user_id in current_ids:
            user = await fetch_get_user(int(user_id),
                                        bot=self.bot,
                                        member=False)

            if user is None:
                new_index.append("User Unavailable")
            else:
                new_index.append(f"{user.name}#{user.discriminator}")
        df.index = new_index
        return df

    # give frequency stats
    @commands.command(
        help="- Gives info on command/bird frequencies",
        usage="[command|commands|c  bird|birds|b] [page]",
        aliases=["freq"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def frequency(self, ctx, scope="", page=1):
        logger.info("command: frequency")

        if scope in ("command", "commands", "c"):
            database_key = "frequency.command:global"
            title = "Most Frequently Used Commands"
        elif scope in ("bird", "birds", "b"):
            database_key = "frequency.bird:global"
            title = "Most Frequent Birds"
        else:
            await ctx.send(
                "**Invalid Scope!**\n*Valid Scopes:* `commands, birds`")
            return

        await send_leaderboard(ctx, title, page, database_key)

    # give bot stats
    @commands.command(
        help="- Gives statistics on different topics",
        usage="[topic]",
        aliases=["stat"],
    )
    @commands.check(CustomCooldown(5.0, bucket=commands.BucketType.channel))
    async def stats(self, ctx, topic="help"):
        logger.info("command: stats")

        if topic in ("scores", "score", "s"):
            topic = "scores"
        elif topic in ("usage", "u"):
            topic = "usage"
        elif topic in ("web", "w"):
            topic = "web"
        elif topic in ("help", ""):
            topic = "help"
        else:
            valid_topics = ("help", "scores", "usage", "web")
            await ctx.send(
                f"**`{topic}` is not a valid topic!**\nValid Topics: `{'`, `'.join(valid_topics)}`"
            )
            return

        embed = discord.Embed(
            title="Bot Stats",
            type="rich",
            color=discord.Color.blue(),
        )

        if topic == "help":
            embed.description = (
                "**Available statistic topics.**\n" +
                "This command is in progress and more stats may be added. " +
                "If there is a statistic you would like to see here, " +
                "please let us know in the support server.")
            embed.add_field(
                name="Scores",
                value=
                "`b!stats [scores|score|s]`\n*Displays stats about scores.*",
            ).add_field(
                name="Usage",
                value="`b!stats [usage|u]`\n*Displays stats about usage.*",
            ).add_field(
                name="Web",
                value="`b!stats [web|w]`\n*Displays stats about web usage.*",
            )

        elif topic == "scores":
            embed.description = "**Score Statistics**"
            scores = self.generate_series("users:global")
            scores = scores[scores > 0]
            c, d = np.histogram(scores,
                                bins=range(0, 1100, 100),
                                range=(0, 1000))
            c = (c / len(scores) * 100).round(1)
            embed.add_field(
                name="Totals",
                inline=False,
                value="**Sum of top 10 user scores:** `{:,}`\n".format(
                    scores.nlargest(n=10).sum()) +
                "**Sum of all positive user scores:** `{:,}`\n".format(
                    scores.sum()),
            ).add_field(
                name="Computations",
                inline=False,
                value="**Mean of all positive user scores:** `{:,.2f}`\n".
                format(scores.mean()) +
                "**Median of all positive user scores:** `{:,.1f}`\n".format(
                    scores.median()),
            ).add_field(
                name="Distributions",
                inline=False,
                value=
                f"**Number of users with scores over mean:** `{len(scores[scores > scores.mean()])}`\n"
                + "**Percentage of users with scores over mean:** `{:.1%}`".
                format(len(scores[scores > scores.mean()]) / len(scores)) +
                "\n**Percentage of users with scores between:**\n" + "".join(
                    f"\u2192 *{d[i]}-{d[i+1]-1}*: `{c[i]}%`\n"  # \u2192 is the "Rightwards Arrow"
                    for i in range(len(c))),
            )

        elif topic == "usage":
            embed.description = "**Usage Statistics**"

            today = datetime.datetime.now(datetime.timezone.utc).date()
            past_month = pd.date_range(  # pylint: disable=no-member
                today - datetime.timedelta(29), today).date
            keys = list(f"daily.score:{str(date)}" for date in past_month)
            keys = ["users:global"] + keys
            titles = reversed(range(
                1, 32))  # label columns by # days ago, today is 1 day ago
            month = self.generate_dataframe(keys, titles)
            total = month.loc[:, 31]
            month = month.loc[:, 30:1]  # remove totals column
            month = month.loc[(month != 0).any(1)]  # remove users with all 0s
            week = month.loc[:, 7:1]  # generate week from month
            week = week.loc[(week != 0).any(1)]
            today = week.loc[:, 1]  # generate today from week
            today = today.loc[today != 0]

            channels_see = len(list(self.bot.get_all_channels()))
            channels_used = int(database.zcard("score:global"))

            embed.add_field(
                name="Today (Since midnight UTC)",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(today)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    today.sum()),
            ).add_field(
                name="Last 7 Days",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(week)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    week.sum().sum()),
            ).add_field(
                name="Last 30 Days",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(month)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    month.sum().sum()),
            ).add_field(
                name="Total",
                inline=False,
                value="**Channels the bot can see:** `{:,}`\n".format(
                    channels_see) +
                "**Channels that have used the bot at least once:** `{:,} ({:,.1%})`\n"
                .format(channels_used, channels_used / channels_see) +
                "*(Note: Deleted channels or channels that the bot can't see anymore are still counted).*\n"
                +
                "**Accounts that have used any command at least once:** `{:,}`\n"
                .format(len(total)) +
                "**Accounts that answered at least 1 correctly:** `{:,} ({:,.1%})`\n"
                .format(len(total[total > 0]),
                        len(total[total > 0]) / len(total)),
            )

        elif topic == "web":
            embed.description = "**Web Usage Statistics**"

            today = datetime.datetime.now(datetime.timezone.utc).date()
            past_month = pd.date_range(  # pylint: disable=no-member
                today - datetime.timedelta(29), today).date
            web_score = (f"daily.webscore:{str(date)}" for date in past_month)
            web_usage = (f"daily.web:{str(date)}" for date in past_month)
            titles = tuple(reversed(range(
                1, 31)))  # label columns by # days ago, today is 1 day ago

            web_score_month = self.generate_dataframe(web_score, titles)
            web_score_week = web_score_month.loc[:, 7:
                                                 1]  # generate week from month
            web_score_week = web_score_week.loc[(
                web_score_week !=
                0).any(1)]  # remove users with no correct answers
            web_score_today = web_score_week.loc[:,
                                                 1]  # generate today from week
            web_score_today = web_score_today.loc[
                web_score_today != 0]  # remove users with no correct answers

            web_usage_month = self.generate_dataframe(web_usage,
                                                      titles,
                                                      index=("check", "skip",
                                                             "hint"))
            web_usage_week = web_usage_month.loc[:, 7:1]
            web_usage_today = web_usage_week.loc[:, 1]

            score_totals_keys = sorted(
                map(
                    lambda x: x.decode("utf-8"),
                    database.scan_iter(match="daily.webscore:????-??-??",
                                       count=5000),
                ))
            score_totals_titles = map(lambda x: x.split(":")[1],
                                      score_totals_keys)
            web_score_total = self.generate_dataframe(score_totals_keys,
                                                      score_totals_titles)

            usage_totals_keys = sorted(
                map(
                    lambda x: x.decode("utf-8"),
                    database.scan_iter(match="daily.web:????-??-??",
                                       count=5000),
                ))
            usage_totals_titles = map(lambda x: x.split(":")[1],
                                      usage_totals_keys)
            web_usage_total = self.generate_dataframe(usage_totals_keys,
                                                      usage_totals_titles,
                                                      index=("check", "skip",
                                                             "hint"))

            embed.add_field(
                name="Today (Since midnight UTC)",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(web_score_today)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    web_score_today.sum()) +
                "**Check command usage:** `{:,}`\n".format(
                    web_usage_today.loc["check"]) +
                "**Skip command usage:** `{:,}`\n".format(
                    web_usage_today.loc["skip"]) +
                "**Hint command usage:** `{:,}`\n".format(
                    web_usage_today.loc["hint"]),
            ).add_field(
                name="Last 7 Days",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(web_score_week)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    web_score_week.sum().sum()) +
                "**Check command usage:** `{:,}`\n".format(
                    web_usage_week.loc["check"].sum()) +
                "**Skip command usage:** `{:,}`\n".format(
                    web_usage_week.loc["skip"].sum()) +
                "**Hint command usage:** `{:,}`\n".format(
                    web_usage_week.loc["hint"].sum()),
            ).add_field(
                name="Last 30 Days",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(web_score_month)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    web_score_month.sum().sum()) +
                "**Check command usage:** `{:,}`\n".format(
                    web_usage_month.loc["check"].sum()) +
                "**Skip command usage:** `{:,}`\n".format(
                    web_usage_month.loc["skip"].sum()) +
                "**Hint command usage:** `{:,}`\n".format(
                    web_usage_month.loc["hint"].sum()),
            ).add_field(
                name="Total",
                inline=False,
                value="**Accounts that answered at least 1 correctly:** `{:,}`\n"
                .format(len(web_score_total)) +
                "**Total birds answered correctly:** `{:,}`\n".format(
                    web_score_total.sum().sum()) +
                "**Check command usage:** `{:,}`\n".format(
                    web_usage_total.loc["check"].sum()) +
                "**Skip command usage:** `{:,}`\n".format(
                    web_usage_total.loc["skip"].sum()) +
                "**Hint command usage:** `{:,}`\n".format(
                    web_usage_total.loc["hint"].sum()),
            )

        await ctx.send(embed=embed)
        return

    # export data as csv
    @commands.command(help="- Exports bot data as a csv")
    @commands.check(CustomCooldown(60.0, bucket=commands.BucketType.channel))
    async def export(self, ctx):
        logger.info("command: export")

        files = []

        async def _export_helper(database_keys,
                                 header: str,
                                 filename: str,
                                 users=False):
            if not isinstance(database_keys, str) and len(database_keys) > 1:
                data = self.generate_dataframe(database_keys,
                                               header.strip().split(",")[1:])
            else:
                key = (database_keys
                       if isinstance(database_keys, str) else database_keys[0])
                data = self.generate_series(key)
            if users:
                data = await self.convert_users(data)
            with StringIO() as f:
                f.write(header)
                data.to_csv(f, header=False)
                with BytesIO(f.getvalue().encode("utf-8")) as b:
                    files.append(discord.File(b, filename))

        logger.info("exporting freq command")
        await _export_helper(
            "frequency.command:global",
            "command,amount used\n",
            "command_frequency.csv",
            users=False,
        )

        logger.info("exporting freq bird")
        await _export_helper(
            "frequency.bird:global",
            "bird,amount seen\n",
            "bird_frequency.csv",
            users=False,
        )

        logger.info("exporting streaks")
        await _export_helper(
            ["streak:global", "streak.max:global"],
            "username#discrim,current streak,max streak\n",
            "streaks.csv",
            True,
        )

        logger.info("exporting missed")
        keys = sorted(
            map(
                lambda x: x.decode("utf-8"),
                database.scan_iter(match="daily.incorrect:????-??-??",
                                   count=5000),
            ))
        titles = ",".join(map(lambda x: x.split(":")[1], keys))
        keys = ["incorrect:global"] + keys
        await _export_helper(keys,
                             f"bird name,total missed,{titles}\n",
                             "missed.csv",
                             users=False)

        logger.info("exporting scores")
        keys = sorted(
            map(
                lambda x: x.decode("utf-8"),
                database.scan_iter(match="daily.score:????-??-??", count=5000),
            ))
        titles = ",".join(map(lambda x: x.split(":")[1], keys))
        keys = ["users:global"] + keys
        await _export_helper(keys,
                             f"username#discrim,total score,{titles}\n",
                             "scores.csv",
                             users=True)

        logger.info("exporting web scores")
        keys = sorted(
            map(
                lambda x: x.decode("utf-8"),
                database.scan_iter(match="daily.webscore:????-??-??",
                                   count=5000),
            ))
        titles = ",".join(map(lambda x: x.split(":")[1], keys))
        await _export_helper(keys,
                             f"username#discrim,{titles}\n",
                             "web_scores.csv",
                             users=True)

        logger.info("exporting web usage")
        keys = sorted(
            map(
                lambda x: x.decode("utf-8"),
                database.scan_iter(match="daily.web:????-??-??", count=5000),
            ))
        titles = ",".join(map(lambda x: x.split(":")[1], keys))
        await _export_helper(keys,
                             f"command,{titles}\n",
                             "web_usage.csv",
                             users=False)

        await ctx.send(files=files)
예제 #13
0
class Voice(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.cleanup.start()

    def cog_unload(self):
        self.cleanup.cancel()

    @commands.command(help="- Play a sound")
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def play(self, ctx):
        logger.info("command: play")
        await voice_functions.play(ctx, None)

    @commands.command(help="- Pause playing")
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def pause(self, ctx):
        logger.info("command: pause")
        await voice_functions.pause(ctx)

    @commands.command(help="- Stop playing")
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def stop(self, ctx):
        logger.info("command: stop")
        await voice_functions.stop(ctx)

    @commands.command(help="- Skip forward 5 seconds",
                      aliases=["fw", "forwards"])
    @commands.check(CustomCooldown(2.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def forward(self, ctx, seconds: int = 5):
        logger.info("command: forward")
        if seconds < 1:
            await ctx.send("Invalid number of seconds!")
            return
        await voice_functions.rel_seek(ctx, seconds)

    @commands.command(help="- Skip back 5 seconds",
                      aliases=["bk", "backward", "backwards"])
    @commands.check(CustomCooldown(2.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def back(self, ctx, seconds: int = 5):
        logger.info("command: back")
        if seconds < 1:
            await ctx.send("Invalid number of seconds!")
            return
        await voice_functions.rel_seek(ctx, seconds * -1)

    @commands.command(help="- Replay", aliases=["rp", "re"])
    @commands.check(CustomCooldown(2.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def replay(self, ctx):
        logger.info("command: replay")
        await voice_functions.rel_seek(ctx, None)

    @commands.command(help="- Disconnect from voice", aliases=["dc"])
    @commands.check(CustomCooldown(3.0, bucket=commands.BucketType.channel))
    @commands.guild_only()
    async def disconnect(self, ctx):
        logger.info("command: disconnect")
        current_voice = database.get(f"voice.server:{ctx.guild.id}")
        if current_voice is not None:
            race = ctx.bot.get_cog("Race")
            await race.stop_race_(ctx)
        else:
            await voice_functions.disconnect(ctx)

    @tasks.loop(minutes=10)
    async def cleanup(self):
        logger.info("running cleanup task")
        await voice_functions.cleanup(self.bot)