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")
async def validate(self, ctx, parsed_birdlist): validated_birdlist = [] async with aiohttp.ClientSession() as session: logger.info("starting validation") await ctx.send( "**Validating bird list...**\n*This may take a while.*") invalid_output = [] valid_output = [] validity = [] for x in range(0, len(parsed_birdlist), 10): validity += await asyncio.gather( *(valid_bird(bird, session) for bird in parsed_birdlist[x:x + 10])) logger.info("sleeping during validation...") await asyncio.sleep(5) logger.info("checking validation") for item in validity: if item[1]: validated_birdlist.append( string.capwords( item[3].split(" - ")[0].strip().replace("-", " "))) valid_output.append( f"Item `{item[0]}`: Detected as **{item[3]}**\n") else: invalid_output.append( f"Item `{item[0]}`: **{item[2]}** {f'(Detected as *{item[3]}*)' if item[3] else ''}\n" ) logger.info("done validating") if valid_output: logger.info("sending validation success") valid_output = ( "**Succeeded Items:** Please verify items were detected correctly.\n" + "".join(valid_output)) await self.broken_send(ctx, valid_output) if invalid_output: logger.info("sending validation failure") invalid_output = "**FAILED ITEMS:** Please fix and resubmit.\n" + "".join( invalid_output) await self.broken_send(ctx, invalid_output) return False await ctx.send("**Saving bird list...**") database.sadd(f"custom.list:{ctx.author.id}", *validated_birdlist) database.expire(f"custom.list:{ctx.author.id}", 86400) database.set(f"custom.confirm:{ctx.author.id}", "valid", ex=86400) await ctx.send( "**Ok!** Your bird list has been temporarily saved. " + "Please use `b!custom validate` to view and confirm your bird list. " + "To start over, upload a new list with the message `b!custom replace`. " + "You have 24 hours to confirm before your bird list will automatically be deleted." ) return True
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")
def update_web_user(user_data): logger.info("updating user data") session_id = get_session_id() 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_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"]), }, ) asyncio.run(user_setup(user_id)) tempScore = int(database.hget(f"web.session:{session_id}", "tempScore")) if tempScore not in (0, -1): database.zincrby("users:global", tempScore, int(user_id)) database.hset(f"web.session:{session_id}", "tempScore", -1) logger.info("updated user data")
async def custom(self, ctx, *, args=""): logger.info("command: custom list set") args = args.lower().strip().split(" ") logger.info(f"parsed args: {args}") if ("replace" not in args and ctx.message.attachments and database.exists(f"custom.list:{ctx.author.id}")): await ctx.send( "Woah there. You already have a custom list. " + "To view its contents, use `b!custom view`. " + "If you want to replace your list, upload the file with `b!custom replace`." ) return if "delete" in args and database.exists( f"custom.list:{ctx.author.id}"): if (database.exists(f"custom.confirm:{ctx.author.id}") and database.get(f"custom.confirm:{ctx.author.id}").decode( "utf-8") == "delete"): database.delete(f"custom.list:{ctx.author.id}", f"custom.confirm:{ctx.author.id}") await ctx.send("Ok, your list was deleted.") return database.set(f"custom.confirm:{ctx.author.id}", "delete", ex=86400) await ctx.send( "Are you sure you want to permanently delete your list? " + "Use `b!custom delete` again within 24 hours to clear your custom list." ) return if ("confirm" in args and database.exists(f"custom.confirm:{ctx.author.id}") and database.get(f"custom.confirm:{ctx.author.id}").decode("utf-8") == "confirm"): # list was validated by server and user, making permanent logger.info("user confirmed") database.persist(f"custom.list:{ctx.author.id}") database.delete(f"custom.confirm:{ctx.author.id}") database.set(f"custom.cooldown:{ctx.author.id}", 0, ex=86400) await ctx.send( "Ok, your custom bird list is now available. Use `b!custom view` " + "to view your list. You can change your list again in 24 hours." ) return if ("validate" in args and database.exists(f"custom.confirm:{ctx.author.id}") and database.get(f"custom.confirm:{ctx.author.id}").decode("utf-8") == "valid"): # list was validated, now for user confirm logger.info("valid list, user needs to confirm") database.expire(f"custom.list:{ctx.author.id}", 86400) database.set(f"custom.confirm:{ctx.author.id}", "confirm", ex=86400) birdlist = "\n".join( bird.decode("utf-8") for bird in database.smembers(f"custom.list:{ctx.author.id}")) await ctx.send( f"**Please confirm the following list.** ({int(database.scard(f'custom.list:{ctx.author.id}'))} items)" ) await self.broken_send(ctx, birdlist, between="```\n") await ctx.send( "Once you have looked over the list and are sure you want to add it, " + "please use `b!custom confirm` to have this list added as a custom list. " + "You have another 24 hours to confirm. " + "To start over, upload a new list with the message `b!custom replace`." ) return if "view" in args: if not database.exists(f"custom.list:{ctx.author.id}"): await ctx.send( "You don't have a custom list. To add a custom list, " + "upload a txt file with a bird's name on each line to this DM " + "and put `b!custom` in the **Add a Comment** section.") return birdlist = "\n".join( bird.decode("utf-8") for bird in database.smembers(f"custom.list:{ctx.author.id}")) birdlist = f"{birdlist}" await ctx.send( f"**Your Custom Bird List** ({int(database.scard(f'custom.list:{ctx.author.id}'))} items)" ) await self.broken_send(ctx, birdlist, between="```\n") return if not database.exists( f"custom.list:{ctx.author.id}") or "replace" in args: # user inputted bird list, now validating start = time.perf_counter() if database.exists(f"custom.cooldown:{ctx.author.id}"): await ctx.send( "Sorry, you'll have to wait 24 hours between changing lists." ) return logger.info("reading received bird list") if not ctx.message.attachments: logger.info("no file detected") await ctx.send( "Sorry, no file was detected. Upload your txt file and put `b!custom` in the **Add a Comment** section." ) return decoded = await auto_decode(await ctx.message.attachments[0].read()) if not decoded: logger.info("invalid character encoding") await ctx.send( "Sorry, something went wrong. Are you sure this is a text file?" ) return parsed_birdlist = set( map(lambda x: x.strip(), decoded.strip().split("\n"))) parsed_birdlist.discard("") parsed_birdlist = list(parsed_birdlist) if len(parsed_birdlist) > 200: logger.info("parsed birdlist too long") await ctx.send( "Sorry, we're not supporting custom lists larger than 200 birds. Make sure there are no empty lines." ) return logger.info("checking for invalid characters") char = re.compile(r"[^A-Za-z '\-\xC0-\xD6\xD8-\xF6\xF8-\xFF]") for item in parsed_birdlist: if len(item) > 1000: logger.info("item too long") await ctx.send( f"Line starting with `{item[:100]}` exceeds 1000 characters." ) return search = char.search(item) if search: logger.info("invalid character detected") await ctx.send( f"An invalid character `{search.group()}` was detected. Only letters, spaces, hyphens, and apostrophes are allowed." ) await ctx.send( f"Error on line starting with `{item[:100]}`, position {search.span()[0]}" ) return database.delete(f"custom.list:{ctx.author.id}", f"custom.confirm:{ctx.author.id}") await self.validate(ctx, parsed_birdlist) elapsed = time.perf_counter() - start await ctx.send( f"**Finished validation in {round(elapsed//60)} minutes {round(elapsed%60, 4)} seconds.** {ctx.author.mention}" ) logger.info( f"Finished validation in {round(elapsed//60)} minutes {round(elapsed%60, 4)} seconds." ) return if database.exists(f"custom.confirm:{ctx.author.id}"): next_step = database.get(f"custom.confirm:{ctx.author.id}").decode( "utf-8") if next_step == "valid": await ctx.send( "You need to validate your list. Use `b!custom validate` to do so. " + "You can also delete or replace your list with `b!custom [delete|replace]`" ) return if next_step == "confirm": await ctx.send( "You need to confirm your list. Use `b!custom confirm` to do so. " + "You can also delete or replace your list with `b!custom [delete|replace]`" ) return if next_step == "delete": await ctx.send( "You're in the process of deleting your list. Use `b!custom delete` to do so. " + "You can also replace your list with `b!custom replace`") return capture_message( f"custom.confirm database invalid with {next_step}") await ctx.send( "Whoops, something went wrong. Please report this incident " + "in the support server below.\nhttps://discord.gg/2HbshwGjnm") return await ctx.send( "Use `b!custom view` to view your bird list or `b!custom replace` to replace your bird list." )
async def send_bird(ctx, bird: str, media_type: str, filters: Filter, on_error=None, message=None): """Gets bird media and sends it to the user. `ctx` - Discord context object\n `bird` (str) - bird to send\n `media_type` (str) - type of media (images/songs)\n `filters` (bot.filters Filter)\n `on_error` (function) - async function to run when an error occurs, passes error as argument\n `message` (str) - text message to send before bird\n """ if bird == "": logger.error("error - bird is blank") await ctx.send("**There was an error fetching birds.**") if on_error is not None: await on_error(GenericError("bird is blank", code=100)) else: await ctx.send("*Please try again.*") return # add special condition for screech owls # since screech owl is a genus and SciOly # doesn't specify a species if bird == "Screech Owl": logger.info("choosing specific Screech Owl") bird = random.choice(screech_owls) delete = await ctx.send("**Fetching.** This may take a while.") # trigger "typing" discord message await ctx.trigger_typing() try: filename, extension = await get_media(ctx, bird, media_type, filters) except GenericError as e: await delete.delete() if e.code == 100: await ctx.send( f"**This combination of filters has no valid {media_type} for the current bird.**" ) elif e.code == 201: capture_exception(e) logger.exception(e) await ctx.send( "**A network error has occurred.**\n*Please try again later.*") database.incrby("cooldown:global", amount=1) database.expire("cooldown:global", 300) else: capture_exception(e) logger.exception(e) await ctx.send( f"**An error has occurred while fetching {media_type}.**\n**Reason:** {e}" ) if on_error is not None: await on_error(e) else: await ctx.send("*Please try again.*") return if os.stat( filename).st_size > MAX_FILESIZE: # another filesize check (4mb) await delete.delete() await ctx.send("**Oops! File too large :(**\n*Please try again.*") return if media_type == "images": if filters.bw: # prevent the black and white conversion from blocking loop = asyncio.get_running_loop() fn = functools.partial(_black_and_white, filename) filename = await loop.run_in_executor(None, fn) elif media_type == "songs" and not filters.vc: # remove spoilers in tag metadata audioFile = eyed3.load(filename) if audioFile is not None and audioFile.tag is not None: audioFile.tag.remove(filename) if message is not None: await ctx.send(message) if media_type == "songs" and filters.vc: await voice_functions.play(ctx, filename) else: # change filename to avoid spoilers file_obj = discord.File(filename, filename=f"bird.{extension}") await ctx.send(file=file_obj) await delete.delete()