async def botinfo(self, ctx): logger.info("command: botinfo") await channel_setup(ctx) await user_setup(ctx) embed = discord.Embed(type="rich", colour=discord.Color.blurple()) embed.set_author(name="Bird ID - An Ornithology Bot") embed.add_field( name="Bot Info", value="This bot was created by EraserBird and person_v1.32 " + "for helping people practice bird identification for Science Olympiad.\n" + "**By adding this bot to a server, you are agreeing to our `Privacy Policy` and `Terms of Service`**.\n" + "<https://github.com/tctree333/Bird-ID/blob/master/PRIVACY.md>, " + "<https://github.com/tctree333/Bird-ID/blob/master/TERMS.md>", inline=False) embed.add_field( name="Support", value="If you are experiencing any issues, have feature requests, " + "or want to get updates on bot status, join our support server below.", inline=False) embed.add_field( name="Stats", value= f"This bot can see {len(self.bot.users)} users and is in {len(self.bot.guilds)} servers. " + f"There are {int(database.zcard('users:global'))} active users in {int(database.zcard('score:global'))} channels. " + f"The WebSocket latency is {str(round((self.bot.latency*1000)))} ms.", inline=False) await ctx.send(embed=embed) await ctx.send("https://discord.gg/fXxYyDJ")
async def get_sciname(bird, session=None): 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) 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: raise GenericError( f"An http error code of {sciname_response.status} occured" + f" while fetching {sciname_url} for {code}", code=201 ) sciname_data = await sciname_response.json() try: sciname = sciname_data[0]["sciName"] except IndexError: raise GenericError(f"No sciname found for {code}", code=111) logger.info(f"sciname: {sciname}") return sciname
async def botinfo(self, ctx): logger.info("command: botinfo") await channel_setup(ctx) await user_setup(ctx) embed = discord.Embed(type="rich", colour=discord.Color.blurple()) embed.set_author(name=bot_name) embed.add_field( name="Bot Info", value="This bot was created by EraserBird, person_v1.32 and hmmm" + "for helping people practice fossil identification for Science Olympiad.", inline=False ) embed.add_field( name="Support", value="If you are experiencing any issues, have feature requests, " + "or want to get updates on bot status, join our support server below.", inline=False ) embed.add_field( name="Stats", value=f"This bot can see {len(self.bot.users)} users and is in {len(self.bot.guilds)} servers. " + f"There are {int(database.zcard('users:global'))} active users in {int(database.zcard('score:global'))} channels. " + f"The WebSocket latency is {str(round((self.bot.latency*1000)))} ms.", inline=False ) await ctx.send(embed=embed) await ctx.send("https://discord.gg/husFeGG")
async def get_image(ctx, bird, addOn=None): # 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:{str(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.debug("prevJ: " + str(prevJ)) logger.debug("j: " + str(j)) for x in range(j, len(images)): # check file type and size image_link = images[x] extension = image_link.split('.')[-1] logger.debug("extension: " + str(extension)) statInfo = os.stat(image_link) logger.debug("size: " + str(statInfo.st_size)) if extension.lower() in valid_image_extensions and statInfo.st_size < 8000000: # 8mb discord limit logger.info("found one!") break elif x == len(images) - 1: j = (j + 1) % (len(images)) raise GenericError("No Valid Images Found", code=999) database.hset(f"channel:{str(ctx.channel.id)}", "prevJ", str(j)) else: raise GenericError("No Images Found", code=100) return [image_link, extension]
async def download_media(bird, media_type, addOn="", directory=None, session=None): if directory is None: directory = f"cache/{media_type}/{bird}{addOn}/" if addOn == "female": sex = "f" else: sex = "" if addOn == "juvenile": age = "j" else: age = "" if media_type == "images": media = "p" elif media_type == "songs": media = "a" async with contextlib.AsyncExitStack() as stack: if session is None: session = await stack.enter_async_context(aiohttp.ClientSession()) urls = await _get_urls(session, bird, media, sex, age) if not os.path.exists(directory): os.makedirs(directory) paths = [f"{directory}{i}" for i in range(len(urls))] filenames = await asyncio.gather(*(_download_helper(path, url, session) for path, url in zip(paths, urls))) logger.info(f"downloaded {media_type} for {bird}") logger.info(f"filenames: {filenames}") return filenames
async def get_song(ctx, bird): # 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:{str(ctx.channel.id)}", "prevK"))[2:-1]) # Randomize start (choose beginning 4/5ths in case it fails checks) if songs: k = (prevK + 1) % len(songs) logger.debug("prevK: " + str(prevK)) logger.debug("k: " + str(k)) for x in range(k, len(songs)): # check file type and size song_link = songs[x] extension = song_link.split('.')[-1] logger.debug("extension: " + str(extension)) statInfo = os.stat(song_link) logger.debug("size: " + str(statInfo.st_size)) if extension.lower() in valid_audio_extensions and statInfo.st_size < 8000000: # 8mb discord limit logger.info("found one!") break elif x == len(songs) - 1: k = (k + 1) % (len(songs)) raise GenericError("No Valid Songs Found", code=999) database.hset(f"channel:{str(ctx.channel.id)}", "prevK", str(k)) else: raise GenericError("No Songs Found", code=100) return [song_link, extension]
async def precache(): logger.info("Starting caching") with ProcessPoolExecutor(max_workers=4) as executor: async with aiohttp.ClientSession() as session: await asyncio.gather(*(fetch_images(fossil, session, executor) for fossil in fossils_list)) logger.info("Finished caching")
async def channel_setup(ctx): logger.info("checking channel setup") if database.exists(f"channel:{str(ctx.channel.id)}"): logger.info("channel data ok") else: database.hmset( f"channel:{str(ctx.channel.id)}", { "bird": "", "answered": 1, "sBird": "", "sAnswered": 1, "goatsucker": "", "gsAnswered": 1, "prevJ": 20, "prevB": "", "prevS": "", "prevK": 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")
async def userscore(self, ctx, *, user: typing.Optional[typing.Union[discord.Member, str]] = None): logger.info("command: userscore") await channel_setup(ctx) await user_setup(ctx) if user is not None: if isinstance(user, str): await ctx.send("Not a user!") return usera = user.id logger.info(usera) if database.zscore("users:global", str(usera)) is not None: times = str(int(database.zscore("users:global", str(usera)))) user = f"<@{str(usera)}>" else: await ctx.send("This user does not exist on our records!") return else: if database.zscore("users:global", str(ctx.author.id)) is not None: user = f"<@{str(ctx.author.id)}>" times = str(int(database.zscore("users:global", str(ctx.author.id)))) else: await ctx.send("You haven't used this bot yet! (except for this)") return embed = discord.Embed(type="rich", colour=discord.Color.blurple()) embed.set_author(name="Bird ID - An Ornithology Bot") embed.add_field(name="User Score:", value=f"{user} has answered correctly {times} times.") await ctx.send(embed=embed)
def _black_and_white(input_image_path): logger.info("black and white") with Image.open(input_image_path) as color_image: bw = color_image.convert('L') final_buffer = BytesIO() bw.save(final_buffer, "png") final_buffer.seek(0) return final_buffer
async def send_as_bot(self, ctx, *, args): logger.info("command: send") logger.info(f"args: {args}") channel_id = int(args.split(' ')[0]) message = args.strip(str(channel_id)) channel = self.bot.get_channel(channel_id) await channel.send(message) await ctx.send("Ok, sent!")
async def view(self, ctx): logger.info("command: view session") await channel_setup(ctx) await user_setup(ctx) if database.exists(f"session.data:{str(ctx.author.id)}"): await self._send_stats(ctx) else: await ctx.send("**There is no session running.** *You can start one with `f!session start`*")
async def stop(self, ctx): logger.info("command: stop race") await channel_setup(ctx) await user_setup(ctx) if database.exists(f"race.data:{str(ctx.channel.id)}"): await self.stop_race_(ctx) else: await ctx.send("**There is no race in session.** *You can start one with `b!race start`*")
async def view(self, ctx): logger.info("command: view race") await channel_setup(ctx) await user_setup(ctx) if database.exists(f"race.data:{str(ctx.channel.id)}"): await self._send_stats(ctx, f"**Race In Progress**") else: await ctx.send("**There is no race in session.** *You can start one with `b!race start`*")
async def score(self, ctx): logger.info("command: score") await channel_setup(ctx) await user_setup(ctx) totalCorrect = int(database.zscore("score:global", str(ctx.channel.id))) await ctx.send( f"Wow, looks like a total of {str(totalCorrect)} birds have been answered correctly in this channel! " + "Good job everyone!" )
async def _send_stats(self, ctx, preamble): placings = 5 database_key = f"race.scores:{str(ctx.channel.id)}" if database.zcard(database_key) is 0: logger.info(f"no users in {database_key}") await ctx.send("There are no users in the database.") return if placings > database.zcard(database_key): placings = database.zcard(database_key) leaderboard_list = database.zrevrangebyscore( database_key, "+inf", "-inf", 0, placings, True) embed = discord.Embed( type="rich", colour=discord.Color.blurple(), title=preamble) embed.set_author(name="Bird ID - An Ornithology Bot") leaderboard = "" for i, stats in enumerate(leaderboard_list): if ctx.guild is not None: user = 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}** ({str(user.mention)})" leaderboard += f"{str(i+1)}. {user} - {str(int(stats[1]))}\n" start = int(database.hget(f"race.data:{str(ctx.channel.id)}", "start")) elapsed = str(datetime.timedelta(seconds=round(time.time()) - start)) embed.add_field(name="Options", value=await self._get_options(ctx), inline=False) embed.add_field( name="Stats", value=f"**Race Duration:** `{elapsed}`", inline=False) embed.add_field(name="Leaderboard", value=leaderboard, inline=False) if database.zscore(database_key, str(ctx.author.id)) is not None: placement = int(database.zrevrank( database_key, str(ctx.author.id))) + 1 embed.add_field( name="You:", value=f"You are #{str(placement)}.", inline=False) else: embed.add_field( name="You:", value="You haven't answered any correctly.") await ctx.send(embed=embed)
async def hint(self, ctx): logger.info("command: hint") await channel_setup(ctx) await user_setup(ctx) current_fossil = str( database.hget(f"channel:{str(ctx.channel.id)}", "fossil"))[2:-1] if current_fossil != "": await ctx.send(f"The first letter is {current_fossil[0]}") else: await ctx.send("You need to ask for a fossil first!")
async def stop(self, ctx): logger.info("command: stop session") await channel_setup(ctx) await user_setup(ctx) if database.exists(f"session.data:{str(ctx.author.id)}"): database.hset(f"session.data:{str(ctx.author.id)}", "stop", round(time.time())) await self._send_stats(ctx) database.delete(f"session.data:{str(ctx.author.id)}") else: await ctx.send("**There is no session running.** *You can start one with `f!session start`*")
async def wiki(self, ctx, *, arg): logger.info("command: wiki") await channel_setup(ctx) await user_setup(ctx) try: page = wikipedia.page(arg) await ctx.send(page.url) except wikipedia.exceptions.DisambiguationError: await ctx.send("Sorry, that page was not found. Try being more specific.") except wikipedia.exceptions.PageError: await ctx.send("Sorry, that page was not found.")
def incorrect_increment(ctx, bird, amount): logger.info(f"incrementing incorrect {bird} by {amount}") database.zincrby("incorrect:global", amount, str(bird)) database.zincrby(f"incorrect.user:{ctx.author.id}", amount, str(bird)) if ctx.guild is not None: logger.info("no dm") database.zincrby(f"incorrect.server:{ctx.guild.id}", amount, str(bird)) else: logger.info("dm context") if database.exists(f"session.data:{str(ctx.author.id)}"): logger.info("session in session") database.zincrby(f"session.incorrect:{ctx.author.id}", amount, str(bird)) else: logger.info("no session")
async def send_song_(self, ctx): songAnswered = int( database.hget(f"channel:{str(ctx.channel.id)}", "sAnswered")) # check to see if previous bird was answered if songAnswered: # if yes, give a new bird roles = check_state_role(ctx) if database.exists(f"session.data:{ctx.author.id}"): logger.info("session active") session_increment(ctx, "total", 1) roles = str( database.hget(f"session.data:{ctx.author.id}", "state"))[2:-1].split(" ") if roles[0] == "": roles = [] if len(roles) is 0: logger.info("no session lists") roles = check_state_role(ctx) logger.info(f"roles: {roles}") if roles: birds = list( itertools.chain.from_iterable(states[state]["songBirds"] for state in roles)) else: birds = songBirds logger.info(f"number of birds: {len(birds)}") currentSongBird = random.choice(birds) prevS = str( database.hget(f"channel:{str(ctx.channel.id)}", "prevS"))[2:-1] while currentSongBird == prevS: currentSongBird = random.choice(birds) database.hset(f"channel:{str(ctx.channel.id)}", "prevS", str(currentSongBird)) database.hset(f"channel:{str(ctx.channel.id)}", "sBird", str(currentSongBird)) logger.info("currentSongBird: " + str(currentSongBird)) await send_birdsong(ctx, currentSongBird, on_error=error_skip_song, message=SONG_MESSAGE) database.hset(f"channel:{str(ctx.channel.id)}", "sAnswered", "0") else: await send_birdsong( ctx, str(database.hget(f"channel:{str(ctx.channel.id)}", "sBird"))[2:-1], on_error=error_skip_song, message=SONG_MESSAGE)
async def remove(self, ctx, *, args): logger.info("command: remove") await channel_setup(ctx) await user_setup(ctx) raw_roles = ctx.author.roles user_role_names = [role.name.lower() for role in raw_roles] user_role_ids = [role.id for role in raw_roles] args = args.upper().split(" ") for arg in args: if arg not in list(states.keys()): logger.info("invalid state") await ctx.send( f"**Sorry, `{arg}` is not a valid state.**\n*Valid States:* `{', '.join(map(str, list(states.keys())))}`" ) elif states[arg]["aliases"][0].lower() not in user_role_names[1:]: logger.info("doesn't have role") await ctx.send( f"**You don't have the `{arg}` state role!**\n*Your Roles:* `{', '.join(map(str, user_role_names[1:]))}`" ) else: logger.info("deleting role") index = user_role_names.index( states[arg]["aliases"][0].lower()) role = ctx.guild.get_role(user_role_ids[index]) await ctx.author.remove_roles( role, reason="Delete state role for bird list") await ctx.send(f"**Ok, role {role.name} deleted!**")
async def send_bird(ctx, bird, on_error=None, message=None, addOn="", bw=False): if bird == "": logger.error("error - bird is blank") await ctx.send("**There was an error fetching birds.**\n*Please try again.*") if on_error is not None: on_error(ctx) return # add special condition for screech owls # since screech owl is a genus and SciOly # doesn't specify a species if bird == "Screech Owl": logger.info("choosing specific Screech Owl") bird = random.choice(screech_owls) delete = await ctx.send("**Fetching.** This may take a while.") # trigger "typing" discord message await ctx.trigger_typing() try: response = await get_image(ctx, bird, addOn) except GenericError as e: await delete.delete() await ctx.send(f"**An error has occurred while fetching images.**\n*Please try again.*\n**Reason:** {str(e)}") logger.exception(e) if on_error is not None: on_error(ctx) return filename = str(response[0]) extension = str(response[1]) statInfo = os.stat(filename) if statInfo.st_size > 8000000: # another filesize check await delete.delete() await ctx.send("**Oops! File too large :(**\n*Please try again.*") else: if bw: loop = asyncio.get_running_loop() fn = partial(_black_and_white, filename) file_stream = await loop.run_in_executor(None, fn) else: file_stream = filename if message is not None: await ctx.send(message) # change filename to avoid spoilers file_obj = discord.File(file_stream, filename=f"bird.{extension}") await ctx.send(file=file_obj) await delete.delete()
async def list_of_birds(self, ctx, state: str = "blank"): logger.info("command: list") await channel_setup(ctx) await user_setup(ctx) state = state.upper() if state not in list(states.keys()): logger.info("invalid state") await ctx.send( f"**Sorry, `{state}` is not a valid state.**\n*Valid States:* `{', '.join(map(str, list(states.keys())))}`" ) return birdLists = [] temp = "" for bird in states[state]['birdList']: temp += f"{str(bird)}\n" if len(temp) > 1950: birdLists.append(temp) temp = "" birdLists.append(temp) songLists = [] temp = "" for bird in states[state]['songBirds']: temp += f"{str(bird)}\n" if len(temp) > 1950: songLists.append(temp) temp = "" songLists.append(temp) if ctx.author.dm_channel is None: await ctx.author.create_dm() await ctx.author.dm_channel.send(f"**The {state} bird list:**") for birds in birdLists: await ctx.author.dm_channel.send(f"```{birds}```") await ctx.author.dm_channel.send(f"**The {state} bird songs:**") for birds in songLists: await ctx.author.dm_channel.send(f"```{birds}```") await ctx.send( f"The `{state}` bird list has **{str(len(states[state]['birdList']))}** birds.\n" + f"The `{state}` bird list has **{str(len(states[state]['songBirds']))}** songs.\n" + "*A full list of birds has been sent to you via DMs.*")
async def skipgoat(self, ctx): logger.info("command: skipgoat") await channel_setup(ctx) await user_setup(ctx) database.zadd("streak:global", {str(ctx.author.id): 0}) currentBird = str(database.hget(f"channel:{str(ctx.channel.id)}", "goatsucker"))[2:-1] database.hset(f"channel:{str(ctx.channel.id)}", "goatsucker", "") database.hset(f"channel:{str(ctx.channel.id)}", "gsAnswered", "1") if currentBird != "": # check if there is bird birdPage = wikipedia.page(f"{currentBird} (bird)") await ctx.send(f"Ok, skipping {currentBird.lower()}\n{birdPage.url}") # sends wiki page else: await ctx.send("You need to ask for a bird first!")
async def state(self, ctx, *, args): logger.info("command: state set") await channel_setup(ctx) await user_setup(ctx) roles = [role.name.lower() for role in ctx.author.roles] args = args.upper().split(" ") for arg in args: if arg not in list(states.keys()): logger.info("invalid state") await ctx.send( f"**Sorry, `{arg}` is not a valid state.**\n*Valid States:* `{', '.join(map(str, list(states.keys())))}`" ) elif len(set(roles).intersection(set( states[arg]["aliases"]))) is 0: # gets similarities # need to add roles (does not have role) logger.info("add roles") raw_roles = ctx.guild.roles guild_role_names = [role.name.lower() for role in raw_roles] guild_role_ids = [role.id for role in raw_roles] if states[arg]["aliases"][0].lower() in guild_role_names: # guild has role index = guild_role_names.index( states[arg]["aliases"][0].lower()) role = ctx.guild.get_role(guild_role_ids[index]) else: # create role logger.info("creating role") role = await ctx.guild.create_role( name=string.capwords(states[arg]["aliases"][0]), permissions=discord.Permissions.none(), hoist=False, mentionable=False, reason="Create state role for bird list") await ctx.author.add_roles( role, reason="Set state role for bird list") await ctx.send(f"**Ok, added the {role.name} role!**") else: # have roles already (there were similarities) logger.info("already has role") await ctx.send(f"**You already have the `{arg}` role!**")
async def refresh_cache(): logger.info("clear cache") try: shutil.rmtree(r'cache/images/', ignore_errors=True) logger.info("Cleared image cache.") except FileNotFoundError: logger.info("Already cleared image cache.") try: shutil.rmtree(r'cache/songs/', ignore_errors=True) logger.info("Cleared songs cache.") except FileNotFoundError: logger.info("Already cleared songs cache.") event_loop = asyncio.get_event_loop() with concurrent.futures.ThreadPoolExecutor(1) as executor: await event_loop.run_in_executor(executor, start_precache)
async def info(self, ctx, *, arg): logger.info("command: info") await channel_setup(ctx) await user_setup(ctx) matches = get_close_matches(arg, fossils_list, n=1) if matches: fossil = matches[0] delete = await ctx.send("Please wait a moment.") await send_fossil(ctx, str(fossil), message="Here's the image!") await delete.delete() else: await ctx.send("Fossil not found. Are you sure it's on the list?")
async def invite(self, ctx): logger.info("command: invite") await channel_setup(ctx) await user_setup(ctx) embed = discord.Embed(type="rich", colour=discord.Color.blurple()) embed.set_author(name=bot_name) embed.add_field( name="Invite", value="""To invite this bot to your own server, use the following invite link: https://discordapp.com/api/oauth2/authorize?client_id=639279425631813648&permissions=0&scope=bot""", inline=False ) await ctx.send(embed=embed) await ctx.send("https://discord.gg/husFeGG")
async def precache(): timeout = aiohttp.ClientTimeout(total=10 * 60) conn = aiohttp.TCPConnector(limit=100) async with aiohttp.ClientSession(connector=conn, timeout=timeout) as session: logger.info("Starting cache") await asyncio.gather(*(download_media(bird, "images", session=session) for bird in sciBirdListMaster)) logger.info("Starting females") await asyncio.gather( *(download_media(bird, "images", addOn="female", session=session) for bird in sciBirdListMaster) ) logger.info("Starting juveniles") await asyncio.gather( *(download_media(bird, "images", addOn="juvenile", session=session) for bird in sciBirdListMaster) ) logger.info("Starting songs") await asyncio.gather(*(download_media(bird, "songs", session=session) for bird in sciSongBirdsMaster)) logger.info("Images Cached")