diff --git a/docs/framework_apikeys.rst b/docs/framework_apikeys.rst index adc72a7ee..aec38aa90 100644 --- a/docs/framework_apikeys.rst +++ b/docs/framework_apikeys.rst @@ -73,3 +73,5 @@ Additional References .. automethod:: Red.set_shared_api_tokens .. automethod:: Red.remove_shared_api_tokens + +.. automethod:: Red.remove_shared_api_services diff --git a/redbot/core/bot.py b/redbot/core/bot.py index 19dd8aad2..b79bc4883 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -26,6 +26,7 @@ from typing import ( Any, Literal, MutableMapping, + overload, ) from types import MappingProxyType @@ -986,22 +987,37 @@ class RedBase( """ return await self._config.guild(discord.Object(id=guild_id)).mod_role() - async def get_shared_api_tokens(self, service_name: str) -> Dict[str, str]: + @overload + async def get_shared_api_tokens(self, service_name: str = ...) -> Dict[str, str]: + ... + + @overload + async def get_shared_api_tokens(self, service_name: None = ...) -> Dict[str, Dict[str, str]]: + ... + + async def get_shared_api_tokens( + self, service_name: Optional[str] = None + ) -> Union[Dict[str, Dict[str, str]], Dict[str, str]]: """ - Gets the shared API tokens for a service + Gets the shared API tokens for a service, or all of them if no argument specified. Parameters ---------- - service_name: str - The service to get tokens for. + service_name: str, optional + The service to get tokens for. Leave empty to get tokens for all services. Returns ------- - Dict[str, str] + Dict[str, Dict[str, str]] or Dict[str, str] A Mapping of token names to tokens. This mapping exists because some services have multiple tokens. + If ``service_name`` is `None`, this method will return + a mapping with mappings for all services. """ - return await self._config.custom(SHARED_API_TOKENS, service_name).all() + if service_name is None: + return await self._config.custom(SHARED_API_TOKENS).all() + else: + return await self._config.custom(SHARED_API_TOKENS, service_name).all() async def set_shared_api_tokens(self, service_name: str, **tokens: str): """ @@ -1051,6 +1067,25 @@ class RedBase( for name in token_names: group.pop(name, None) + async def remove_shared_api_services(self, *service_names: str): + """ + Removes shared API services, as well as keys and tokens associated with them. + + Parameters + ---------- + *service_names: str + The services to remove. + + Examples + ---------- + Removing the youtube service + + >>> await ctx.bot.remove_shared_api_services("youtube") + """ + async with self._config.custom(SHARED_API_TOKENS).all() as group: + for service in service_names: + group.pop(service, None) + async def get_context(self, message, *, cls=commands.Context): return await super().get_context(message, cls=cls) diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index bf4b00a0b..1d5e1e25b 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -23,6 +23,7 @@ import aiohttp import discord from babel import Locale as BabelLocale, UnknownLocaleError from redbot.core.data_manager import storage_type +from redbot.core.utils.chat_formatting import box, pagify from . import ( __version__, @@ -2071,10 +2072,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): else: await ctx.send(_("Text must be fewer than 1024 characters long.")) - @_set.command() + @_set.group(invoke_without_command=True) @checks.is_owner() async def api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter): - """Set various external API tokens. + """Set, list or remove various external API tokens. This setting will be asked for by some 3rd party cogs and some core cogs. @@ -2089,6 +2090,47 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): await ctx.bot.set_shared_api_tokens(service, **tokens) await ctx.send(_("`{service}` API tokens have been set.").format(service=service)) + @api.command(name="list") + async def api_list(self, ctx: commands.Context): + """Show all external API services along with their keys that have been set. + + Secrets are not shown.""" + + services: dict = await ctx.bot.get_shared_api_tokens() + if not services: + await ctx.send(_("No API services have been set yet.")) + return + + sorted_services = sorted(services.keys(), key=str.lower) + + joined = _("Set API services:\n") if len(services) > 1 else _("Set API service:\n") + for service_name in sorted_services: + joined += "+ {}\n".format(service_name) + for key_name in services[service_name].keys(): + joined += " - {}\n".format(key_name) + for page in pagify(joined, ["\n"], shorten_by=16): + await ctx.send(box(page.lstrip(" "), lang="diff")) + + @api.command(name="remove") + async def api_remove(self, ctx: commands.Context, *services: str): + """Remove the given services with all their keys and tokens.""" + bot_services = (await ctx.bot.get_shared_api_tokens()).keys() + services = [s for s in services if s in bot_services] + + if services: + await self.bot.remove_shared_api_services(*services) + if len(services) > 1: + msg = _("Services deleted successfully:\n{services_list}").format( + services_list=humanize_list(services) + ) + else: + msg = _("Service deleted successfully: {service_name}").format( + service_name=services[0] + ) + await ctx.send(msg) + else: + await ctx.send(_("None of the services you provided had any keys set.")) + @commands.group() @checks.is_owner() async def helpset(self, ctx: commands.Context):