예제 #1
0
async def spotify_command(
    ctx: tanjun.abc.Context,
    query: str,
    session: alluka.Injected[aiohttp.ClientSession],
    component_client: alluka.Injected[yuyo.ComponentClient],
    spotify_auth: typing.Annotated[utility.ClientCredentialsOauth2,
                                   tanjun.cached_inject(_build_spotify_auth)],
    **kwargs: str,
) -> None:
    """Search for a resource on spotify.

    Arguments:
        * query: The greedy string query to search by.

    Options:
        * type:
            Type of resource to search for. This can be one of "track", "album", "artist" or "playlist" and defaults
            to track.
    """
    resource_type = kwargs["type"].lower()
    if resource_type not in SPOTIFY_RESOURCE_TYPES:
        raise tanjun.CommandError(
            f"{resource_type!r} is not a valid resource type"
        )  # TODO: delete row

    paginator = yuyo.ComponentPaginator(
        SpotifyPaginator(spotify_auth.acquire_token, session, {
            "query": query,
            "type": resource_type
        }),
        authors=[ctx.author.id],
    )

    try:
        if not (first_response := await paginator.get_next_entry()):
            raise tanjun.CommandError(
                f"Couldn't find {resource_type}") from None  # TODO: delete row

    except RuntimeError as exc:
        raise tanjun.CommandError(str(exc)) from None  # TODO: delete row

    except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError) as exc:
        _LOGGER.exception("Spotify returned invalid data", exc_info=exc)
        await ctx.respond(content="Spotify returned invalid data.",
                          component=utility.delete_row(ctx))
        raise

    else:
        content, embed = first_response
        message = await ctx.respond(content,
                                    embed=embed,
                                    component=paginator,
                                    ensure_result=True)
        component_client.set_executor(message, paginator)
예제 #2
0
    def _on_client_response_error(
            self, exception: aiohttp.ClientResponseError) -> bool:
        if exception.status in self._break_on:
            self._backoff_handler.finish()
            return False

        if exception.status >= 500:
            return False

        if exception.status == 429:
            raw_retry_after: str | None = exception.headers.get(
                "Retry-After") if exception.headers else None
            if raw_retry_after is not None:
                retry_after = float(raw_retry_after)

                if retry_after <= 10:
                    self._backoff_handler.set_next_backoff(retry_after)

            return False

        if self._on_404 is not None and exception.status == 404:
            if isinstance(self._on_404, str):
                raise tanjun.CommandError(self._on_404) from None

            else:
                self._on_404()

        return True
예제 #3
0
    def verify(value: _T) -> _T:
        if value in choices:
            return value

        raise tanjun.CommandError(
            f"`{value}` is not a valid choice, must be one of " +
            ", ".join(map(str, choices)))
예제 #4
0
async def role_command(ctx: tanjun.abc.Context, role: hikari.Role) -> None:
    """Get information about a role in the current guild.

    Arguments:
        * role: Mention or ID of the role to get information about.
    """
    if role.guild_id != ctx.guild_id:
        raise tanjun.CommandError("Role not found")

    permissions = utility.basic_name_grid(role.permissions) or "None"
    role_information = [
        f"Created: {tanjun.conversion.from_datetime(role.created_at)}",
        f"Position: {role.position}"
    ]

    if role.colour:
        role_information.append(f"Color: `{role.colour}`")

    if role.is_hoisted:
        role_information.append("Member list hoisted")

    if role.is_managed:
        role_information.append("Managed by an integration")

    if role.is_mentionable:
        role_information.append("Can be mentioned")

    embed = hikari.Embed(
        colour=role.colour,
        title=role.name,
        description="\n".join(role_information) +
        f"\n\nPermissions:\n{permissions}",
    )
    await ctx.respond(embed=embed, component=utility.delete_row(ctx))
예제 #5
0
async def lyrics_command(
    ctx: tanjun.abc.Context,
    query: str,
    session: alluka.Injected[aiohttp.ClientSession],
    component_client: alluka.Injected[yuyo.ComponentClient],
) -> None:
    """Get a song's lyrics.

    Arguments:
        * query: Greedy query string (e.g. name) to search a song by.
    """
    retry = yuyo.Backoff(max_retries=5)
    error_manager = utility.AIOHTTPStatusHandler(
        retry, on_404=f"Couldn't find the lyrics for `{query[:1960]}`")
    async for _ in retry:
        with error_manager:
            response = await session.get("https://evan.lol/lyrics/search/top",
                                         params={"q": query})
            response.raise_for_status()
            break

    else:
        # TODO: delete row
        raise tanjun.CommandError("Couldn't get the lyrics in time") from None

    try:
        data = await response.json()
    except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError,
            ValueError) as exc:
        await ctx.respond(content="Invalid data returned by server.",
                          component=utility.delete_row(ctx))

        _LOGGER.exception(
            "Received unexpected data from lyrics.tsu.sh of type %s\n %s",
            response.headers.get(CONTENT_TYPE_HEADER, "unknown"),
            await response.text(),
            exc_info=exc,
        )
        # TODO: delete row
        raise tanjun.CommandError("Failed to receive lyrics")

    icon: str | None = None
    if "album" in data and (icon_data := data["album"]["icon"]):
        icon = icon_data.get("url")
예제 #6
0
async def query_nekos_life(
    endpoint: str,
    response_key: str,
    session: alluka.Injected[aiohttp.ClientSession],
) -> str:
    # TODO: retries
    response = await session.get(url="https://nekos.life/api/v2" + endpoint)

    try:
        data = await response.json()
    except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError, ValueError):
        data = None

    # Ok so here's a fun fact, whoever designed this api seems to have decided that it'd be appropriate to
    # return error status codes in the json body under the "msg" key while leaving the response as a 200 OK
    # (e.g. a 200 with the json payload '{"msg": "404"}') so here we have to try to get the response code from
    # the json payload (if available) and then fall back to the actual status code.
    # We cannot consistently rely on this behaviour either as any internal server errors will likely return an
    # actual 5xx response.
    try:
        status_code = int(data["msg"])  # type: ignore
    except (LookupError, ValueError, TypeError):
        status_code = response.status

    if status_code == 404:
        raise tanjun.CommandError(
            "Query not found.") from None  # TODO: delete row

    if status_code >= 500 or data is None or response_key not in data:
        raise tanjun.CommandError(
            "Unable to fetch image at the moment due to server error or malformed response."
        ) from None  # TODO: delete row

    if status_code >= 300:
        # TODO: delete row
        raise tanjun.CommandError(
            f"Unable to fetch image due to unexpected error {status_code}"
        ) from None

    result = data[response_key]
    assert isinstance(result, str)
    return result
예제 #7
0
async def youtube_command(
    ctx: tanjun.abc.Context,
    query: str,
    region: str | None,
    language: str | None,
    order: str,
    safe_search: bool | None,
    session: alluka.Injected[aiohttp.ClientSession],
    tokens: alluka.Injected[config_.Tokens],
    component_client: alluka.Injected[yuyo.ComponentClient],
    **kwargs: str,
) -> None:
    """Search for a resource on youtube.

    Arguments:
        * query: Greedy query string to search for a resource by.

    Options:
        * safe search (--safe, -s, --safe-search): whether safe search should be enabled or not.
            By default this will be decided based on the current channel's nsfw status and this cannot be set to
            `false` for a channel that's not nsfw.
        * order (-o, --order): The order to return results in.
            This can be one of "date", "relevance", "title", "videoCount" or "viewCount" and defaults to "relevance".
        * language (-l, --language): The ISO 639-1 two letter identifier of the language to limit search to.
        * region (-r, --region): The ISO 3166-1 code of the region to search for results in.
        * type (--type, -t): The type of resource to search for.
            This can be one of "channel", "playlist" or "video" and defaults to "video".
    """
    resource_type = kwargs["type"]
    assert tokens.google is not None
    if safe_search is not False:
        channel: hikari.PartialChannel | None
        if ctx.cache and (channel := ctx.cache.get_guild_channel(
                ctx.channel_id)):
            channel_is_nsfw = channel.is_nsfw

        else:
            # TODO: handle retires
            channel = await ctx.rest.fetch_channel(ctx.channel_id)
            channel_is_nsfw = channel.is_nsfw if isinstance(
                channel, hikari.GuildChannel) else False

        if safe_search is None:
            safe_search = not channel_is_nsfw

        elif not safe_search and not channel_is_nsfw:
            # TODO: delete row
            raise tanjun.CommandError(
                "Cannot disable safe search in a sfw channel")
예제 #8
0
    async def __call__(
        self,
        ctx: tanjun.abc.Context,
        path: str,
        absolute: bool,
        public: bool,
        component_client: alluka.Injected[yuyo.ComponentClient],
    ) -> None:
        if absolute:
            if not (result := self.index.get_references(path)):
                raise tanjun.CommandError(
                    f"No references found for the absolute path `{path}`")

            full_path = path
            uses = result
예제 #9
0
async def clear_command(
    ctx: tanjun.abc.Context, after: hikari.Snowflake | None, before: hikari.Snowflake | None, **kwargs: typing.Any
) -> None:
    """Clear new messages from chat.

    !!! note
        This can only be used on messages under 14 days old.

    Arguments:
        * count: The amount of messages to delete.

    Options:
        * users (--user): Mentions and/or IDs of the users to delete messages from.
        * human only (--human): Whether this should only delete messages sent by actual users.
            This defaults to false and will be set to true if provided without a value.
        * bot only (--bot): Whether this should only delete messages sent by bots and webhooks.
        * before  (--before): ID of a message to delete messages which were sent before.
        * after (--after): ID of a message to delete messages which were sent after.
        * suppress (-s, --suppress): Provided without a value to stop the bot from sending a message once the
            command's finished.
    """
    now = _now()
    after_too_old = after and now - after.created_at >= MAX_MESSAGE_BULK_DELETE
    before_too_old = before and now - before.created_at >= MAX_MESSAGE_BULK_DELETE

    if after_too_old or before_too_old:
        # TODO: delete row
        raise tanjun.CommandError("Cannot delete messages that are over 14 days old")

    iterator = (
        iter_messages(ctx, after=after, before=before, **kwargs)
        .take_while(lambda message: _now() - message.created_at < MAX_MESSAGE_BULK_DELETE)
        .map(lambda x: x.id)
        .chunk(100)
    )

    await ctx.respond("Starting message deletes", component=utility.delete_row(ctx))
    async for messages in iterator:
        await ctx.rest.delete_messages(ctx.channel_id, *messages)
        break

    try:
        await ctx.edit_last_response(content="Cleared messages.", component=utility.delete_row(ctx), delete_after=2)
    except hikari.NotFoundError:
        await ctx.respond(content="Cleared messages.", component=utility.delete_row(ctx), delete_after=2)
예제 #10
0
async def colour_command(ctx: tanjun.abc.Context, color: hikari.Colour | None,
                         role: hikari.Role | None) -> None:
    """Get a visual representation of a color or role's color.

    Argument:
        colour: Either the hex/int literal representation of a colour to show or the ID/mention of a role to get
            the colour of.
    """
    if role:
        color = role.color

    elif color is None:
        # TODO: delete row
        raise tanjun.CommandError("Either role or color must be provided")

    embed = (hikari.Embed(colour=color).add_field(
        name="RGB", value=str(color.rgb)).add_field(name="HEX",
                                                    value=str(color.hex_code)))
    await ctx.respond(embed=embed, component=utility.delete_row(ctx))
예제 #11
0
    async def __anext__(self) -> tuple[str, hikari.UndefinedType]:
        if not self._buffer and self._offset is not None:
            retry = yuyo.Backoff(max_retries=5)
            resource_type = self._parameters["type"]
            assert isinstance(resource_type, str)
            error_manager = utility.AIOHTTPStatusHandler(
                retry, on_404=utility.raise_error(None, StopAsyncIteration))
            parameters = self._parameters.copy()
            parameters["offset"] = self._offset
            self._offset += self._limit

            async for _ in retry:
                with error_manager:
                    response = await self._session.get(
                        "https://api.spotify.com/v1/search",
                        params=parameters,
                        headers={
                            "Authorization":
                            await self._acquire_authorization(self._session)
                        },
                    )
                    response.raise_for_status()
                    break

            else:
                raise tanjun.CommandError(
                    f"Couldn't fetch {resource_type} in time") from None

            try:
                data = await response.json()
            except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError,
                    ValueError) as exc:
                raise exc

            # TODO: only store urls?
            self._buffer.extend(data[resource_type + "s"]["items"])

        if not self._buffer:
            self._offset = None
            raise StopAsyncIteration

        return (self._buffer.pop(0)["external_urls"]["spotify"],
                hikari.UNDEFINED)
예제 #12
0
    async def build(
        cls, ctx: tanjun.abc.Context, reason: str, delete_message_days: int, members_only: bool
    ) -> _MultiBanner:
        assert ctx.member is not None

        guild = ctx.get_guild() or await ctx.fetch_guild()
        assert guild is not None
        is_owner = ctx.member.id == guild.owner_id

        if not ctx.member.role_ids and not is_owner:
            # If they have no role and aren't the guild owner then the role
            # hierarchy would never let them ban anyone.
            # TODO: delete row
            raise tanjun.CommandError("You cannot ban any of these members")

        if is_owner:
            # If the author is the owner then we don't actually check the role
            # hierarchy so dummy data can be safely used here.
            top_role_position = 999999
            roles: collections.Mapping[hikari.Snowflake, hikari.Role] = {}

        elif isinstance(guild, hikari.RESTGuild):
            roles = guild.roles
            top_role = get_top_role(ctx.member.role_ids, roles)
            top_role_position = top_role.position if top_role else 0

        else:
            roles = guild.get_roles() or {r.id: r for r in await guild.fetch_roles()}
            top_role = get_top_role(ctx.member.role_ids, roles)
            top_role_position = top_role.position if top_role else 0

        return cls(
            ctx=ctx,
            reason=reason,
            author_role_position=top_role_position,
            author_is_guild_owner=is_owner,
            guild=guild,
            delete_message_days=delete_message_days,
            members_only=members_only,
            roles=roles,
        )
예제 #13
0
    async def acquire_token(self, session: aiohttp.ClientSession) -> str:
        if self._token and not self._expired:
            return self._token

        response = await session.post(
            self._path,
            data={"grant_type": "client_credentials"},
            auth=self._authorization)

        if 200 <= response.status < 300:
            try:
                data = await response.json()
                expire = round(time.time()) + data["expires_in"] - 120
                token = data["access_token"]

            except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError,
                    ValueError, KeyError, TypeError) as exc:
                _LOGGER.exception(
                    "Couldn't decode or handle client credentials response received from %s: %r",
                    self._path,
                    await response.text(),
                    exc_info=exc,
                )

            else:
                self._expire_at = expire
                self._token = f"{self._prefix} {token}"
                return self._token

        else:
            _LOGGER.warning(
                "Received %r from %s while trying to authenticate as client credentials",
                response.status,
                self._path,
            )
        raise tanjun.CommandError("Couldn't authenticate")
예제 #14
0
async def moe_command(
    ctx: tanjun.abc.Context,
    session: alluka.Injected[aiohttp.ClientSession],
    source: str | None = None,
) -> None:
    params = {}
    if source is not None:
        params["source"] = source

    retry = yuyo.Backoff(max_retries=5)
    error_manager = utility.AIOHTTPStatusHandler(
        retry,
        on_404=f"Couldn't find source `{source[:1970]}`"
        if source is not None else "couldn't access api")
    async for _ in retry:
        with error_manager:
            response = await session.get("http://api.cutegirls.moe/json",
                                         params=params)
            response.raise_for_status()
            break

    else:
        raise tanjun.CommandError(
            "Couldn't get an image in time") from None  # TODO: delete row

    try:
        data = (await response.json())["data"]
    except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError, LookupError,
            ValueError) as exc:
        await ctx.respond(content="Image API returned invalid data.",
                          component=utility.delete_row(ctx))
        raise exc

    await ctx.respond(
        content=f"{data['image']} (source {data.get('source') or 'unknown'})",
        component=utility.delete_row(ctx))
예제 #15
0
async def eval_command(
    ctx: tanjun.abc.MessageContext,
    component: alluka.Injected[tanjun.abc.Component],
    component_client: alluka.Injected[yuyo.ComponentClient],
    file_output: bool = False,
    # ephemeral_response: bool = False,
    suppress_response: bool = False,
) -> None:
    """Dynamically evaluate a script in the bot's environment.

    This can only be used by the bot's owner.

    Arguments:
        * code: Greedy multi-line string argument of the code to execute. This should be in a code block.
        * suppress_response (-s, --suppress): Whether to suppress this command's confirmation response.
            This defaults to false and will be set to true if no value is provided.
    """
    assert ctx.message.content is not None  # This shouldn't ever be the case in a command client.
    code = re.findall(r"```(?:[\w]*\n?)([\s\S(^\\`{3})]*?)\n*```",
                      ctx.message.content)
    if not code:
        raise tanjun.CommandError("Expected a python code block.")

    if suppress_response:
        await eval_python_code_no_capture(ctx, component, "<string>", code[0])
        return

    stdout, stderr, exec_time, failed = await eval_python_code(
        ctx, component, code[0])

    if file_output:
        await ctx.respond(
            attachments=[
                hikari.Bytes(stdout,
                             "stdout.py",
                             mimetype="text/x-python;charset=utf-8"),
                hikari.Bytes(stderr,
                             "stderr.py",
                             mimetype="text/x-python;charset=utf-8"),
            ],
            component=utility.delete_row(ctx),
        )
        return

    colour = utility.FAILED_COLOUR if failed else utility.PASS_COLOUR
    string_paginator = yuyo.sync_paginate_string(_yields_results(
        stdout, stderr),
                                                 wrapper="```python\n{}\n```",
                                                 char_limit=2034)
    embed_generator = ((
        hikari.UNDEFINED,
        hikari.Embed(colour=colour,
                     description=text,
                     title=f"Eval page {page}").set_footer(
                         text=f"Time taken: {exec_time} ms"),
    ) for text, page in string_paginator)
    paginator = yuyo.ComponentPaginator(
        embed_generator,
        authors=[ctx.author.id],
        triggers=(
            yuyo.pagination.LEFT_DOUBLE_TRIANGLE,
            yuyo.pagination.LEFT_TRIANGLE,
            yuyo.pagination.STOP_SQUARE,
            yuyo.pagination.RIGHT_TRIANGLE,
            yuyo.pagination.RIGHT_DOUBLE_TRIANGLE,
        ),
        timeout=datetime.timedelta(
            days=99999),  # TODO: switch to passing None here
    )
    first_response = await paginator.get_next_entry()
    executor = utility.paginator_with_to_file(
        ctx,
        paginator,
        make_files=lambda: [
            _bytes_from_io(stdout, "stdout.py"),
            _bytes_from_io(stderr, "stderr.py")
        ])

    assert first_response is not None
    content, embed = first_response
    message = await ctx.respond(content=content,
                                embed=embed,
                                components=executor.builders,
                                ensure_result=True)
    component_client.set_executor(message, executor)
예제 #16
0
def cache_check(ctx: tanjun.abc.Context) -> bool:
    if ctx.cache:
        return True

    # TODO: delete row
    raise tanjun.CommandError("Client is cache-less")
예제 #17
0
        path: str,
        absolute: bool,
        public: bool,
        component_client: alluka.Injected[yuyo.ComponentClient],
    ) -> None:
        if absolute:
            if not (result := self.index.get_references(path)):
                raise tanjun.CommandError(
                    f"No references found for the absolute path `{path}`")

            full_path = path
            uses = result

        else:
            if not (result := self.index.search(path)):
                raise tanjun.CommandError(f"No references found for `{path}`")

            full_path, uses = result

        iterator = utility.embed_iterator(
            utility.chunk(iter(uses), 10),
            lambda entries:
            "Note: This only searches return types and attributes.\n\n" + "\n".
            join(entries),
            title=f"{len(uses)} references found for {full_path}",
            cast_embed=lambda e: e.set_footer(text=self.library_repr),
        )
        paginator = yuyo.ComponentPaginator(
            iterator,
            authors=(ctx.author, ) if not public else (),
            triggers=(
예제 #18
0
        "type": resource_type,
    }

    if region is not None:
        parameters["regionCode"] = region

    if language is not None:
        parameters["relevanceLanguage"] = language

    paginator = yuyo.ComponentPaginator(YoutubePaginator(session, parameters),
                                        authors=[ctx.author.id])
    try:
        if not (first_response := await paginator.get_next_entry()):
            # data["pageInfo"]["totalResults"] will not reliably be `0` when no data is returned and they don't use 404
            # for that so we'll just check to see if nothing is being returned.
            raise tanjun.CommandError(
                f"Couldn't find `{query}`.")  # TODO: delete row

    except RuntimeError as exc:
        raise tanjun.CommandError(str(exc)) from None  # TODO: delete row

    except (aiohttp.ContentTypeError, aiohttp.ClientPayloadError) as exc:
        _LOGGER.exception("Youtube returned invalid data", exc_info=exc)
        await ctx.respond(content="Youtube returned invalid data.",
                          component=utility.delete_row(ctx))
        raise

    else:
        content, embed = first_response
        message = await ctx.respond(content,
                                    embed=embed,
                                    component=paginator,
예제 #19
0
def iter_messages(
    ctx: tanjun.abc.Context,
    count: int | None,
    after: hikari.Snowflake | None,
    before: hikari.Snowflake | None,
    bot_only: bool,
    human_only: bool,
    has_attachments: bool,
    has_embeds: bool,
    regex: re.Pattern[str] | None,
    users: collections.Collection[hikari.Snowflake] | None,
) -> hikari.LazyIterator[hikari.Message]:
    if human_only and bot_only:
        # TODO: delete row
        raise tanjun.CommandError("Can only specify one of `human_only` or `user_only`")

    if count is None and after is None:
        # TODO: delete row
        raise tanjun.CommandError("Must specify `count` when `after` is not specified")

    elif count is not None and count <= 0:
        # TODO: delete row
        raise tanjun.CommandError("Count must be greater than 0.")

    if before is None and after is None:
        before = hikari.Snowflake.from_datetime(ctx.created_at)

    if before is not None and after is not None:
        iterator = ctx.rest.fetch_messages(ctx.channel_id, before=before).take_while(lambda message: message.id > after)

    else:
        iterator = ctx.rest.fetch_messages(
            ctx.channel_id,
            before=hikari.UNDEFINED if before is None else before,
            after=hikari.UNDEFINED if after is None else after,
        )

    if human_only:
        iterator = iterator.filter(lambda message: not message.author.is_bot)

    elif bot_only:
        iterator = iterator.filter(lambda message: message.author.is_bot)

    if has_attachments:
        iterator = iterator.filter(lambda message: bool(message.attachments))

    if has_embeds:
        iterator = iterator.filter(lambda message: bool(message.embeds))

    if regex:
        iterator = iterator.filter(lambda message: bool(message.content and regex.match(message.content)))

    if users is not None:
        if not users:
            # TODO: delete row
            raise tanjun.CommandError("Must specify at least one user.")

        iterator = iterator.filter(lambda message: message.author.id in users)

    # TODO: Should we limit count or at least default it to something other than no limit?
    if count:
        iterator = iterator.limit(count)

    return iterator