From b45e62f3544773caf37d977a720a627f30e8cece Mon Sep 17 00:00:00 2001 From: Vuks <51289041+Vuks69@users.noreply.github.com> Date: Mon, 5 Oct 2020 22:10:14 +0200 Subject: [PATCH] [Core] add `[p]set api list` and `[p]set api remove` (#4370) * add get_shared_api_keys * add set listapi command * fix typo * refactor `[p]set listapi` into `[p]set api list` * remove unnecessary check * fix decorators * corrected wording * add remove_shared_api_services * add `set api remove` * further typo sniping * bugsniping * minor typo * aaaaaaaaa * Apply suggestions from code review Co-authored-by: Draper <27962761+Drapersniper@users.noreply.github.com> * update api docstring to reflect new subcommands * Apply suggestions from code review Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com> * update framework_apikeys.rst with new methods * Update redbot/core/bot.py Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com> * rewrite get_shared_api_services into a special case of get_shared_api_tokens * rewrite api_list to include keys and tokens in response * don't show secrets * better api_remove response * black * remove nonexistent method * remove unnecessary import * fix wording * Improve docstrings and type hints - added overloads to help out developers a bit more * this wasn't necessary, but development tools work better with this information - fixed type hint to use Union as now its return type depends on whether `service_name` is passed - updated docstrings to contain information about the added behavior * Use `humanize_list()` rather than `str.join()` This is done for consistency within Red and it makes the list have the last element joined with `and` (or its equivalent in chosen locale) * Use `.format()` after translation is applied If `.format()` is used before `_()`, the search for translation is done with the string that already has the dynamic text added, which results in no matches. * Add plural support * Improve error message Updated message to be more specific (used phrases: "None of the services" and "had any keys set") and a bit more nice to the user (judging word "anyway" removed) * Improve readability of `[p]set api list` It's a lot clearer when the sublists are indented, especially on mobile where code blocks aren't colored at all. New look: https://user-images.githubusercontent.com/6032823/94614944-3bbd7c80-02a7-11eb-89e4-64bdf06c650b.png Co-authored-by: Draper <27962761+Drapersniper@users.noreply.github.com> Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com> --- docs/framework_apikeys.rst | 2 ++ redbot/core/bot.py | 47 +++++++++++++++++++++++++++++++----- redbot/core/core_commands.py | 46 +++++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 8 deletions(-) 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):