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
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)
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
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
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
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
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]
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
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
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)
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
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]
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
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]
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)
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]
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
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
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
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
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
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)
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()
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)
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)
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!*", )