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)
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
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)))
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))
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")
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
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")
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
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)
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))
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)
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, )
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")
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))
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)
def cache_check(ctx: tanjun.abc.Context) -> bool: if ctx.cache: return True # TODO: delete row raise tanjun.CommandError("Client is cache-less")
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=(
"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,
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