Beispiel #1
0
    async def is_holiday(ctx):
        """Sends a picture of a turkey on Thanksgiving.

        Can be extended to other holidays as well.
        """
        logger.info("global check: checking holiday")
        now = datetime.now(tz=timezone(-timedelta(hours=4))).date()
        us = holidays.US()
        if now in us:
            if us.get(now) == "Thanksgiving":
                await send_bird(
                    ctx,
                    "Wild Turkey",
                    "images",
                    Filter(),
                    message=
                    "**It's Thanksgiving!**\nGo celebrate with your family.",
                )
                raise GenericError(code=666)
            if us.get(now) == "Independence Day":
                await send_bird(
                    ctx,
                    "Bald Eagle",
                    "images",
                    Filter(),
                    message=
                    "**It's Independence Day!**\nEnjoy this birb responsibly.",
                )
                raise GenericError(code=666)
        elif now == date(now.year, 4, 1):
            return await drone_attack(ctx)
        return True
Beispiel #2
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 #3
0
async def _download_helper(path, url, session, sem):
    """Downloads media from the given URL.

    Returns the file path to the downloaded item.

    `path` (str) - path with filename of location to download, no extension\n
    `url` (str) - url to the item to be downloaded\n
    `session` (aiohttp ClientSession)
    """
    async with sem:
        try:
            async with session.get(url) as response:
                media_size = response.headers.get("content-length")
                if (response.status != 200 or media_size is None
                        or int(media_size) > MAX_FILESIZE):
                    logger.info(
                        f"FAIL: status: {response.status}; size: {media_size}")
                    logger.info(url)
                    return None

                # from https://stackoverflow.com/questions/29674905/convert-content-type-header-into-file-extension
                content_type = (
                    response.headers["content-type"].partition(";")[0].strip())
                if content_type.partition("/")[0] == "image":
                    try:
                        ext = valid_types["images"][content_type]
                    except KeyError:
                        raise GenericError(
                            f"No valid extensions found. Content-Type: {content_type}"
                        )

                elif content_type.partition("/")[0] == "audio":
                    try:
                        ext = valid_types["songs"][content_type]
                    except KeyError:
                        raise GenericError(
                            f"No valid extensions found. Content-Type: {content_type}"
                        )
                else:
                    raise GenericError("Invalid content-type.")

                filename = f"{path}.{ext}"
                # from https://stackoverflow.com/questions/38358521/alternative-of-urllib-urlretrieve-in-python-3-5
                with open(filename, "wb") as out_file:
                    block_size = 1024 * 8
                    while True:
                        block = await response.content.read(block_size)  # pylint: disable=no-member
                        if not block:
                            break
                        out_file.write(block)
                return filename

        except aiohttp.ClientError as e:
            logger.info(f"Client Error with url {url} and path {path}")
            capture_exception(e)
            raise
Beispiel #4
0
async def _download_helper(path, url, session):
    """Downloads media from the given URL.

    Returns the file path to the downloaded item.

    `path` (str) - path with filename of location to download, no extension\n
    `url` (str) - url to the item to be downloaded\n
    `session` (aiohttp ClientSession)
    """
    try:
        async with session.get(url) as response:
            # from https://stackoverflow.com/questions/29674905/convert-content-type-header-into-file-extension
            content_type = response.headers['content-type'].partition(
                ';')[0].strip()
            if content_type.partition("/")[0] == "image":
                try:
                    ext = "." + \
                                                      (set(ext[1:] for ext in guess_all_extensions(
                        content_type)).intersection(valid_image_extensions)).pop()
                except KeyError:
                    raise GenericError(
                        f"No valid extensions found. Extensions: {guess_all_extensions(content_type)}"
                    )

            elif content_type.partition("/")[0] == "audio":
                try:
                    ext = "." + (set(
                        ext[1:] for ext in guess_all_extensions(content_type)).
                                 intersection(valid_audio_extensions)).pop()
                except KeyError:
                    raise GenericError(
                        f"No valid extensions found. Extensions: {guess_all_extensions(content_type)}"
                    )

            else:
                ext = guess_extension(content_type)
                if ext is None:
                    raise GenericError(f"No extensions found.")

            filename = f"{path}{ext}"
            # from https://stackoverflow.com/questions/38358521/alternative-of-urllib-urlretrieve-in-python-3-5
            with open(filename, 'wb') as out_file:
                block_size = 1024 * 8
                while True:
                    block = await response.content.read(block_size)  # pylint: disable=no-member
                    if not block:
                        break
                    out_file.write(block)
            return filename
    except aiohttp.ClientError as e:
        logger.info(f"Client Error with url {url} and path {path}")
        capture_exception(e)
        raise
Beispiel #5
0
    def moderation_check(ctx):
        """Checks different moderation checks.

        Disallows:
        - Users that are banned from the bot,
        - Channels that are ignored
        """
        logger.info("global check: checking banned")
        if database.zscore("ignore:global", str(ctx.channel.id)) is not None:
            raise GenericError(code=192)
        if database.zscore("banned:global", str(ctx.author.id)) is not None:
            raise GenericError(code=842)
        return True
Beispiel #6
0
async def _get_urls(
    session: aiohttp.ClientSession,
    bird: str,
    media_type: str,
    filters: Filter,
    retries: int = 0,
):
    """Returns a list of urls to Macaulay Library media.

    The amount of urls returned is specified in `COUNT`.
    Media URLs are fetched using Macaulay Library's internal JSON API,
    with `CATALOG_URL`. Raises a `GenericError` if fails.\n
    Some urls may return an error code of 476 (because it is still being processed),
    if so, ignore that url.

    `session` (aiohttp ClientSession)\n
    `bird` (str) - can be either common name or scientific name\n
    `media_type` (str) - either `p` for pictures, `a` for audio, or `v` for video\n
    `filters` (bot.filters Filter)
    """
    logger.info(f"getting file urls for {bird}")
    taxon_code = (await get_taxon(bird, session))[0]
    catalog_url = filters.url(taxon_code, media_type)
    async with session.get(catalog_url) as catalog_response:
        if catalog_response.status != 200:
            if retries >= 3:
                logger.info("Retried more than 3 times. Aborting...")
                raise GenericError(
                    f"An http error code of {catalog_response.status} occurred "
                    +
                    f"while fetching {catalog_url} for a {'image'if media_type=='p' else 'song'} 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)
            urls = await _get_urls(session, bird, media_type, filters, retries)
            return urls

        catalog_data = await catalog_response.json()
        content = catalog_data["results"]["content"]
        urls = ([data["mediaUrl"]
                 for data in content] if filters.large or media_type == "a"
                else [data["previewUrl"] for data in content])
        if not urls:
            raise GenericError("No urls found.", code=100)
        return urls
Beispiel #7
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 #8
0
async def get_sciname(bird: str, session=None, retries=0) -> str:
    """Returns the scientific name of a bird.

    Scientific names are found using the eBird API from the Cornell Lab of Ornithology,
    using `SCINAME_URL` to fetch data.
    Raises a `GenericError` if a scientific name is not found or an HTTP error occurs.

    `bird` (str) - common/scientific name of the bird you want to look up\n
    `session` (optional) - an aiohttp client session
    """
    logger.info(f"getting sciname for {bird}")
    async with contextlib.AsyncExitStack() as stack:
        if session is None:
            session = await stack.enter_async_context(aiohttp.ClientSession())
        try:
            code = (await get_taxon(bird, session))[0]
        except GenericError as e:
            if e.code == 111:
                code = bird
            else:
                raise

        sciname_url = SCINAME_URL.format(urllib.parse.quote(code))
        async with session.get(sciname_url) as sciname_response:
            if sciname_response.status != 200:
                if retries >= 3:
                    logger.info("Retried more than 3 times. Aborting...")
                    raise GenericError(
                        f"An http error code of {sciname_response.status} occurred"
                        + f" while fetching {sciname_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)
                sciname = await get_sciname(bird, session, retries)
                return sciname

            sciname_data = await sciname_response.json()
            try:
                sciname = sciname_data[0]["sciName"]
            except IndexError as e:
                raise GenericError(f"No sciname found for {code}",
                                   code=111) from e
    logger.info(f"sciname: {sciname}")
    return sciname
Beispiel #9
0
    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
Beispiel #10
0
 def user_banned(ctx):
     """Disallows users that are banned from the bot."""
     logger.info("global check: checking banned")
     if database.zscore("banned:global", str(ctx.author.id)) is None:
         return True
     else:
         raise GenericError(code=842)
Beispiel #11
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 #12
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 #13
0
def build_id_list(user_id=None,
                  taxon=None,
                  state=None,
                  media="images") -> list:
    """Generates an ID list based on given arguments

    - `user_id`: User ID of custom list
    - `taxon`: taxon string/list
    - `state`: state string/list
    - `media`: images/songs
    """
    logger.info("building id list")
    if isinstance(taxon, str):
        taxon = taxon.split(" ")
    if isinstance(state, str):
        state = state.split(" ")

    state_roles = state if state else []
    if media in ("songs", "song", "s", "a"):
        state_list = "songBirds"
        default = songBirds
    elif media in ("images", "image", "i", "p"):
        state_list = "birdList"
        default = birdList
    else:
        raise GenericError("Invalid media type", code=990)

    custom_list = []
    if (user_id and "CUSTOM" in state_roles
            and database.exists(f"custom.list:{user_id}")
            and not database.exists(f"custom.confirm:{user_id}")):
        custom_list = [
            bird.decode("utf-8")
            for bird in database.smembers(f"custom.list:{user_id}")
        ]

    birds = []
    if taxon:
        birds_in_taxon = set(
            itertools.chain.from_iterable(taxons.get(o, []) for o in taxon))
        if state_roles:
            birds_in_state = set(
                itertools.chain(
                    *(states[state][state_list] for state in state_roles),
                    custom_list))
            birds = list(birds_in_taxon.intersection(birds_in_state))
        else:
            birds = list(birds_in_taxon.intersection(set(default)))
    elif state_roles:
        birds = list(
            set(
                itertools.chain(
                    *(states[state][state_list] for state in state_roles),
                    custom_list)))
    else:
        birds = default
    logger.info(f"number of birds: {len(birds)}")
    return birds
Beispiel #14
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 #15
0
async def send_leaderboard(ctx, title, page, database_key=None, data=None):
    logger.info("building/sending leaderboard")

    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

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

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

    if page > entry_count:
        page = entry_count - (entry_count % 10)

    items_per_page = 10
    leaderboard_list = (
        map(
            lambda x: (x[0].decode("utf-8"), x[1]),
            database.zrevrangebyscore(
                database_key, "+inf", "-inf", page, items_per_page, True
            ),
        )
        if database_key is not None
        else data.iloc[page : page + items_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):
        leaderboard += f"{i+1+page}. **{stats[0]}** - {int(stats[1])}\n"
    embed.add_field(name=title, value=leaderboard, inline=False)

    await ctx.send(embed=embed)
Beispiel #16
0
async def get_media(
    request: Request, bird: str, media_type: str, filters: Filter
):  # images or songs
    if bird not in birdList + screech_owls:
        raise GenericError("Invalid Bird", code=990)

    if media_type not in ("images", "songs"):
        logger.error(f"invalid media type {media_type}")
        raise HTTPException(status_code=422, detail="Invalid media type")

    # fetch scientific names of birds
    try:
        sciBird = await get_sciname(bird)
    except GenericError:
        sciBird = bird

    session_id = get_session_id(request)
    database_key = f"web.session:{session_id}"

    media = await get_files(sciBird, media_type, filters)
    logger.info(f"fetched {media_type}: {media}")
    prevJ = int(database.hget(database_key, "prevJ").decode("utf-8"))
    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)
            media_path = media[y]
            extension = media_path.split(".")[-1]
            logger.info("extension: " + str(extension))
            if extension.lower() in valid_types[media_type].values():
                logger.info("found one!")
                break
            if y == prevJ:
                raise GenericError(f"No Valid {media_type.title()} Found", code=999)

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

    return media_path, extension, content_type_lookup[extension]
Beispiel #17
0
    async def prechecks(ctx):
        await ctx.trigger_typing()

        logger.info("global check: checking permissions")
        await commands.bot_has_permissions(send_messages=True,
                                           embed_links=True,
                                           attach_files=True).predicate(ctx)

        logger.info("global check: checking banned")
        if database.zscore("ignore:global", str(ctx.channel.id)) is not None:
            raise GenericError(code=192)
        if database.zscore("banned:global", str(ctx.author.id)) is not None:
            raise GenericError(code=842)

        logger.info("global check: logging command frequency")
        database.zincrby("frequency.command:global", 1, str(ctx.command))

        logger.info("global check: database setup")
        await channel_setup(ctx)
        await user_setup(ctx)

        return True
Beispiel #18
0
async def get_media(bird, media_type, filters):  # images or songs
    if bird not in birdList + screech_owls:
        raise GenericError("Invalid Bird", code=990)

    # fetch scientific names of birds
    try:
        sciBird = await get_sciname(bird)
    except GenericError:
        sciBird = bird

    session_id = get_session_id()
    database_key = f"web.session:{session_id}"

    media = await get_files(sciBird, media_type, filters)
    logger.info(f"fetched {media_type}: {media}")
    prevJ = int(database.hget(database_key, "prevJ").decode("utf-8"))
    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)
            media_path = media[y]
            extension = media_path.split('.')[-1]
            logger.info("extension: " + str(extension))
            if (media_type == "images" and extension.lower() in valid_types["images"].values()) or \
                    (media_type == "songs" and extension.lower() in valid_types["songs"].values()):
                logger.info("found one!")
                break
            if y == prevJ:
                raise GenericError(f"No Valid {media_type.title()} Found",
                                   code=999)

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

    return media_path, extension
Beispiel #19
0
async def _get_urls(session,
                    bird,
                    media_type,
                    sex="",
                    age="",
                    sound_type="",
                    retries=0):
    """Returns a list of urls to Macaulay Library media.

    The amount of urls returned is specified in `COUNT`. 
    Media URLs are fetched using Macaulay Library's internal JSON API, 
    with `CATALOG_URL`. Raises a `GenericError` if fails.\n
    Some urls may return an error code of 476 (because it is still being processed), 
    if so, ignore that url.

    `session` (aiohttp ClientSession)\n
    `bird` (str) - can be either common name or scientific name\n
    `media_type` (str) - either `p` for pictures, `a` for audio, or `v` for video\n
    `sex` (str) - `m`, `f`, or blank\n
    `age` (str) - `a` for adult, `j` for juvenile, `i` for immature (may not have many), or blank\n
    `sound_type` (str) - `s` for song, `c` for call, or blank\n
    """
    logger.info(f"getting file urls for {bird}")
    taxon_code = await get_taxon(bird, session)
    catalog_url = CATALOG_URL.format(taxon_code, COUNT, media_type, sex, age,
                                     sound_type)
    async with session.get(catalog_url) as catalog_response:
        if catalog_response.status != 200:
            if retries >= 3:
                logger.info("Retried more than 3 times. Aborting...")
                raise GenericError(
                    f"An http error code of {catalog_response.status} occured "
                    +
                    f"while fetching {catalog_url} for a {'image'if media_type=='p' else 'song'} for {bird}",
                    code=201)
            else:
                retries += 1
                logger.info(f"An HTTP error occurred; Retries: {retries}")
                urls = await _get_urls(session, bird, media_type, sex, age,
                                       sound_type, retries)
                return urls
        catalog_data = await catalog_response.json()
        content = catalog_data["results"]["content"]
        urls = [data["mediaUrl"] for data in content]
        return urls
Beispiel #20
0
 async def is_holiday(ctx):
     """Sends a picture of a turkey on Thanksgiving.
     
     Can be extended to other holidays as well.
     """
     logger.info("global check: checking holiday")
     now = datetime.now(tz=timezone(-timedelta(hours=4)))
     now = date(now.year, now.month, now.day)
     us = holidays.US()
     if now in us:
         if us.get(now) == "Thanksgiving":
             await send_bird(ctx, "Wild Turkey")
             await ctx.send(
                 "**It's Thanksgiving!**\nGo celebrate with your family.")
             raise GenericError(code=666)
     elif now == date(now.year, 4, 1):
         return await drone_attack(ctx)
     return True
Beispiel #21
0
async def get_files(sciBird: str,
                    media_type: str,
                    filters: Filter,
                    retries: int = 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
    `filters` (bot.filters Filter)\n
    """
    logger.info(f"get_files retries: {retries}")
    directory = f"bot_files/cache/{media_type}/{sciBird}{filters.to_int()}/"
    # track counts for more accurate eviction
    database.zincrby("frequency.media:global", 1,
                     f"{media_type}/{sciBird}{filters.to_int()}")
    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, filters,
                                         directory)
        if not filenames:
            if retries < 3:
                retries += 1
                return await get_files(sciBird, media_type, filters, retries)
            logger.info("More than 3 retries")

        return filenames
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
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.**"
            )
        else:
            await ctx.send(
                f"**An error has occurred while fetching {media_type}.**\n**Reason:** {e}"
            )
            logger.exception(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":
        # remove spoilers in tag metadata
        audioFile = eyed3.load(filename)
        if audioFile is not None and audioFile.tag is not None:
            audioFile.tag.remove(filename)

    # change filename to avoid spoilers
    file_obj = discord.File(filename, filename=f"bird.{extension}")
    if message is not None:
        await ctx.send(message)
    await ctx.send(file=file_obj)
    await delete.delete()
Beispiel #24
0
    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)
Beispiel #25
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 #26
0
    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!*",
            )