Beispiel #1
0
 def _cache_get(key, default=None):
     if local:
         return _cache.get(key, default)
     data = database.get(f"cache.{func.__name__}:{key}")
     if data is None:
         return default
     return pickle.loads(data)
Beispiel #2
0
    def __call__(self, ctx: commands.Context):
        if (ctx.command.name in (
                "bird",
                "song",
                "goatsucker",
                "check",
                "skip",
        ) and database.exists("cooldown:global")
                and int(database.get("cooldown:global")) > 1):
            bucket = self.rate_limit_mapping.get_bucket(ctx.message)

        elif not self.disable and ctx.guild is None:
            bucket = self.dm_mapping.get_bucket(ctx.message)

        elif ctx.channel.name.startswith(
                "racing") and ctx.command.name.startswith("check"):
            bucket = self.race_mapping.get_bucket(ctx.message)

        else:
            bucket = self.default_mapping.get_bucket(ctx.message)

        retry_after = bucket.update_rate_limit()
        if retry_after:
            raise commands.CommandOnCooldown(bucket, retry_after)
        return True
Beispiel #3
0
 async def disconnect(self, ctx):
     logger.info("command: disconnect")
     current_voice = database.get(f"voice.server:{ctx.guild.id}")
     if current_voice is not None:
         race = ctx.bot.get_cog("Race")
         await race.stop_race_(ctx)
     else:
         await voice_functions.disconnect(ctx)
Beispiel #4
0
async def get_voice_client(
        ctx,
        connect: bool = False,
        silent: bool = False) -> Optional[discord.VoiceClient]:
    logger.info("fetching voice client")

    voice = None
    if ctx.author:
        voice = ctx.author.voice
        if voice is None or voice.channel is None or voice.channel.guild != ctx.guild:
            await _send(ctx, silent,
                        "**Please join a voice channel to connect the bot!**")
            return None

    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")
        bound_channel = ctx.guild.get_channel(int(current_voice))
        await ctx.send("**The voice channel is currently in use!**" +
                       (f"\n*Use {bound_channel.mention} for vc settings!*"
                        if bound_channel else ""))
        return None

    client: discord.VoiceClient = discord.utils.find(
        lambda x: x.guild == ctx.guild, ctx.bot.voice_clients)
    if client is None:
        if connect and voice:
            try:
                client = await voice.channel.connect()
                await _send(ctx, silent,
                            f"Connected to {voice.channel.mention}")
                return client
            except asyncio.TimeoutError:
                await _send(
                    ctx,
                    silent,
                    "**Could not connect to voice channel in time.**\n*Please try again.*",
                )
            except discord.ClientException:
                await _send(
                    ctx, silent,
                    "**I'm already connected to another voice channel!**")
        else:
            await _send(ctx, silent, "**The bot isn't in a voice channel!**")
        return None

    if voice and client.channel != voice.channel:
        await _send(
            ctx, silent,
            "**You need to be in the same voice channel as the bot!**")
        return None
    return client
Beispiel #5
0
async def cleanup(bot):
    logger.info("cleaning up empty channels")
    for client in bot.voice_clients:
        if len(client.channel.voice_states) == 1:
            logger.info("found empty")
            current_voice = database.get(f"voice.server:{client.guild.id}")
            if current_voice is not None:
                logger.info("vc race")
                bound_channel = client.guild.get_channel(int(current_voice))
                race = bot.get_cog("Race")
                await race.stop_race_(FauxContext(bound_channel, bot))
            else:
                await client.disconnect()
    logger.info("done cleaning!")
Beispiel #6
0
    async def start(self, ctx, *, args_str: str = ""):
        logger.info("command: start race")

        if not str(ctx.channel.name).startswith("racing"):
            logger.info("not race channel")
            await ctx.send(
                "**Sorry, racing is not available in this channel.**\n"
                + "*Set the channel name to start with `racing` to enable it.*"
            )
            return

        if database.exists(f"race.data:{ctx.channel.id}"):
            logger.info("already race")
            await ctx.send(
                "**There is already a race in session.** *Change settings/view stats with `b!race view`*"
            )
            return

        filters = Filter.parse(args_str, use_numbers=False)
        if filters.vc:
            if database.get(f"voice.server:{ctx.guild.id}") is not None:
                logger.info("already vc race")
                await ctx.send(
                    "**There is already a VC race in session in this server!**"
                )
                return
            client = await voice_functions.get_voice_client(ctx, connect=True)
            if client is None:
                return
            database.set(f"voice.server:{ctx.guild.id}", str(ctx.channel.id))

        args = args_str.split(" ")
        logger.info(f"args: {args}")

        taxon_args = set(taxons.keys()).intersection({arg.lower() for arg in args})
        if taxon_args:
            taxon = " ".join(taxon_args).strip()
        else:
            taxon = ""

        if "strict" in args:
            strict = "strict"
        else:
            strict = ""

        if "alpha" in args:
            alpha = "alpha"
        else:
            alpha = ""

        states_args = set(states.keys()).intersection({arg.upper() for arg in args})
        if states_args:
            if {"CUSTOM"}.issubset(states_args):
                if database.exists(
                    f"custom.list:{ctx.author.id}"
                ) and not database.exists(f"custom.confirm:{ctx.author.id}"):
                    states_args.discard("CUSTOM")
                    states_args.add(f"CUSTOM:{ctx.author.id}")
                else:
                    states_args.discard("CUSTOM")
                    await ctx.send(
                        "**You don't have a custom list set.**\n*Ignoring the argument.*"
                    )
            state = " ".join(states_args).strip()
        else:
            state = ""

        song = "song" in args or "songs" in args or "s" in args or filters.vc
        image = (
            "image" in args
            or "images" in args
            or "i" in args
            or "picture" in args
            or "pictures" in args
            or "p" in args
        )
        if song and image:
            await ctx.send(
                "**Songs and images are not yet supported.**\n*Please try again*"
            )
            return
        if song:
            media = "song"
        elif image:
            media = "image"
        else:
            media = "image"

        ints = []
        for n in args:
            try:
                ints.append(int(n))
            except ValueError:
                continue
        if ints:
            limit = int(ints[0])
        else:
            limit = 10

        if limit > 1000000:
            await ctx.send("**Sorry, the maximum amount to win is 1 million.**")
            limit = 1000000

        logger.info(
            f"adding filters: {filters}; state: {state}; media: {media}; limit: {limit}"
        )

        database.hset(
            f"race.data:{ctx.channel.id}",
            mapping={
                "start": round(time.time()),
                "stop": 0,
                "limit": limit,
                "filter": str(filters.to_int()),
                "state": state,
                "media": media,
                "taxon": taxon,
                "strict": strict,
                "alpha": alpha,
            },
        )

        database.zadd(f"race.scores:{ctx.channel.id}", {str(ctx.author.id): 0})
        await ctx.send(
            f"**Race started with options:**\n{await self._get_options(ctx)}"
        )

        media = database.hget(f"race.data:{ctx.channel.id}", "media").decode("utf-8")
        logger.info("clearing previous bird")
        database.hset(f"channel:{ctx.channel.id}", "bird", "")
        database.hset(f"channel:{ctx.channel.id}", "answered", "1")

        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)),  # type: ignore
            taxon.decode("utf-8"),  # type: ignore
            state.decode("utf-8"),  # type: ignore
        )
Beispiel #7
0
    async def send_bird_(
        self,
        ctx,
        media_type: Optional[str],
        filters: Filter,
        taxon_str: str = "",
        role_str: str = "",
        retries=0,
    ):
        media_type = ("images" if media_type in ("images", "image", "i",
                                                 "p") else
                      ("songs" if media_type in ("songs", "song", "s",
                                                 "a") else None))
        if not media_type:
            raise GenericError("Invalid media type", code=990)

        if media_type == "songs" and filters.vc:
            current_voice = database.get(f"voice.server:{ctx.guild.id}")
            if current_voice is not None and current_voice.decode(
                    "utf-8") != str(ctx.channel.id):
                logger.info("already vc race")
                await ctx.send("**The voice channel is currently in use!**")
                return

        if taxon_str:
            taxon = taxon_str.split(" ")
        else:
            taxon = []

        if role_str:
            roles = role_str.split(" ")
        else:
            roles = []

        logger.info(
            "bird: " +
            database.hget(f"channel:{ctx.channel.id}", "bird").decode("utf-8"))

        currently_in_race = bool(
            database.exists(f"race.data:{ctx.channel.id}"))

        answered = int(database.hget(f"channel:{ctx.channel.id}", "answered"))
        logger.info(f"answered: {answered}")
        # check to see if previous bird was answered
        if answered:  # if yes, give a new bird
            session_increment(ctx, "total", 1)

            logger.info(f"filters: {filters}; taxon: {taxon}; roles: {roles}")

            if not currently_in_race and retries == 0:
                await ctx.send(
                    "**Recognized arguments:** " +
                    f"*Active Filters*: `{'`, `'.join(filters.display())}`, " +
                    f"*Taxons*: `{'None' if taxon_str == '' else taxon_str}`, "
                    +
                    f"*Detected State*: `{'None' if role_str == '' else role_str}`"
                )

            find_custom_role = {
                i if i.startswith("CUSTOM:") else ""
                for i in roles
            }
            find_custom_role.discard("")
            if (database.exists(f"race.data:{ctx.channel.id}")
                    and len(find_custom_role) == 1):
                custom_role = find_custom_role.pop()
                roles.remove(custom_role)
                roles.append("CUSTOM")
                user_id = custom_role.split(":")[1]
                birds = build_id_list(user_id=user_id,
                                      taxon=taxon,
                                      state=roles,
                                      media=media_type)
            else:
                birds = build_id_list(user_id=ctx.author.id,
                                      taxon=taxon,
                                      state=roles,
                                      media=media_type)

            if not birds:
                logger.info("no birds for taxon/state")
                await ctx.send(
                    "**Sorry, no birds could be found for the taxon/state combo.**\n*Please try again*"
                )
                return

            currentBird = random.choice(birds)
            self.increment_bird_frequency(ctx, currentBird)

            prevB = database.hget(f"channel:{ctx.channel.id}",
                                  "prevB").decode("utf-8")
            while currentBird == prevB and len(birds) > 1:
                currentBird = random.choice(birds)
            database.hset(f"channel:{ctx.channel.id}", "prevB",
                          str(currentBird))
            database.hset(f"channel:{ctx.channel.id}", "bird",
                          str(currentBird))
            logger.info("currentBird: " + str(currentBird))
            database.hset(f"channel:{ctx.channel.id}", "answered", "0")
            await send_bird(
                ctx,
                currentBird,
                media_type,
                filters,
                on_error=self.error_handle(ctx, media_type, filters, taxon_str,
                                           role_str, retries),
                message=(
                    SONG_MESSAGE if media_type == "songs" else BIRD_MESSAGE)
                if not currently_in_race else "*Here you go!*",
            )
        else:  # if no, give the same bird
            await ctx.send(
                f"**Active Filters**: `{'`, `'.join(filters.display())}`")
            await send_bird(
                ctx,
                database.hget(f"channel:{ctx.channel.id}",
                              "bird").decode("utf-8"),
                media_type,
                filters,
                on_error=self.error_handle(ctx, media_type, filters, taxon_str,
                                           role_str, retries),
                message=(
                    SONG_MESSAGE if media_type == "songs" else BIRD_MESSAGE)
                if not currently_in_race else "*Here you go!*",
            )
Beispiel #8
0
    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."
        )
Beispiel #9
0
async def _get_urls(
    session: aiohttp.ClientSession,
    bird: str,
    media_type: str,
    filters: Filter,
    retries: int = 0,
):
    """Returns a list of urls to Macaulay Library media.

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

    `session` (aiohttp ClientSession)\n
    `bird` (str) - can be either common name or scientific name\n
    `media_type` (str) - either `p` for pictures, `a` for audio, or `v` for video\n
    `filters` (bot.filters Filter)
    """
    logger.info(f"getting file urls for {bird}")
    taxon_code = (await get_taxon(bird, session))[0]
    database_key = f"{media_type}/{bird}{filters.to_int()}"
    cursor = (database.get(f"media.cursor:{database_key}") or b"").decode()
    catalog_url = filters.url(taxon_code, media_type, COUNT, cursor)
    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()
        database.set(
            f"media.cursor:{database_key}",
            catalog_data["results"]["nextCursorMark"] or b"",
        )
        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:
            if retries >= 1:
                raise GenericError("No urls found.", code=100)
            logger.info("retrying without cursor")
            retries += 1
            urls = await _get_urls(session, bird, media_type, filters, retries)
            return urls

        return urls