mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
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:
parent
ec55622418
commit
acdc1df084
@ -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:
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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,6 +3121,14 @@ 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 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:
|
if ctx.channel.permissions_for(ctx.me).manage_messages:
|
||||||
await ctx.message.delete()
|
await ctx.message.delete()
|
||||||
await ctx.bot.set_shared_api_tokens(service, **tokens)
|
await ctx.bot.set_shared_api_tokens(service, **tokens)
|
||||||
|
|||||||
176
redbot/core/utils/views.py
Normal file
176
redbot/core/utils/views.py
Normal 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)
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user