Beispiel #1
0
    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
        )
Beispiel #2
0
    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)
Beispiel #3
0
async def send_bird(ctx,
                    bird: str,
                    media_type: str,
                    filters: Filter,
                    on_error=None,
                    message=None):
    """Gets bird media and sends it to the user.

    `ctx` - Discord context object\n
    `bird` (str) - bird to send\n
    `media_type` (str) - type of media (images/songs)\n
    `filters` (bot.filters Filter)\n
    `on_error` (function) - async function to run when an error occurs, passes error as argument\n
    `message` (str) - text message to send before bird\n
    """
    if bird == "":
        logger.error("error - bird is blank")
        await ctx.send("**There was an error fetching birds.**")
        if on_error is not None:
            await on_error(GenericError("bird is blank", code=100))
        else:
            await ctx.send("*Please try again.*")
        return

    # add special condition for screech owls
    # since screech owl is a genus and SciOly
    # doesn't specify a species
    if bird == "Screech Owl":
        logger.info("choosing specific Screech Owl")
        bird = random.choice(screech_owls)

    delete = await ctx.send("**Fetching.** This may take a while.")
    # trigger "typing" discord message
    await ctx.trigger_typing()

    try:
        filename, extension = await get_media(ctx, bird, media_type, filters)
    except GenericError as e:
        await delete.delete()
        if e.code == 100:
            await ctx.send(
                f"**This combination of filters has no valid {media_type} for the current bird.**"
            )
        elif e.code == 201:
            capture_exception(e)
            logger.exception(e)
            await ctx.send(
                "**A network error has occurred.**\n*Please try again later.*")
            database.incrby("cooldown:global", amount=1)
            database.expire("cooldown:global", 300)
        else:
            capture_exception(e)
            logger.exception(e)
            await ctx.send(
                f"**An error has occurred while fetching {media_type}.**\n**Reason:** {e}"
            )
        if on_error is not None:
            await on_error(e)
        else:
            await ctx.send("*Please try again.*")
        return

    if os.stat(
            filename).st_size > MAX_FILESIZE:  # another filesize check (4mb)
        await delete.delete()
        await ctx.send("**Oops! File too large :(**\n*Please try again.*")
        return

    if media_type == "images":
        if filters.bw:
            # prevent the black and white conversion from blocking
            loop = asyncio.get_running_loop()
            fn = functools.partial(_black_and_white, filename)
            filename = await loop.run_in_executor(None, fn)

    elif media_type == "songs" and not filters.vc:
        # remove spoilers in tag metadata
        audioFile = eyed3.load(filename)
        if audioFile is not None and audioFile.tag is not None:
            audioFile.tag.remove(filename)

    if message is not None:
        await ctx.send(message)

    if media_type == "songs" and filters.vc:
        await voice_functions.play(ctx, filename)
    else:
        # change filename to avoid spoilers
        file_obj = discord.File(filename, filename=f"bird.{extension}")
        await ctx.send(file=file_obj)
    await delete.delete()
Beispiel #4
0
    async def refresh_backup():
        """Sends a copy of the database to a discord channel (BACKUPS_CHANNEL)."""
        logger.info("TASK: Refreshing backup")
        try:
            os.remove("bot_files/backups/dump.dump")
            logger.info("Cleared backup dump")
        except FileNotFoundError:
            logger.info("Already cleared backup dump")
        try:
            os.remove("bot_files/backups/keys.txt")
            logger.info("Cleared backup keys")
        except FileNotFoundError:
            logger.info("Already cleared backup keys")

        event_loop = asyncio.get_event_loop()
        with concurrent.futures.ThreadPoolExecutor(1) as executor:
            await event_loop.run_in_executor(executor, backup_all)

        logger.info("Sending backup files")
        channel = bot.get_channel(int(BACKUPS_CHANNEL))
        with open("bot_files/backups/dump.dump", "rb") as f:
            await channel.send(file=discord.File(f, filename="dump"))
        with open("bot_files/backups/keys.txt", "r") as f:
            await channel.send(file=discord.File(f, filename="keys.txt"))
        logger.info("Backup Files Sent!")
Beispiel #5
0
async def user_setup(ctx):
    """Sets up a new discord user for score tracking.

    `ctx` - Discord context object or user id
    """
    if isinstance(ctx, (str, int)):
        user_id = str(ctx)
        guild = None
        ctx = None
    else:
        user_id = str(ctx.author.id)
        guild = ctx.guild

    logger.info("checking user data")
    if database.zscore("users:global", user_id) is None:
        database.zadd("users:global", {user_id: 0})
        logger.info("user global added")
        if ctx is not None:
            await ctx.send("Welcome <@" + user_id + ">!")

    date = str(datetime.datetime.now(datetime.timezone.utc).date())
    if database.zscore(f"daily.score:{date}", user_id) is None:
        database.zadd(f"daily.score:{date}", {user_id: 0})
        logger.info("user daily added")

    # Add streak
    if (database.zscore("streak:global", user_id) is None) or (database.zscore(
            "streak.max:global", user_id) is None):
        database.zadd("streak:global", {user_id: 0})
        database.zadd("streak.max:global", {user_id: 0})
        logger.info("added streak")

    if guild is not None:
        global_score = database.zscore("users:global", str(ctx.author.id))
        database.zadd(f"users.server:{ctx.guild.id}",
                      {str(ctx.author.id): global_score})
        logger.info("synced scores")

        if not database.exists(f"custom.list:{ctx.author.id}"):
            role_ids = [role.id for role in ctx.author.roles]
            role_names = [role.name.lower() for role in ctx.author.roles]
            if set(role_names).intersection(set(states["CUSTOM"]["aliases"])):
                index = role_names.index(
                    states["CUSTOM"]["aliases"][0].lower())
                role = ctx.guild.get_role(role_ids[index])
                await ctx.author.remove_roles(
                    role, reason="Remove state role for bird list")
                logger.info("synced roles")
Beispiel #6
0
    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)

        accepted_answers = [currentBird, sciBird]
        if currentBird == "screech owl":
            accepted_answers += screech_owls
            accepted_answers += sci_screech_owls

        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 accepted_answers
            else:
                logger.info("spelling leniency")
                correct = better_spellcheck(
                    arg, accepted_answers, birdListMaster + sciListMaster
                )

            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 accepted_answers
            else:
                logger.info("spelling leniency")
                correct = (
                    better_spellcheck(
                        arg, accepted_answers, birdListMaster + sciListMaster
                    )
                    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)
Beispiel #7
0
 async def refresh_user_cache():
     """Task to update User cache to increase performance of commands."""
     logger.info("TASK: Updating User cache")
     await get_all_users(bot)
Beispiel #8
0
async def bird_setup(ctx, bird: str):
    """Sets up a new bird for incorrect tracking.
    
    `ctx` - Discord context object
    `bird` - bird to setup
    """
    logger.info("checking bird data")
    if database.zscore("incorrect:global", string.capwords(bird)) is not None:
        logger.info("bird global ok")
    else:
        database.zadd("incorrect:global", {string.capwords(bird): 0})
        logger.info("bird global added")

    if database.zscore(f"incorrect.user:{ctx.author.id}",
                       string.capwords(bird)) is not None:
        logger.info("bird user ok")
    else:
        database.zadd(f"incorrect.user:{ctx.author.id}",
                      {string.capwords(bird): 0})
        logger.info("bird user added")

    if ctx.guild is not None:
        logger.info("no dm")
        if database.zscore(f"incorrect.server:{ctx.guild.id}",
                           string.capwords(bird)) is not None:
            logger.info("bird server ok")
        else:
            database.zadd(f"incorrect.server:{ctx.guild.id}",
                          {string.capwords(bird): 0})
            logger.info("bird server added")
    else:
        logger.info("dm context")

    if database.exists(f"session.data:{ctx.author.id}"):
        logger.info("session in session")
        if database.zscore(f"session.incorrect:{ctx.author.id}",
                           string.capwords(bird)) is not None:
            logger.info("bird session ok")
        else:
            database.zadd(f"session.incorrect:{ctx.author.id}",
                          {string.capwords(bird): 0})
            logger.info("bird session added")
    else:
        logger.info("no session")
Beispiel #9
0
async def send_bird(ctx,
                    bird: str,
                    on_error=None,
                    message=None,
                    addOn="",
                    bw=False):
    """Gets a bird picture and sends it to the user.

    `ctx` - Discord context object\n
    `bird` (str) - bird picture to send\n
    `on_error` (function)- function to run when an error occurs\n
    `message` (str) - text message to send before bird picture\n
    `addOn` (str) - string to append to search for female/juvenile birds\n
    `bw` (bool) - whether the image should be black and white (converts with `_black_and_white()`)
    """
    if bird == "":
        logger.error("error - bird is blank")
        await ctx.send(
            "**There was an error fetching birds.**\n*Please try again.*")
        if on_error is not None:
            on_error(ctx)
        return

    # add special condition for screech owls
    # since screech owl is a genus and SciOly
    # doesn't specify a species
    if bird == "Screech Owl":
        logger.info("choosing specific Screech Owl")
        bird = random.choice(screech_owls)

    delete = await ctx.send("**Fetching.** This may take a while.")
    # trigger "typing" discord message
    await ctx.trigger_typing()

    try:
        response = await get_image(ctx, bird, addOn)
    except GenericError as e:
        await delete.delete()
        await ctx.send(
            f"**An error has occurred while fetching images.**\n*Please try again.*\n**Reason:** {e}"
        )
        logger.exception(e)
        if on_error is not None:
            on_error(ctx)
        return

    filename = str(response[0])
    extension = str(response[1])
    statInfo = os.stat(filename)
    if statInfo.st_size > 4000000:  # another filesize check (4mb)
        await delete.delete()
        await ctx.send("**Oops! File too large :(**\n*Please try again.*")
    else:
        if bw:
            # prevent the black and white conversion from blocking
            loop = asyncio.get_running_loop()
            fn = partial(_black_and_white, filename)
            file_stream = await loop.run_in_executor(None, fn)
        else:
            file_stream = filename

        if message is not None:
            await ctx.send(message)

        # change filename to avoid spoilers
        file_obj = discord.File(file_stream, filename=f"bird.{extension}")
        await ctx.send(file=file_obj)
        await delete.delete()
Beispiel #10
0
async def handle_error(ctx, error):
    """Function for comprehensive error handling."""

    if isinstance(error, commands.CommandOnCooldown):  # send cooldown
        await ctx.send(
            ("**Cooldowns have been temporarily increased due to increased usage.**"
             if getattr(error.cooldown, "rate_limit", False) else
             "**Cooldown.** ") + "Try again after " +
            str(round(error.retry_after, 2)) + " s.",
            delete_after=5.0,
        )

    elif isinstance(error, commands.CommandNotFound):
        capture_exception(error)
        await ctx.send("Sorry, the command was not found.")

    elif isinstance(error, commands.MissingRequiredArgument):
        await ctx.send("This command requires an argument!")

    elif isinstance(error, commands.BadArgument):
        await ctx.send("The argument passed was invalid. Please try again.")

    elif isinstance(error, commands.ArgumentParsingError):
        await ctx.send("An invalid character was detected. Please try again.")

    elif isinstance(error, commands.BotMissingPermissions):
        await ctx.send(
            "**The bot does not have enough permissions to fully function.**\n"
            +
            f"**Permissions Missing:** `{', '.join(map(str, error.missing_perms))}`\n"
            + "*Please try again once the correct permissions are set.*")

    elif isinstance(error, commands.MissingPermissions):
        await ctx.send(
            "You do not have the required permissions to use this command.\n" +
            f"**Required Perms:** `{'`, `'.join(error.missing_perms)}`")

    elif isinstance(error, commands.NoPrivateMessage):
        await ctx.send("**This command is unavailable in DMs!**")

    elif isinstance(error, commands.PrivateMessageOnly):
        await ctx.send("**This command is only available in DMs!**")

    elif isinstance(error, commands.NotOwner):
        logger.info("not owner")
        await ctx.send("Sorry, the command was not found.")

    elif isinstance(error, GenericError):
        if error.code == 192:
            # channel is ignored
            return
        if error.code == 842:
            await ctx.send("**Sorry, you cannot use this command.**")
        elif error.code == 666:
            logger.info("GenericError 666")
        elif error.code == 201:
            logger.info("HTTP Error")
            capture_exception(error)
            await ctx.send(
                "**An unexpected HTTP Error has occurred.**\n *Please try again.*"
            )
        else:
            logger.info("uncaught generic error")
            capture_exception(error)
            await ctx.send(
                "**An uncaught generic error has occurred.**\n" +
                "*Please log this message in #support in the support server below, or try again.*\n"
                + f"**Error code:** `{error.code}`")
            await ctx.send("https://discord.gg/2HbshwGjnm")
            raise error

    elif isinstance(error, commands.CommandInvokeError):
        if isinstance(error.original, redis.exceptions.ResponseError):
            capture_exception(error.original)
            if database.exists(f"channel:{ctx.channel.id}"):
                await ctx.send(
                    "**An unexpected ResponseError has occurred.**\n" +
                    "*Please log this message in #support in the support server below, or try again.*\n"
                )
                await ctx.send("https://discord.gg/2HbshwGjnm")
            else:
                await channel_setup(ctx)
                await ctx.send("Please run that command again.")

        elif isinstance(error.original,
                        wikipedia.exceptions.DisambiguationError):
            await ctx.send("Wikipedia page not found. (Disambiguation Error)")

        elif isinstance(error.original, wikipedia.exceptions.PageError):
            await ctx.send("Wikipedia page not found. (Page Error)")

        elif isinstance(error.original,
                        wikipedia.exceptions.WikipediaException):
            capture_exception(error.original)
            await ctx.send("Wikipedia page unavailable. Try again later.")

        elif isinstance(error.original, discord.Forbidden):
            if error.original.code == 50007:
                await ctx.send(
                    "I was unable to DM you. Check if I was blocked and try again."
                )
            elif error.original.code == 50013:
                await ctx.send(
                    "There was an error with permissions. Check the bot has proper permissions and try again."
                )
            else:
                capture_exception(error)
                await ctx.send(
                    "**An unexpected Forbidden error has occurred.**\n" +
                    "*Please log this message in #support in the support server below, or try again.*\n"
                    + f"**Error code:** `{error.original.code}`")
                await ctx.send("https://discord.gg/2HbshwGjnm")

        elif isinstance(error.original, discord.HTTPException):
            capture_exception(error.original)
            if error.original.status == 502:
                await ctx.send(
                    "**An error has occurred with discord. :(**\n*Please try again.*"
                )
            else:
                await ctx.send(
                    "**An unexpected HTTPException has occurred.**\n" +
                    "*Please log this message in #support in the support server below, or try again*\n"
                    + f"**Reponse Code:** `{error.original.status}`")
                await ctx.send("https://discord.gg/2HbshwGjnm")

        elif isinstance(error.original, aiohttp.ClientOSError):
            capture_exception(error.original)
            if error.original.errno == errno.ECONNRESET:
                await ctx.send(
                    "**An error has occurred with discord. :(**\n*Please try again.*"
                )
            else:
                await ctx.send(
                    "**An unexpected ClientOSError has occurred.**\n" +
                    "*Please log this message in #support in the support server below, or try again.*\n"
                    + f"**Error code:** `{error.original.errno}`")
                await ctx.send("https://discord.gg/2HbshwGjnm")

        elif isinstance(error.original, aiohttp.ServerDisconnectedError):
            capture_exception(error.original)
            await ctx.send("**The server disconnected.**\n*Please try again.*")

        elif isinstance(error.original, asyncio.TimeoutError):
            capture_exception(error.original)
            await ctx.send(
                "**The request timed out.**\n*Please try again in a bit.*")

        elif isinstance(error.original, OSError):
            capture_exception(error.original)
            if error.original.errno == errno.ENOSPC:
                await ctx.send(
                    "**No space is left on the server!**\n" +
                    "*Please report this message in #support in the support server below!*\n"
                )
                await ctx.send("https://discord.gg/2HbshwGjnm")
            else:
                await ctx.send(
                    "**An unexpected OSError has occurred.**\n" +
                    "*Please log this message in #support in the support server below, or try again.*\n"
                    + f"**Error code:** `{error.original.errno}`")
                await ctx.send("https://discord.gg/2HbshwGjnm")

        else:
            logger.info("uncaught command error")
            capture_exception(error.original)
            await ctx.send(
                "**An uncaught command error has occurred.**\n" +
                "*Please log this message in #support in the support server below, or try again.*\n"
            )
            await ctx.send("https://discord.gg/2HbshwGjnm")
            raise error

    else:
        logger.info("uncaught non-command")
        capture_exception(error)
        await ctx.send(
            "**An uncaught non-command error has occurred.**\n" +
            "*Please log this message in #support in the support server below, or try again.*\n"
        )
        await ctx.send("https://discord.gg/2HbshwGjnm")
        raise error
Beispiel #11
0
 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.")
Beispiel #12
0
async def get_all_users(bot):
    logger.info("Starting user cache")
    user_ids = map(int, database.zrangebyscore("users:global", "-inf", "+inf"))
    for user_id in user_ids:
        await fetch_get_user(user_id, bot=bot, member=False)
    logger.info("User cache finished")
Beispiel #13
0
async def drone_attack(ctx):
    logger.info(f"holiday check: invoked command: {str(ctx.command)}")

    def video_embed():
        if random.randint(0, 1) == 1:
            embed = discord.Embed(
                title="YouTube",
                type="rich",
                colour=discord.Colour(0xD0021B),
                url="https://bit.ly/are-birds-real",
            )
            embed.set_image(
                url="http://i3.ytimg.com/vi/Fg_JcKSHUtQ/hqdefault.jpg")
            embed.add_field(
                name="TED",
                value=
                "[A robot that flies like a bird | Markus Fischer](https://bit.ly/are-birds-real)",
            )
        else:
            embed = discord.Embed(
                title="Are Birds Real?",
                type="rich",
                colour=discord.Colour.default(),
                url="https://bit.ly/are-birds-real",
            )
            embed.set_image(
                url=
                "https://www.sciencenews.org/sites/default/files/main/articles/feature_drones_opener.jpg"
            )
            embed.add_field(
                name="Wikipedia",
                value=
                "In 1947 the C.I.A. was founded, its sole responsibility to watch and survey tens of thousands of Americans suspected of doing communist things. In 1953 Allen Dulles was made the first civilian director of the Central Intelligence Agency (C.I.A.) and made it his mission to ramp up the surveillance program. Dulles and his team hated birds with a passion, as they would often poop on their cars in the parking lot of the C.I.A. headquarters. This was one of the driving forces that led Dulles to not only implement robots into the sky, but actually replace birds in the process...",
            )

        return embed

    if str(ctx.command) in (
            "help",
            "covid",
            "botinfo",
            "invite",
            "list",
            "meme",
            "taxon",
            "wikipedia",
            "remove",
            "set",
            "give_role",
            "remove_role",
            "test",
            "error",
            "ban",
            "unban",
            "send_as_bot",
    ):
        logger.info("Passthrough Command")
        return True

    if str(ctx.command) in ("bird", "song", "goatsucker"):
        images = os.listdir("bot/media/images/drone")
        path = f"bot/media/images/drone/{images[random.randint(0,len(images)-1)]}"
        BASE_MESSAGE = (
            "*Here you go!* \n**Use `b!{new_cmd}` again to get a new {media} of the same bird, "
            +
            "or `b!{skip_cmd}` to get a new bird. Use `b!{check_cmd} guess` to check your answer. "
            + "Use `b!{hint_cmd}` for a hint.**")

        if str(ctx.command) == "bird":
            await ctx.send(
                BASE_MESSAGE.format(
                    media="image",
                    new_cmd="bird",
                    skip_cmd="skip",
                    check_cmd="check",
                    hint_cmd="hint",
                ) + "\n*This is an image.*")
        elif str(ctx.command) == "goatsucker":
            await ctx.send(
                BASE_MESSAGE.format(
                    media="image",
                    new_cmd="gs",
                    skip_cmd="skip",
                    check_cmd="check",
                    hint_cmd="hint",
                ))
        elif str(ctx.command) == "bird":
            await ctx.send(
                BASE_MESSAGE.format(
                    media="song",
                    new_cmd="song",
                    skip_cmd="skip",
                    check_cmd="check",
                    hint_cmd="hint",
                ))

        file_obj = discord.File(path, filename=f"bird.{path.split('.')[-1]}")
        await ctx.send(file=file_obj)

    elif str(ctx.command) in ("check", ):
        args = ctx.message.content.split(" ")[1:]
        matches = difflib.get_close_matches(" ".join(args),
                                            birdListMaster + sciListMaster,
                                            n=1)
        if "drone" in args:
            await ctx.send(
                "SHHHHHH! Birds are **NOT** government drones! You'll blow our cover, and we'll need to get rid of you."
            )
        elif matches:
            await ctx.send(
                "Correct! Good job! The bird was **definitely a real bird**.")
            await ctx.send(embed=video_embed())
        else:
            await ctx.send(
                "Sorry, the bird was actually **definitely a real bird**.")
            await ctx.send(embed=video_embed())

    elif str(ctx.command) in ("skip", ):
        await ctx.send("Ok, skipping **definitely a real bird.**")
        await ctx.send(embed=video_embed())

    elif str(ctx.command) in ("hint", ):
        await ctx.send(
            "This is definitely a real bird, **NOT** a government drone.")

    elif str(ctx.command) in ("info", ):
        await ctx.send(
            "Birds are real. Don't believe what others may say. **BIRDS ARE VERY REAL!**"
        )

    elif str(ctx.command) in ("race", "session"):
        await ctx.send(
            "Races and sessions have been disabled today. We apologize for any inconvenience."
        )

    elif str(ctx.command) in ("leaderboard", "missed", "score", "streak",
                              "userscore"):
        embed = discord.Embed(
            type="rich",
            colour=discord.Color.blurple(),
            title=f"**{str(ctx.command).title()}**",
        )
        embed.set_author(name="Bird ID - An Ornithology Bot")
        embed.add_field(
            name=f"**{str(ctx.command).title()}**",
            value=
            "User scores and data have been cleared. We apologize for the inconvenience.",
            inline=False,
        )
        await ctx.send(embed=embed)

    raise GenericError(code=666)
Beispiel #14
0
    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.append(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="".join(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)
Beispiel #15
0
    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 += f"Item `{item[0]}`: Detected as **{item[3]}**\n"
                else:
                    invalid_output += 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"
                + 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" +
                              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
Beispiel #16
0
async def get_image(ctx, bird, addOn=None):
    """Chooses an image from a list of images.

    This function chooses a valid image to pass to send_bird().
    Valid images are based on file extension and size. (8mb discord limit)

    Returns a list containing the file path and extension type.

    `ctx` - Discord context object\n
    `bird` (str) - bird to get image of\n
    `addOn` (str) - string to append to search for female/juvenile birds\n
    """

    # fetch scientific names of birds
    try:
        sciBird = await get_sciname(bird)
    except GenericError:
        sciBird = bird
    images = await get_files(sciBird, "images", addOn)
    logger.info("images: " + str(images))
    prevJ = int(str(database.hget(f"channel:{ctx.channel.id}", "prevJ"))[2:-1])
    # Randomize start (choose beginning 4/5ths in case it fails checks)
    if images:
        j = (prevJ + 1) % len(images)
        logger.info("prevJ: " + str(prevJ))
        logger.info("j: " + str(j))

        for x in range(0, len(images)):  # check file type and size
            y = (x + j) % len(images)
            image_link = images[y]
            extension = image_link.split('.')[-1]
            logger.info("extension: " + str(extension))
            statInfo = os.stat(image_link)
            logger.info("size: " + str(statInfo.st_size))
            if extension.lower(
            ) in valid_image_extensions and statInfo.st_size < 4000000:  # keep files less than 4mb
                logger.info("found one!")
                break
            elif y == prevJ:
                raise GenericError("No Valid Images Found", code=999)

        database.hset(f"channel:{ctx.channel.id}", "prevJ", str(j))
    else:
        raise GenericError("No Images Found", code=100)

    return [image_link, extension]
Beispiel #17
0
    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 ""))
Beispiel #18
0
async def get_song(ctx, bird):
    """Chooses a song from a list of songs.

    This function chooses a valid song to pass to send_birdsong().
    Valid songs are based on file extension and size. (8mb discord limit)

    Returns a list containing the file path and extension type.

    `ctx` - Discord context object\n
    `bird` (str) - bird to get song of
    """

    # fetch scientific names of birds
    try:
        sciBird = await get_sciname(bird)
    except GenericError:
        sciBird = bird
    songs = await get_files(sciBird, "songs")
    logger.info("songs: " + str(songs))
    prevK = int(str(database.hget(f"channel:{ctx.channel.id}", "prevK"))[2:-1])
    if songs:
        k = (prevK + 1) % len(songs)
        logger.info("prevK: " + str(prevK))
        logger.info("k: " + str(k))

        for x in range(0, len(songs)):  # check file type and size
            y = (x + k) % len(songs)
            song_link = songs[y]
            extension = song_link.split('.')[-1]
            logger.info("extension: " + str(extension))
            statInfo = os.stat(song_link)
            logger.info("size: " + str(statInfo.st_size))
            if extension.lower(
            ) in valid_audio_extensions and statInfo.st_size < 4000000:  # keep files less than 4mb
                logger.info("found one!")
                break
            elif y == prevK:
                raise GenericError("No Valid Songs Found", code=999)

        database.hset(f"channel:{ctx.channel.id}", "prevK", str(k))
    else:
        raise GenericError("No Songs Found", code=100)

    return [song_link, extension]
Beispiel #19
0
 async def refresh_cache():
     """Task to delete a random selection of cached birds every hour."""
     logger.info("TASK: Refreshing some cache items")
     event_loop = asyncio.get_event_loop()
     with concurrent.futures.ThreadPoolExecutor(1) as executor:
         await event_loop.run_in_executor(executor, rotate_cache)
Beispiel #20
0
async def get_files(sciBird, media_type, addOn="", retries=0):
    """Returns a list of image/song filenames.

    This function also does cache management,
    looking for files in the cache for media and
    downloading images to the cache if not found.

    `sciBird` (str) - scientific name of bird\n
    `media_type` (str) - type of media (images/songs)\n
    `addOn` (str) - string to append to search for female/juvenile birds\n
    """
    logger.info(f"get_files retries: {retries}")
    directory = f"cache/{media_type}/{sciBird}{addOn}/"
    try:
        logger.info("trying")
        files_dir = os.listdir(directory)
        logger.info(directory)
        if not files_dir:
            raise GenericError("No Files", code=100)
        return [f"{directory}{path}" for path in files_dir]
    except (FileNotFoundError, GenericError):
        logger.info("fetching files")
        # if not found, fetch images
        logger.info("scibird: " + str(sciBird))
        filenames = await download_media(sciBird, media_type, addOn, directory)
        if not filenames:
            if retries < 3:
                retries += 1
                return await get_files(sciBird, media_type, addOn, retries)
            else:
                logger.info("More than 3 retries")
        return filenames
Beispiel #21
0
 async def evict_user_cache():
     """Task to remove keys from the User cache to ensure freshness."""
     logger.info("TASK: Removing user keys")
     prune_user_cache(10)
Beispiel #22
0
async def drone_attack(ctx):
    logger.info(f"holiday check: invoked command: {str(ctx.command)}")
    if str(ctx.command) in ("help", "covid", "botinfo", "invite", "list",
                            "meme", "taxon", "wikipedia", "remove", "set",
                            "give_role", "remove_role", "test", "error", "ban",
                            "unban", "send_as_bot"):
        logger.info("Passthrough Command")
        return True

    elif str(ctx.command) in ("bird", "song", "goatsucker"):
        images = os.listdir("bot/media/images/drone")
        path = f"bot/media/images/drone/{images[random.randint(0,len(images)-1)]}"
        BASE_MESSAGE = (
            "*Here you go!* \n**Use `b!{new_cmd}` again to get a new {media} of the same bird, "
            +
            "or `b!{skip_cmd}` to get a new bird. Use `b!{check_cmd} guess` to check your answer. "
            + "Use `b!{hint_cmd}` for a hint.**")

        if str(ctx.command) == "bird":
            await ctx.send(
                BASE_MESSAGE.format(media="image",
                                    new_cmd="bird",
                                    skip_cmd="skip",
                                    check_cmd="check",
                                    hint_cmd="hint") + "\n*This is an image.*")
        elif str(ctx.command) == "goatsucker":
            await ctx.send(
                BASE_MESSAGE.format(media="image",
                                    new_cmd="gs",
                                    skip_cmd="skipgoat",
                                    check_cmd="checkgoat",
                                    hint_cmd="hintgoat"))
        elif str(ctx.command) == "bird":
            await ctx.send(
                BASE_MESSAGE.format(media="song",
                                    new_cmd="song",
                                    skip_cmd="skipsong",
                                    check_cmd="checksong",
                                    hint_cmd="hintsong"))

        file_obj = discord.File(path, filename=f"bird.{path.split('.')[-1]}")
        await ctx.send(file=file_obj)

    elif str(ctx.command) in ("check", "checkgoat", "checksong"):
        args = ctx.message.content.split(" ")[1:]
        matches = difflib.get_close_matches(" ".join(args),
                                            birdListMaster + sciBirdListMaster,
                                            n=1)
        if "drone" in args:
            await ctx.send(
                "SHHHHHH! Birds are **NOT** government drones! You'll blow our cover, and we'll need to get rid of you."
            )
        elif matches:
            await ctx.send("Correct! Good job!")
            url = get_wiki_url(matches[0])
            await ctx.send(url)
        else:
            await ctx.send(
                "Sorry, the bird was actually **definitely a real bird.**")
            await ctx.send(
                ("https://en.wikipedia.org/wiki/Bird" if random.randint(0, 1)
                 == 0 else "https://youtu.be/Fg_JcKSHUtQ"))

    elif str(ctx.command) in ("skip", "skipgoat", "skipsong"):
        await ctx.send("Ok, skipping **definitely a real bird.**")
        await ctx.send(("https://en.wikipedia.org/wiki/Bird" if random.randint(
            0, 1) == 0 else "https://youtu.be/Fg_JcKSHUtQ"))

    elif str(ctx.command) in ("hint", "hintgoat", "hintsong"):
        await ctx.send(
            "This is definitely a real bird, **NOT** a government drone.")

    elif str(ctx.command) in ("info"):
        await ctx.send(
            "Birds are real. Don't believe what others may say. **BIRDS ARE VERY REAL!**"
        )

    elif str(ctx.command) in ("race", "session"):
        await ctx.send(
            "Races and sessions have been disabled today. We apologize for any inconvenience."
        )

    elif str(ctx.command) in ("leaderboard", "missed", "score", "streak",
                              "userscore"):
        embed = discord.Embed(type="rich",
                              colour=discord.Color.blurple(),
                              title=f"**{str(ctx.command).title()}**")
        embed.set_author(name="Bird ID - An Ornithology Bot")
        embed.add_field(
            name=f"**{str(ctx.command).title()}**",
            value=
            "User scores and data have been cleared. We apologize for the inconvenience.",
            inline=False)
        await ctx.send(embed=embed)

    raise GenericError(code=666)
Beispiel #23
0
def bird_setup(ctx, bird: str):
    """Sets up a new bird for incorrect tracking.

    `ctx` - Discord context object or user id\n
    `bird` - bird to setup
    """
    if isinstance(ctx, (str, int)):
        user_id = ctx
        guild = None
    else:
        user_id = ctx.author.id
        guild = ctx.guild

    logger.info("checking bird data")
    if database.zscore("incorrect:global", string.capwords(bird)) is not None:
        logger.info("bird global ok")
    else:
        database.zadd("incorrect:global", {string.capwords(bird): 0})
        logger.info("bird global added")

    if database.zscore(f"incorrect.user:{user_id}",
                       string.capwords(bird)) is not None:
        logger.info("incorrect bird user ok")
    else:
        database.zadd(f"incorrect.user:{user_id}", {string.capwords(bird): 0})
        logger.info("incorrect bird user added")

    if database.zscore(f"correct.user:{user_id}",
                       string.capwords(bird)) is not None:
        logger.info("correct bird user ok")
    else:
        database.zadd(f"correct.user:{user_id}", {string.capwords(bird): 0})
        logger.info("correct bird user added")

    date = str(datetime.datetime.now(datetime.timezone.utc).date())
    if database.zscore(f"daily.incorrect:{date}",
                       string.capwords(bird)) is not None:
        logger.info("bird daily ok")
    else:
        database.zadd(f"daily.incorrect:{date}", {string.capwords(bird): 0})
        logger.info("bird daily added")

    if database.zscore("frequency.bird:global",
                       string.capwords(bird)) is not None:
        logger.info("bird freq global ok")
    else:
        database.zadd("frequency.bird:global", {string.capwords(bird): 0})
        logger.info("bird freq global added")

    if guild is not None:
        logger.info("no dm")
        if (database.zscore(f"incorrect.server:{ctx.guild.id}",
                            string.capwords(bird)) is not None):
            logger.info("bird server ok")
        else:
            database.zadd(f"incorrect.server:{ctx.guild.id}",
                          {string.capwords(bird): 0})
            logger.info("bird server added")
    else:
        logger.info("dm context")

    if database.exists(f"session.data:{user_id}"):
        logger.info("session in session")
        if (database.zscore(f"session.incorrect:{user_id}",
                            string.capwords(bird)) is not None):
            logger.info("bird session ok")
        else:
            database.zadd(f"session.incorrect:{user_id}",
                          {string.capwords(bird): 0})
            logger.info("bird session added")
    else:
        logger.info("no session")
Beispiel #24
0
async def user_setup(ctx):
    """Sets up a new discord user for score tracking.
    
    `ctx` - Discord context object
    """
    logger.info("checking user data")
    if database.zscore("users:global", str(ctx.author.id)) is not None:
        logger.info("user global ok")
    else:
        database.zadd("users:global", {str(ctx.author.id): 0})
        logger.info("user global added")
        await ctx.send("Welcome <@" + str(ctx.author.id) + ">!")

    #Add streak
    if (database.zscore("streak:global", str(ctx.author.id))
            is not None) and (database.zscore("streak.max:global",
                                              str(ctx.author.id)) is not None):
        logger.info("user streak in already")
    else:
        database.zadd("streak:global", {str(ctx.author.id): 0})
        database.zadd("streak.max:global", {str(ctx.author.id): 0})
        logger.info("added streak")

    if ctx.guild is not None:
        logger.info("no dm")
        if database.zscore(f"users.server:{ctx.guild.id}",
                           str(ctx.author.id)) is not None:
            server_score = database.zscore(f"users.server:{ctx.guild.id}",
                                           str(ctx.author.id))
            global_score = database.zscore("users:global", str(ctx.author.id))
            if server_score is global_score:
                logger.info("user server ok")
            else:
                database.zadd(f"users.server:{ctx.guild.id}",
                              {str(ctx.author.id): global_score})
        else:
            score = int(database.zscore("users:global", str(ctx.author.id)))
            database.zadd(f"users.server:{ctx.guild.id}",
                          {str(ctx.author.id): score})
            logger.info("user server added")
    else:
        logger.info("dm context")
Beispiel #25
0
    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
Beispiel #26
0
async def backup_all():
    """Backs up the database to a file.
    
    This function serializes all data in the REDIS database
    into a file in the `backups` directory.

    This function is run with a task every 6 hours and sends the files
    to a specified discord channel.
    """
    logger.info("Starting Backup")
    logger.info("Creating Dump")
    keys = [key.decode("utf-8") for key in database.keys()]
    dump = map(database.dump, keys)
    logger.info("Finished Dump")
    logger.info("Writing To File")
    try:
        os.mkdir("backups")
        logger.info("Created backups directory")
    except FileExistsError:
        logger.info("Backups directory exists")
    with open("backups/dump.dump", 'wb') as f:
        with open("backups/keys.txt", 'w') as k:
            for item, key in zip(dump, keys):
                pickle.dump(item, f)
                k.write(f"{key}\n")
    logger.info("Backup Finished")
Beispiel #27
0
async def get_taxon(bird: str, session=None, retries=0) -> Tuple[str, str]:
    """Returns the taxonomic code of a bird.

    Taxonomic codes are used by the Cornell Lab of Ornithology to identify species of birds.
    This function uses the Macaulay Library's internal API to fetch the taxon code
    from the common or scientific name, using `TAXON_CODE_URL`.
    Raises a `GenericError` if a code is not found or if an HTTP error occurs.

    `bird` (str) - common/scientific name of bird you want to look up\n
    `session` (optional) - an aiohttp client session
    """
    logger.info(f"getting taxon code for {bird}")
    async with contextlib.AsyncExitStack() as stack:
        if session is None:
            session = await stack.enter_async_context(aiohttp.ClientSession())
        taxon_code_url = TAXON_CODE_URL.format(
            urllib.parse.quote(bird.replace("-", " ").replace("'s", "")))
        async with session.get(taxon_code_url) as taxon_code_response:
            if taxon_code_response.status != 200:
                if retries >= 3:
                    logger.info("Retried more than 3 times. Aborting...")
                    raise GenericError(
                        f"An http error code of {taxon_code_response.status} occurred"
                        + f" while fetching {taxon_code_url} for {bird}",
                        code=201,
                    )
                retries += 1
                logger.info(
                    f"An HTTP error occurred; Retries: {retries}; Sleeping: {1.5**retries}"
                )
                await asyncio.sleep(1.5**retries)
                return await get_taxon(bird, session, retries)

            taxon_code_data = await taxon_code_response.json()
            try:
                logger.info(f"raw data: {taxon_code_data}")
                taxon_code = taxon_code_data[0]["code"]
                item_name = taxon_code_data[0]["name"]
                logger.info(f"first item: {taxon_code_data[0]}")
                if len(taxon_code_data) > 1:
                    logger.info("entering check")
                    for item in taxon_code_data:
                        logger.info(f"checking: {item}")
                        if spellcheck(item["name"].split(" - ")[0], bird,
                                      4) or spellcheck(
                                          item["name"].split(" - ")[1], bird,
                                          4):
                            logger.info("ok")
                            taxon_code = item["code"]
                            item_name = item["name"]
                            break
                        logger.info("fail")
            except IndexError as e:
                raise GenericError(f"No taxon code found for {bird}",
                                   code=111) from e
    logger.info(f"taxon code: {taxon_code}")
    logger.info(f"name: {item_name}")
    return (taxon_code, item_name)
Beispiel #28
0
    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/fXxYyDJ")
            return

        await ctx.send(
            "Use `b!custom view` to view your bird list or `b!custom replace` to replace your bird list."
        )
Beispiel #29
0
async def get_media(ctx, bird: str, media_type: str, filters: Filter):
    """Chooses media from a list of filenames.

    This function chooses a valid image to pass to send_bird().
    Valid images are based on file extension and size. (8mb discord limit)

    Returns a list containing the file path and extension type.

    `ctx` - Discord context object\n
    `bird` (str) - bird to get media of\n
    `media_type` (str) - type of media (images/songs)\n
    `filters` (bot.filters Filter)\n
    """

    # fetch scientific names of birds
    try:
        sciBird = await get_sciname(bird)
    except GenericError:
        sciBird = bird
    media = await get_files(sciBird, media_type, filters)
    logger.info("media: " + str(media))
    prevJ = int(database.hget(f"channel:{ctx.channel.id}", "prevJ"))
    # Randomize start (choose beginning 4/5ths in case it fails checks)
    if media:
        j = (prevJ + 1) % len(media)
        logger.info("prevJ: " + str(prevJ))
        logger.info("j: " + str(j))

        for x in range(0, len(media)):  # check file type and size
            y = (x + j) % len(media)
            path = media[y]
            extension = path.split(".")[-1]
            logger.info("extension: " + str(extension))
            statInfo = os.stat(path)
            logger.info("size: " + str(statInfo.st_size))
            if (extension.lower() in valid_types[media_type].values()
                    and statInfo.st_size <
                    MAX_FILESIZE):  # keep files less than 4mb
                logger.info("found one!")
                break
            raise GenericError(f"No Valid {media_type.title()} Found",
                               code=999)

        database.hset(f"channel:{ctx.channel.id}", "prevJ", str(j))
    else:
        raise GenericError(f"No {media_type.title()} Found", code=100)

    return [path, extension]
Beispiel #30
0
 async def cleanup(self):
     logger.info("running cleanup task")
     await voice_functions.cleanup(self.bot)