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 .. automodule:: redbot.core.utils.common_filters
:members: :members:
Utility UI
==========
.. automodule:: redbot.core.utils.views
:members:

View File

@ -254,13 +254,13 @@ else:
args = self.pattern.split(argument) args = self.pattern.split(argument)
if len(args) % 2 != 0: if len(args) % 2 != 0:
raise BadArgument() raise BadArgument(_("Missing a key or value."))
iterator = iter(args) iterator = iter(args)
for key in iterator: for key in iterator:
if self.expected_keys and key not in self.expected_keys: 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) ret[key] = next(iterator)

View File

@ -19,6 +19,7 @@ import traceback
from pathlib import Path from pathlib import Path
from redbot.core import data_manager from redbot.core import data_manager
from redbot.core.utils.menus import menu from redbot.core.utils.menus import menu
from redbot.core.utils.views import SetApiView
from redbot.core.commands import GuildConverter, RawUserIdConverter from redbot.core.commands import GuildConverter, RawUserIdConverter
from string import ascii_letters, digits from string import ascii_letters, digits
from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict, Set 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) @_set.group(name="api", invoke_without_command=True)
@checks.is_owner() @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. 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. 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 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. 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. Note: API tokens are sensitive, so this command should only be used in a private channel or in DM with the bot.
**Examples:** **Examples:**
- `[p]set api`
- `[p]set api spotify
- `[p]set api spotify redirect_uri localhost` - `[p]set api spotify redirect_uri localhost`
- `[p]set api github client_id,whoops client_secret,whoops` - `[p]set api github client_id,whoops client_secret,whoops`
@ -3110,10 +3121,18 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
- `<service>` - The service you're adding tokens to. - `<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 `;`. - `<tokens>` - Pairs of token keys and values. The key and value should be separated by one of ` `, `,`, or `;`.
""" """
if ctx.channel.permissions_for(ctx.me).manage_messages: if service is None: # Handled in order of missing operations
await ctx.message.delete() await ctx.send(_("Click the button below to set your keys."), view=SetApiView())
await ctx.bot.set_shared_api_tokens(service, **tokens) elif tokens is None:
await ctx.send(_("`{service}` API tokens have been set.").format(service=service)) 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)
await ctx.send(_("`{service}` API tokens have been set.").format(service=service))
@_set_api.command(name="list") @_set_api.command(name="list")
async def _set_api_list(self, ctx: commands.Context): async def _set_api_list(self, ctx: commands.Context):

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)
)