Add support for set api Modals (#5637)

* Add support for set api Modals

Co-authored-by: TrustyJAID <TrustyJAID@gmail.com>

* Blaacckkkk!

* Swap locations of interaction and button.

* Clarified template tokens

* Update docs and some string

* More docs

* Rework the client

Co-authored-by: TrustyJAID <TrustyJAID@gmail.com>

* Goddamned black!

* Missed a few arguments

* Black... Again

* Update redbot/core/utils/views.py

Co-authored-by: TrustyJAID <TrustyJAID@gmail.com>

* Update redbot/core/core_commands.py

Co-authored-by: TrustyJAID <TrustyJAID@gmail.com>

Co-authored-by: TrustyJAID <TrustyJAID@gmail.com>
This commit is contained in:
Kowlin 2022-05-20 21:58:18 +02:00 committed by GitHub
parent ec55622418
commit acdc1df084
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 208 additions and 7 deletions

View File

@ -73,3 +73,9 @@ Common Filters
.. automodule:: redbot.core.utils.common_filters
:members:
Utility UI
==========
.. automodule:: redbot.core.utils.views
:members:

View File

@ -254,13 +254,13 @@ else:
args = self.pattern.split(argument)
if len(args) % 2 != 0:
raise BadArgument()
raise BadArgument(_("Missing a key or value."))
iterator = iter(args)
for key in iterator:
if self.expected_keys and key not in self.expected_keys:
raise BadArgument(_("Unexpected key {key}").format(key=key))
raise BadArgument(_("Unexpected key `{key}`.").format(key=key))
ret[key] = next(iterator)

View File

@ -19,6 +19,7 @@ import traceback
from pathlib import Path
from redbot.core import data_manager
from redbot.core.utils.menus import menu
from redbot.core.utils.views import SetApiView
from redbot.core.commands import GuildConverter, RawUserIdConverter
from string import ascii_letters, digits
from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict, Set
@ -3091,18 +3092,28 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
@_set.group(name="api", invoke_without_command=True)
@checks.is_owner()
async def _set_api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter):
async def _set_api(
self,
ctx: commands.Context,
service: Optional[str] = None,
*,
tokens: Optional[TokenConverter] = None,
):
"""
Commands to set, list or remove various external API tokens.
This setting will be asked for by some 3rd party cogs and some core cogs.
If passed without the `<service>` or `<tokens>` arguments it will allow you to open a modal to set your API keys securely.
To add the keys provide the service name and the tokens as a comma separated
list of key,values as described by the cog requesting this command.
Note: API tokens are sensitive, so this command should only be used in a private channel or in DM with the bot.
**Examples:**
- `[p]set api`
- `[p]set api spotify
- `[p]set api spotify redirect_uri localhost`
- `[p]set api github client_id,whoops client_secret,whoops`
@ -3110,6 +3121,14 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
- `<service>` - The service you're adding tokens to.
- `<tokens>` - Pairs of token keys and values. The key and value should be separated by one of ` `, `,`, or `;`.
"""
if service is None: # Handled in order of missing operations
await ctx.send(_("Click the button below to set your keys."), view=SetApiView())
elif tokens is None:
await ctx.send(
_("Click the button below to set your keys."),
view=SetApiView(default_service=service),
)
else:
if ctx.channel.permissions_for(ctx.me).manage_messages:
await ctx.message.delete()
await ctx.bot.set_shared_api_tokens(service, **tokens)

176
redbot/core/utils/views.py Normal file
View File

@ -0,0 +1,176 @@
from __future__ import annotations
import discord
from discord.ext.commands import BadArgument
from typing import List, Dict, Union, Optional
from redbot.core.commands.converter import get_dict_converter
from redbot.core.i18n import Translator
_ = Translator("UtilsViews", __file__)
class SetApiModal(discord.ui.Modal):
"""
A secure ``discord.ui.Modal`` used to set API keys.
This Modal can either be used standalone with its own ``discord.ui.View``
for custom implementations, or created via ``SetApiView``
to have an easy to implemement secure way of setting API keys.
Parameters
----------
default_service: Optional[str]
The service to add the API keys to.
If this is omitted the bot owner is allowed to set his own service.
Defaults to ``None``.
default_keys: Optional[Dict[str, str]]
The API keys the service is expecting.
This will only allow the bot owner to set keys the Modal is expecting.
Defaults to ``None``.
"""
def __init__(
self,
default_service: Optional[str] = None,
default_keys: Optional[Dict[str, str]] = None,
):
self.default_service = default_service
self.default_keys: List[str] = []
if default_keys is not None:
self.default_keys = list(default_keys.keys())
self.default_keys_fmt = self._format_keys(default_keys)
_placeholder_service = "service"
if self.default_service is not None:
_placeholder_service = self.default_service
_placeholder_token = "client_id YOUR_CLIENT_ID\nclient_secret YOUR_CLIENT_SECRET"
if self.default_keys_fmt is not None:
_placeholder_token = self.default_keys_fmt
self.title = _("Set API Keys")
self.keys_label = _("Keys and tokens")
if self.default_service is not None:
self.title = _("Set API Keys for {service}").format(service=self.default_service)
self.keys_label = _("Keys and tokens for {service}").format(
service=self.default_service
)
self.default_service = self.default_service.lower()
# Lower here to prevent someone from capitalizing a service name for the sake of UX.
super().__init__(title=self.title)
self.service_input = discord.ui.TextInput(
label=_("Service"),
required=True,
placeholder=_placeholder_service,
default=self.default_service,
)
self.token_input = discord.ui.TextInput(
label=self.keys_label,
style=discord.TextStyle.long,
required=True,
placeholder=_placeholder_token,
default=self.default_keys_fmt,
)
if self.default_service is None:
self.add_item(self.service_input)
self.add_item(self.token_input)
@staticmethod
def _format_keys(keys: Optional[Dict[str, str]]) -> Optional[str]:
"""Format the keys to be used on a long discord.TextInput format"""
if keys is not None:
ret = ""
for k, v in keys.items():
if v:
ret += f"{k} {v}\n"
else:
ret += f"{k} YOUR_{k.upper()}\n"
return ret
else:
return None
async def on_submit(self, interaction: discord.Interaction):
if not await interaction.client.is_owner(
interaction.user
): # Prevent non-bot owners from somehow aquiring and saving the modal.
return await interaction.response.send_message(
_("This modal is for bot owners only. Whoops!"), ephemeral=True
)
if self.default_keys is not None:
converter = get_dict_converter(*self.default_keys, delims=[";", ",", " "])
else:
converter = get_dict_converter(delims=[";", ",", " "])
tokens = " ".join(self.token_input.value.split("\n")).rstrip()
try:
tokens = await converter().convert(None, tokens)
except BadArgument as exc:
return await interaction.response.send_message(
_("{error_message}\nPlease try again.").format(error_message=str(exc)),
ephemeral=True,
)
if self.default_service is not None: # Check is there is a service set.
await interaction.client.set_shared_api_tokens(self.default_service, **tokens)
return await interaction.response.send_message(
_("`{service}` API tokens have been set.").format(service=self.default_service),
ephemeral=True,
)
else:
service = self.service_input.value.lower()
await interaction.client.set_shared_api_tokens(service, **tokens)
return await interaction.response.send_message(
_("`{service}` API tokens have been set.").format(service=service),
ephemeral=True,
)
class SetApiView(discord.ui.View):
"""
A secure ``discord.ui.View`` used to set API keys.
This view is an standalone, easy to implement ``discord.ui.View``
to allow an bot owner to securely set API keys in a public environment.
Parameters
----------
default_service: Optional[str]
The service to add the API keys to.
If this is omitted the bot owner is allowed to set his own service.
Defaults to ``None``.
default_keys: Optional[Dict[str, str]]
The API keys the service is expecting.
This will only allow the bot owner to set keys the Modal is expecting.
Defaults to ``None``.
"""
def __init__(
self,
default_service: Optional[str] = None,
default_keys: Optional[Dict[str, str]] = None,
):
self.default_service = default_service
self.default_keys = default_keys
super().__init__()
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if not await interaction.client.is_owner(interaction.user):
await interaction.response.send_message(
_("This button is for bot owners only, oh well."), ephemeral=True
)
return False
return True
@discord.ui.button(
label=_("Set API token"),
style=discord.ButtonStyle.grey,
)
async def auth_button(self, interaction: discord.Interaction, button: discord.Button):
return await interaction.response.send_modal(
SetApiModal(self.default_service, self.default_keys)
)