def test_bird_other_options(self): self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "bird", "Canada Goose") database.hset(f"channel:{self.ctx.channel.id}", "answered", "0") coroutine = self.cog.bird.callback( # pylint: disable=no-member self.cog, self.ctx, args_str="large egg nest" ) assert asyncio.run(coroutine) is None for i in ( "Active Filters", "tags: eggs", "tags: nest", "large: yes", "quality: good", ): assert i in self.ctx.messages[2].content for i in ("Taxons", "Detected State"): assert i not in self.ctx.messages[2].content for i in ( "Here you go!", "Use `b!bird` again", "b!skip", "Use `b!check guess` to check your answer.", ): assert i in self.ctx.messages[4].content
async def inner(error): # pylint: disable=unused-argument # skip current bird database.hset(f"channel:{ctx.channel.id}", "bird", "") database.hset(f"channel:{ctx.channel.id}", "answered", "1") await ctx.send("*Please try again.*")
async def goatsucker(self, ctx): logger.info("command: goatsucker") await channel_setup(ctx) await user_setup(ctx) answered = int(database.hget(f"channel:{ctx.channel.id}", "gsAnswered")) # check to see if previous bird was answered if answered: # if yes, give a new bird if database.exists(f"session.data:{ctx.author.id}"): logger.info("session active") session_increment(ctx, "total", 1) database.hset(f"channel:{ctx.channel.id}", "gsAnswered", "0") currentBird = random.choice(goatsuckers) database.hset(f"channel:{ctx.channel.id}", "goatsucker", str(currentBird)) logger.info("currentBird: " + str(currentBird)) await send_bird(ctx, currentBird, on_error=error_skip_goat, message=GS_MESSAGE) else: # if no, give the same bird await send_bird(ctx, database.hget(f"channel:{ctx.channel.id}", "goatsucker").decode("utf-8"), on_error=error_skip_goat, message=GS_MESSAGE)
async def stop_race_(self, ctx): first = database.zrevrange(f"race.scores:{ctx.channel.id}", 0, 0, True)[0] if ctx.guild is not None: user = ctx.guild.get_member(int(first[0])) else: user = None if user is None: user = self.bot.get_user(int(first[0])) if user is None: user = "******" else: user = f"{user.name}#{user.discriminator}" else: user = f"{user.name}#{user.discriminator} ({user.mention})" await ctx.send( f"**Congratulations, {user}!**\n" + f"You have won the race by correctly identifying `{int(first[1])}` birds. " + "*Way to go!*") database.hset(f"race.data:{ctx.channel.id}", "stop", round(time.time())) await self._send_stats(ctx, "**Race stopped.**") database.delete(f"race.data:{ctx.channel.id}") database.delete(f"race.scores:{ctx.channel.id}")
async def channel_setup(ctx): """Sets up a new discord channel. `ctx` - Discord context object """ logger.info("checking channel setup") if database.exists(f"channel:{ctx.channel.id}"): logger.info("channel data ok") else: database.hset( f"channel:{ctx.channel.id}", mapping={"bird": "", "answered": 1, "prevB": "", "prevJ": 20}, ) # true = 1, false = 0, index 0 is last arg, prevJ is 20 to define as integer logger.info("channel data added") await ctx.send("Ok, setup! I'm all ready to use!") if database.zscore("score:global", str(ctx.channel.id)) is not None: logger.info("channel score ok") else: database.zadd("score:global", {str(ctx.channel.id): 0}) logger.info("channel score added") if ctx.guild is not None: if ( database.zadd("channels:global", {f"{ctx.guild.id}:{ctx.channel.id}": 0}) != 0 ): logger.info("server lookup ok") else: logger.info("server lookup added")
def error_skip_goat(ctx): """Skips the current goatsucker. Passed to send_bird() as on_error to skip the bird when an error occurs to prevent error loops. """ logger.info("ok") database.hset(f"channel:{ctx.channel.id}", "goatsucker", "") database.hset(f"channel:{ctx.channel.id}", "gsAnswered", "1")
def error_skip_song(ctx): """Skips the current song. Passed to send_birdsong() as on_error to skip the bird when an error occurs to prevent error loops. """ logger.info("ok") database.hset(f"channel:{ctx.channel.id}", "sBird", "") database.hset(f"channel:{ctx.channel.id}", "sAnswered", "1")
def test_check_song_dm_2(self): test_word = "Northern Cardinal" self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "sBird", test_word) coroutine = self.cog.checksong.callback(self.cog, self.ctx, arg=test_word*2) # pylint: disable=no-member assert asyncio.run(coroutine) is None assert self.ctx.messages[1].content == f"Sorry, the bird was actually {test_word.lower()}."
def test_check_song_dm_1(self): test_word = "Northern Cardinal" self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "sBird", test_word) coroutine = self.cog.checksong.callback(self.cog, self.ctx, arg=test_word) # pylint: disable=no-member assert asyncio.run(coroutine) is None assert self.ctx.messages[1].content == "Correct! Good job!"
def test_check_goat_dm_1(self): test_word = "Common Pauraque" self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "goatsucker", test_word) coroutine = self.cog.checkgoat.callback(self.cog, self.ctx, arg=test_word) # pylint: disable=no-member assert asyncio.run(coroutine) is None assert self.ctx.messages[1].content == "Correct! Good job!"
def test_check_goat_dm_2(self): test_word = "Common Pauraque" self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "goatsucker", test_word) coroutine = self.cog.checkgoat.callback(self.cog, self.ctx, arg=test_word*2) # pylint: disable=no-member assert asyncio.run(coroutine) is None assert self.ctx.messages[1].content == f"Sorry, the bird was actually {test_word.lower()}."
def test_skipsong_bird_dm(self): test_word = "Northern Cardinal" self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "sBird", test_word) coroutine = self.cog.skipsong.callback(self.cog, self.ctx) # pylint: disable=no-member assert asyncio.run(coroutine) is None assert self.ctx.messages[ 1].content == f"Ok, skipping {test_word.lower()}"
def test_check_bird_dm_1(self): self.setup(guild=True) test_word = "Canada Goose" database.hset(f"channel:{self.ctx.channel.id}", "bird", test_word) coroutine = self.cog.check.callback( # pylint: disable=no-member self.cog, self.ctx, arg=test_word) assert asyncio.run(coroutine) is None assert self.ctx.messages[2].content == "Correct! Good job!"
def test_skipgoat_bird_dm(self): test_word = "Common Pauraque" self.setup(guild=True) database.hset(f"channel:{self.ctx.channel.id}", "goatsucker", test_word) coroutine = self.cog.skipgoat.callback(self.cog, self.ctx) # pylint: disable=no-member assert asyncio.run(coroutine) is None assert self.ctx.messages[ 1].content == f"Ok, skipping {test_word.lower()}"
def test_hint_bird_dm(self): self.setup(guild=True) test_word = "banana_test" database.hset(f"channel:{self.ctx.channel.id}", "bird", test_word) coroutine = self.cog.hint.callback( # pylint: disable=no-member self.cog, self.ctx) assert asyncio.run(coroutine) is None assert self.ctx.messages[ 2].content == f"The first letter is {test_word[0]}"
def test_skip_bird_dm(self): self.setup(guild=True) test_word = "Canada Goose" database.hset(f"channel:{self.ctx.channel.id}", "bird", test_word) coroutine = self.cog.skip.callback( # pylint: disable=no-member self.cog, self.ctx) assert asyncio.run(coroutine) is None assert self.ctx.messages[ 2].content == f"Ok, skipping {test_word.lower()}"
def session_increment(ctx, item: str, amount: int): """Increments the value of a database hash field by `amount`. `ctx` - Discord context object\n `item` - hash field to increment (see data.py for details, possible values include correct, incorrect, total)\n `amount` (int) - amount to increment by, usually 1 """ logger.info(f"incrementing {item} by {amount}") value = int(database.hget(f"session.data:{ctx.author.id}", item)) value += int(amount) database.hset(f"session.data:{ctx.author.id}", item, str(value))
def test_check_bird_dm_2(self): self.setup(guild=True) test_word = "Canada Goose" database.hset(f"channel:{self.ctx.channel.id}", "bird", test_word) coroutine = self.cog.check.callback( # pylint: disable=no-member self.cog, self.ctx, arg=test_word * 2) assert asyncio.run(coroutine) is None assert (self.ctx.messages[2].content == f"Sorry, the bird was actually {test_word.lower()}.")
async def stop(self, ctx): logger.info("command: stop session") await channel_setup(ctx) await user_setup(ctx) if database.exists(f"session.data:{ctx.author.id}"): database.hset(f"session.data:{ctx.author.id}", "stop", round(time.time())) await self._send_stats(ctx, "**Session stopped.**\n") database.delete(f"session.data:{ctx.author.id}") database.delete(f"session.incorrect:{ctx.author.id}") else: await ctx.send("**There is no session running.** *You can start one with `b!session start`*")
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 skip(self, ctx): logger.info("command: skip") currentBird = database.hget(f"channel:{ctx.channel.id}", "bird").decode("utf-8") database.hset(f"channel:{ctx.channel.id}", "bird", "") database.hset(f"channel:{ctx.channel.id}", "answered", "1") if currentBird != "": # check if there is bird url = get_wiki_url(ctx, currentBird) await ctx.send(f"Ok, skipping {currentBird.lower()}") await ctx.send(url) # sends wiki page streak_increment(ctx, None) # reset streak if database.exists(f"race.data:{ctx.channel.id}"): if Filter.from_int( int( database.hget(f"race.data:{ctx.channel.id}", "filter"))).vc: await voice_functions.stop(ctx, silent=True) media = database.hget(f"race.data:{ctx.channel.id}", "media").decode("utf-8") limit = int( database.hget(f"race.data:{ctx.channel.id}", "limit")) first = database.zrevrange(f"race.scores:{ctx.channel.id}", 0, 0, True)[0] if int(first[1]) >= limit: logger.info("race ending") race = self.bot.get_cog("Race") await race.stop_race_(ctx) else: logger.info(f"auto sending next bird {media}") filter_int, taxon, state = database.hmget( f"race.data:{ctx.channel.id}", ["filter", "taxon", "state"]) birds = self.bot.get_cog("Birds") await birds.send_bird_( ctx, media, Filter.from_int(int(filter_int)), taxon.decode("utf-8"), state.decode("utf-8"), ) else: await ctx.send("You need to ask for a bird first!")
async def skipgoat(self, ctx): logger.info("command: skipgoat") await channel_setup(ctx) await user_setup(ctx) currentBird = str( database.hget(f"channel:{ctx.channel.id}", "goatsucker"))[2:-1] database.hset(f"channel:{ctx.channel.id}", "goatsucker", "") database.hset(f"channel:{ctx.channel.id}", "gsAnswered", "1") if currentBird != "": # check if there is bird url = get_wiki_url(currentBird) await ctx.send(f"Ok, skipping {currentBird.lower()}") await ctx.send(url) # sends wiki page database.zadd("streak:global", {str(ctx.author.id): 0}) else: await ctx.send("You need to ask for a bird first!")
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]
async def inner(error): nonlocal retries # skip current bird database.hset(f"channel:{ctx.channel.id}", "bird", "") database.hset(f"channel:{ctx.channel.id}", "answered", "1") if retries >= 2: # only retry twice await ctx.send("**Too many retries.**\n*Please try again.*") return if isinstance(error, GenericError) and error.code == 100: retries += 1 await ctx.send("**Retrying...**") await self.send_bird_(ctx, media_type, filters, taxon_str, role_str, retries) else: await ctx.send("*Please try again.*")
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 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]
def web_session_setup(session_id): logger.info("setting up session") session_id = str(session_id) if database.exists(f"web.session:{session_id}"): logger.info("session data ok") else: database.hset( f"web.session:{session_id}", mapping={ "bird": "", "answered": 1, # true = 1, false = 0 "prevB": "", "prevJ": 20, "tempScore": 0, # not used = -1 "user_id": 0, # not set = 0 }, ) database.expire(f"web.session:{session_id}", DATABASE_SESSION_EXPIRE) logger.info("session set up")
async def skip(self, ctx): logger.info("command: skip") await channel_setup(ctx) await user_setup(ctx) currentBird = str(database.hget(f"channel:{ctx.channel.id}", "bird"))[2:-1] database.hset(f"channel:{ctx.channel.id}", "bird", "") database.hset(f"channel:{ctx.channel.id}", "answered", "1") if currentBird != "": # check if there is bird url = get_wiki_url(currentBird) await ctx.send(f"Ok, skipping {currentBird.lower()}") await ctx.send( url if not database.exists(f"race.data:{ctx.channel.id}") else f"<{url}>") # sends wiki page database.zadd("streak:global", {str(ctx.author.id): 0}) # end streak if database.exists( f"race.data:{ctx.channel.id}") and database.hget( f"race.data:{ctx.channel.id}", "media").decode("utf-8") == "image": limit = int( database.hget(f"race.data:{ctx.channel.id}", "limit")) first = database.zrevrange(f"race.scores:{ctx.channel.id}", 0, 0, True)[0] if int(first[1]) >= limit: logger.info("race ending") race = self.bot.get_cog("Race") await race.stop_race_(ctx) else: logger.info("auto sending next bird image") addon, bw, taxon = database.hmget( f"race.data:{ctx.channel.id}", ["addon", "bw", "taxon"]) birds = self.bot.get_cog("Birds") await birds.send_bird_(ctx, addon.decode("utf-8"), bw.decode("utf-8"), taxon.decode("utf-8")) else: await ctx.send("You need to ask for a bird first!")
def session_increment(ctx, item: str, amount: int): """Increments the value of a database hash field by `amount`. `ctx` - Discord context object or user id\n `item` - hash field to increment (see data.py for details, possible values include correct, incorrect, total)\n `amount` (int) - amount to increment by, usually 1 """ if isinstance(ctx, (str, int)): user_id = ctx else: user_id = ctx.author.id if database.exists(f"session.data:{user_id}"): logger.info("session active") logger.info(f"incrementing {item} by {amount}") value = int(database.hget(f"session.data:{user_id}", item)) value += int(amount) database.hset(f"session.data:{user_id}", item, str(value)) else: logger.info("session not active")
async def update_web_user(request: Request, user_data: dict): logger.info("updating user data") session_id = get_session_id(request) user_id = str(user_data["id"]) database.hset(f"web.session:{session_id}", "user_id", user_id) database.expire(f"web.session:{session_id}", DATABASE_SESSION_USER_EXPIRE) database.hset( f"web.user:{user_id}", mapping={ "avatar_hash": str(user_data["avatar"]), "avatar_url": f"https://cdn.discordapp.com/avatars/{user_id}/{user_data['avatar']}.png", "username": str(user_data["username"]), "discriminator": str(user_data["discriminator"]), }, ) await user_setup(user_id) tempScore = int(database.hget(f"web.session:{session_id}", "tempScore")) if tempScore not in (0, -1): score_increment(user_id, tempScore) database.zincrby( f"daily.webscore:{str(datetime.datetime.now(datetime.timezone.utc).date())}", 1, user_id, ) database.hset(f"web.session:{session_id}", "tempScore", -1) logger.info("updated user data")