mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 01:12:33 -05:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
652d9fe950 | ||
|
|
e956e6e320 | ||
|
|
2e58922d01 | ||
|
|
33b7652b62 | ||
|
|
0e9086ca1f | ||
|
|
3ca2a9af28 | ||
|
|
e7b615d921 | ||
|
|
2cb6e98092 | ||
|
|
1ccc441aab | ||
|
|
8ddc5aa63e | ||
|
|
f894b62bfe | ||
|
|
aac9369f3f | ||
|
|
1581604f71 | ||
|
|
56161c0a88 | ||
|
|
242df83785 | ||
|
|
2338ad8223 | ||
|
|
b4f4e080af | ||
|
|
132545e057 | ||
|
|
68590dfdb8 | ||
|
|
2e271d695b | ||
|
|
cd745d35c2 | ||
|
|
6928e2aca2 | ||
|
|
49e86614c5 | ||
|
|
51dcf65fd7 | ||
|
|
c6c0165214 | ||
|
|
342935de49 | ||
|
|
ced5bb4631 | ||
|
|
0a832cee9c | ||
|
|
1cfce8b72c | ||
|
|
cdcde26dfc | ||
|
|
1ffb79f852 | ||
|
|
644aaf0c0e | ||
|
|
cdea03792d | ||
|
|
b190e7417e | ||
|
|
7dd3ff7c8d | ||
|
|
21a253103e | ||
|
|
db3fb29b30 | ||
|
|
c5d2ae5831 | ||
|
|
9d0ca00f89 | ||
|
|
79e5d2c9d7 | ||
|
|
3a62d392b4 | ||
|
|
f2858ea48c | ||
|
|
3c78fb420b |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
patreon: Red_Devs
|
||||||
@@ -106,7 +106,7 @@ plugins directly from Discord! A few examples are:
|
|||||||
- Casino
|
- Casino
|
||||||
- Reaction roles
|
- Reaction roles
|
||||||
- Slow Mode
|
- Slow Mode
|
||||||
- Anilist
|
- AniList
|
||||||
- And much, much more!
|
- And much, much more!
|
||||||
|
|
||||||
Feel free to take a [peek](https://cogboard.red/t/approved-repositories/210) at a list of
|
Feel free to take a [peek](https://cogboard.red/t/approved-repositories/210) at a list of
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Trivia
|
|||||||
Utility Functions
|
Utility Functions
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
* New: ``chat_formatting.humaize_timedelta`` (`#2412`_)
|
* New: ``chat_formatting.humanize_timedelta`` (`#2412`_)
|
||||||
* ``Tunnel`` - Spelling correction of method name - changed ``files_from_attatch`` to ``files_from_attach`` (old name is left for backwards compatibility) (`#2496`_)
|
* ``Tunnel`` - Spelling correction of method name - changed ``files_from_attatch`` to ``files_from_attach`` (old name is left for backwards compatibility) (`#2496`_)
|
||||||
* ``Tunnel`` - fixed behavior of ``react_close()``, now when tunnel closes message will be sent to other end (`#2507`_)
|
* ``Tunnel`` - fixed behavior of ``react_close()``, now when tunnel closes message will be sent to other end (`#2507`_)
|
||||||
* ``chat_formatting.humanize_list`` - Improved error handling of empty lists (`#2597`_)
|
* ``chat_formatting.humanize_list`` - Improved error handling of empty lists (`#2597`_)
|
||||||
|
|||||||
@@ -188,23 +188,17 @@ Choose one of the following commands to install Red.
|
|||||||
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||||
``python3.7 -m pip`` commands.
|
``python3.7 -m pip`` commands.
|
||||||
|
|
||||||
To install without audio support:
|
To install without MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python3.7 -m pip install -U Red-DiscordBot
|
python3.7 -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
Or, to install with audio support:
|
Or, to install with MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python3.7 -m pip install -U Red-DiscordBot[voice]
|
python3.7 -m pip install -U Red-DiscordBot[mongo]
|
||||||
|
|
||||||
Or, install with audio and MongoDB support:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
python3.7 -m pip install -U Red-DiscordBot[voice,mongo]
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|||||||
@@ -62,23 +62,17 @@ Installing Red
|
|||||||
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||||
``pip`` commands.
|
``pip`` commands.
|
||||||
|
|
||||||
* No audio:
|
* No MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
* With audio:
|
* With MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot[voice]
|
python -m pip install -U Red-DiscordBot[mongo]
|
||||||
|
|
||||||
* With audio and MongoDB support:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot[voice,mongo]
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class VersionInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.1.0"
|
__version__ = "3.1.2"
|
||||||
version_info = VersionInfo.from_str(__version__)
|
version_info = VersionInfo.from_str(__version__)
|
||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ class Alias(commands.Cog):
|
|||||||
await ctx.send(_("There is no alias with the name `{name}`").format(name=alias_name))
|
await ctx.send(_("There is no alias with the name `{name}`").format(name=alias_name))
|
||||||
|
|
||||||
@checks.mod_or_permissions(manage_guild=True)
|
@checks.mod_or_permissions(manage_guild=True)
|
||||||
@alias.command(name="del")
|
@alias.command(name="delete", aliases=["del", "remove"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Delete an existing alias on this server."""
|
"""Delete an existing alias on this server."""
|
||||||
@@ -394,7 +394,7 @@ class Alias(commands.Cog):
|
|||||||
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
|
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@global_.command(name="del")
|
@global_.command(name="delete", aliases=["del", "remove"])
|
||||||
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Delete an existing global alias."""
|
"""Delete an existing global alias."""
|
||||||
aliases = await self.unloaded_global_aliases()
|
aliases = await self.unloaded_global_aliases()
|
||||||
|
|||||||
@@ -592,15 +592,15 @@ class Audio(commands.Cog):
|
|||||||
async def spotifyapi(self, ctx):
|
async def spotifyapi(self, ctx):
|
||||||
"""Instructions to set the Spotify API tokens."""
|
"""Instructions to set the Spotify API tokens."""
|
||||||
message = _(
|
message = _(
|
||||||
f"1. Go to Spotify developers and log in with your Spotify account\n"
|
"1. Go to Spotify developers and log in with your Spotify account.\n"
|
||||||
"(https://developer.spotify.com/dashboard/applications)\n"
|
"(https://developer.spotify.com/dashboard/applications)\n"
|
||||||
'2. Click "Create An App"\n'
|
'2. Click "Create An App".\n'
|
||||||
"3. Fill out the form provided with your app name, etc.\n"
|
"3. Fill out the form provided with your app name, etc.\n"
|
||||||
'4. When asked if you\'re developing commercial integration select "No"\n'
|
'4. When asked if you\'re developing commercial integration select "No".\n'
|
||||||
"5. Accept the terms and conditions.\n"
|
"5. Accept the terms and conditions.\n"
|
||||||
"6. Copy your client ID and your client secret into:\n"
|
"6. Copy your client ID and your client secret into:\n"
|
||||||
"`{prefix}set api spotify client_id,your_client_id "
|
"`{prefix}set api spotify client_id,<your_client_id_here> "
|
||||||
"client_secret,your_client_secret`"
|
"client_secret,<your_client_secret_here>`"
|
||||||
).format(prefix=ctx.prefix)
|
).format(prefix=ctx.prefix)
|
||||||
await ctx.maybe_send_embed(message)
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
@@ -660,7 +660,7 @@ class Audio(commands.Cog):
|
|||||||
"6. Click on Create Credential at the top.\n"
|
"6. Click on Create Credential at the top.\n"
|
||||||
'7. At the top click the link for "API key".\n'
|
'7. At the top click the link for "API key".\n'
|
||||||
"8. No application restrictions are needed. Click Create at the bottom.\n"
|
"8. No application restrictions are needed. Click Create at the bottom.\n"
|
||||||
"9. You now have a key to add to `{prefix}set api youtube api_key,your_api_key`"
|
"9. You now have a key to add to `{prefix}set api youtube api_key,<your_api_key_here>`"
|
||||||
).format(prefix=ctx.prefix)
|
).format(prefix=ctx.prefix)
|
||||||
await ctx.maybe_send_embed(message)
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import aiohttp
|
|||||||
from redbot.core import data_manager
|
from redbot.core import data_manager
|
||||||
|
|
||||||
JAR_VERSION = "3.2.0.3"
|
JAR_VERSION = "3.2.0.3"
|
||||||
JAR_BUILD = 772
|
JAR_BUILD = 796
|
||||||
LAVALINK_DOWNLOAD_URL = (
|
LAVALINK_DOWNLOAD_URL = (
|
||||||
f"https://github.com/Cog-Creators/Lavalink-Jars/releases/download/{JAR_VERSION}_{JAR_BUILD}/"
|
f"https://github.com/Cog-Creators/Lavalink-Jars/releases/download/{JAR_VERSION}_{JAR_BUILD}/"
|
||||||
f"Lavalink.jar"
|
f"Lavalink.jar"
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class Cleanup(commands.Cog):
|
|||||||
Tries its best to cleanup after itself if the response is positive.
|
Tries its best to cleanup after itself if the response is positive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if ctx.assume_yes:
|
||||||
|
return True
|
||||||
|
|
||||||
prompt = await ctx.send(
|
prompt = await ctx.send(
|
||||||
_("Are you sure you want to delete {number} messages? (y/n)").format(number=number)
|
_("Are you sure you want to delete {number} messages? (y/n)").format(number=number)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ class CommandObj:
|
|||||||
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
||||||
|
|
||||||
async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]:
|
async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]:
|
||||||
|
if not command:
|
||||||
|
raise NotFound()
|
||||||
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
||||||
if not ccinfo:
|
if not ccinfo:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
@@ -296,7 +298,7 @@ class CustomCommands(commands.Cog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@customcom.command(name="delete")
|
@customcom.command(name="delete", aliases=["del", "remove"])
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_delete(self, ctx, command: str.lower):
|
async def cc_delete(self, ctx, command: str.lower):
|
||||||
"""Delete a custom command.
|
"""Delete a custom command.
|
||||||
|
|||||||
@@ -199,7 +199,8 @@ class Downloader(commands.Cog):
|
|||||||
if not deps:
|
if not deps:
|
||||||
return await ctx.send_help()
|
return await ctx.send_help()
|
||||||
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
||||||
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
async with ctx.typing():
|
||||||
|
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
await ctx.send(_("Libraries installed."))
|
await ctx.send(_("Libraries installed."))
|
||||||
@@ -245,7 +246,7 @@ class Downloader(commands.Cog):
|
|||||||
if repo.install_msg is not None:
|
if repo.install_msg is not None:
|
||||||
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@repo.command(name="delete", aliases=["remove"], usage="<repo_name>")
|
@repo.command(name="delete", aliases=["remove", "del"], usage="<repo_name>")
|
||||||
async def _repo_del(self, ctx, repo: Repo):
|
async def _repo_del(self, ctx, repo: Repo):
|
||||||
"""Remove a repo and its files."""
|
"""Remove a repo and its files."""
|
||||||
await self._repo_manager.delete_repo(repo.name)
|
await self._repo_manager.delete_repo(repo.name)
|
||||||
@@ -423,35 +424,39 @@ class Downloader(commands.Cog):
|
|||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
_("None of the updated cogs were previously loaded. Update complete.")
|
_("None of the updated cogs were previously loaded. Update complete.")
|
||||||
)
|
)
|
||||||
message = _("Would you like to reload the updated cogs?")
|
|
||||||
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
|
|
||||||
if not can_react:
|
|
||||||
message += " (y/n)"
|
|
||||||
query: discord.Message = await ctx.send(message)
|
|
||||||
if can_react:
|
|
||||||
# noinspection PyAsyncCall
|
|
||||||
start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS, ctx.bot.loop)
|
|
||||||
pred = ReactionPredicate.yes_or_no(query, ctx.author)
|
|
||||||
event = "reaction_add"
|
|
||||||
else:
|
|
||||||
pred = MessagePredicate.yes_or_no(ctx)
|
|
||||||
event = "message"
|
|
||||||
try:
|
|
||||||
await ctx.bot.wait_for(event, check=pred, timeout=30)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await query.delete()
|
|
||||||
return
|
|
||||||
|
|
||||||
if pred.result is True:
|
if not ctx.assume_yes:
|
||||||
|
message = _("Would you like to reload the updated cogs?")
|
||||||
|
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
|
||||||
|
if not can_react:
|
||||||
|
message += " (y/n)"
|
||||||
|
query: discord.Message = await ctx.send(message)
|
||||||
if can_react:
|
if can_react:
|
||||||
with contextlib.suppress(discord.Forbidden):
|
# noinspection PyAsyncCall
|
||||||
await query.clear_reactions()
|
start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS, ctx.bot.loop)
|
||||||
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
pred = ReactionPredicate.yes_or_no(query, ctx.author)
|
||||||
else:
|
event = "reaction_add"
|
||||||
if can_react:
|
|
||||||
await query.delete()
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("OK then."))
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
|
event = "message"
|
||||||
|
try:
|
||||||
|
await ctx.bot.wait_for(event, check=pred, timeout=30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await query.delete()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not pred.result:
|
||||||
|
if can_react:
|
||||||
|
await query.delete()
|
||||||
|
else:
|
||||||
|
await ctx.send(_("OK then."))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if can_react:
|
||||||
|
with contextlib.suppress(discord.Forbidden):
|
||||||
|
await query.clear_reactions()
|
||||||
|
|
||||||
|
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
||||||
|
|
||||||
@cog.command(name="list", usage="<repo_name>")
|
@cog.command(name="list", usage="<repo_name>")
|
||||||
async def _cog_list(self, ctx, repo: Repo):
|
async def _cog_list(self, ctx, repo: Repo):
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ class Installable(RepoJSONMixin):
|
|||||||
if self._location.is_file():
|
if self._location.is_file():
|
||||||
copy_func = shutil.copy2
|
copy_func = shutil.copy2
|
||||||
else:
|
else:
|
||||||
|
# clear copy_tree's cache to make sure missing directories are created (GH-2690)
|
||||||
|
distutils.dir_util._path_created = {}
|
||||||
copy_func = distutils.dir_util.copy_tree
|
copy_func = distutils.dir_util.copy_tree
|
||||||
|
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class Filter(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Those words were already in the filter."))
|
await ctx.send(_("Those words were already in the filter."))
|
||||||
|
|
||||||
@_filter.command(name="remove")
|
@_filter.command(name="delete", aliases=["remove", "del"])
|
||||||
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
||||||
"""Remove words from the filter.
|
"""Remove words from the filter.
|
||||||
|
|
||||||
|
|||||||
@@ -307,14 +307,17 @@ class General(commands.Cog):
|
|||||||
messages = []
|
messages = []
|
||||||
for ud in data["list"]:
|
for ud in data["list"]:
|
||||||
ud.setdefault("example", "N/A")
|
ud.setdefault("example", "N/A")
|
||||||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
|
||||||
if len(description) > 2048:
|
|
||||||
description = "{}...".format(description[:2045])
|
|
||||||
|
|
||||||
message = _(
|
message = _(
|
||||||
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
||||||
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
||||||
).format(word=ud.pop("word").capitalize(), description=description, **ud)
|
).format(word=ud.pop("word").capitalize(), description="{description}", **ud)
|
||||||
|
max_desc_len = 2000 - len(message)
|
||||||
|
|
||||||
|
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||||
|
if len(description) > max_desc_len:
|
||||||
|
description = "{}...".format(description[: max_desc_len - 3])
|
||||||
|
|
||||||
|
message = message.format(description=description)
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
if messages is not None and len(messages) > 0:
|
if messages is not None and len(messages) > 0:
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ from redbot.core import checks, Config, commands
|
|||||||
|
|
||||||
_ = Translator("Image", __file__)
|
_ = Translator("Image", __file__)
|
||||||
|
|
||||||
GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Image(commands.Cog):
|
class Image(commands.Cog):
|
||||||
@@ -138,20 +136,20 @@ class Image(commands.Cog):
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def imgurcreds(self, ctx):
|
async def imgurcreds(self, ctx):
|
||||||
"""Explain how to set imgur API tokens"""
|
"""Explain how to set imgur API tokens."""
|
||||||
|
|
||||||
message = _(
|
message = _(
|
||||||
"To get an Imgur Client ID:\n"
|
"To get an Imgur Client ID:\n"
|
||||||
"1. Login to an Imgur account.\n"
|
"1. Login to an Imgur account.\n"
|
||||||
"2. Visit [this](https://api.imgur.com/oauth2/addclient) page\n"
|
"2. Visit this page https://api.imgur.com/oauth2/addclient.\n"
|
||||||
"3. Enter a name for your application\n"
|
"3. Enter a name for your application.\n"
|
||||||
"4. Select *Anonymous usage without user authorization* for the auth type\n"
|
"4. Select *Anonymous usage without user authorization* for the auth type.\n"
|
||||||
"5. Set the authorization callback URL to `https://localhost`\n"
|
"5. Set the authorization callback URL to `https://localhost`.\n"
|
||||||
"6. Leave the app website blank\n"
|
"6. Leave the app website blank.\n"
|
||||||
"7. Enter a valid email address and a description\n"
|
"7. Enter a valid email address and a description.\n"
|
||||||
"8. Check the captcha box and click next\n"
|
"8. Check the captcha box and click next.\n"
|
||||||
"9. Your Client ID will be on the next page.\n"
|
"9. Your Client ID will be on the next page.\n"
|
||||||
"10. do `{prefix}set api imgur client_id,your_client_id`\n"
|
"10. Run the command `{prefix}set api imgur client_id,<your_client_id_here>`.\n"
|
||||||
).format(prefix=ctx.prefix)
|
).format(prefix=ctx.prefix)
|
||||||
|
|
||||||
await ctx.maybe_send_embed(message)
|
await ctx.maybe_send_embed(message)
|
||||||
@@ -166,8 +164,17 @@ class Image(commands.Cog):
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
giphy_api_key = await ctx.bot.db.api_tokens.get_raw("GIPHY", default=None)
|
||||||
|
if not giphy_api_key:
|
||||||
|
await ctx.send(
|
||||||
|
_("An API key has not been set! Please set one with `{prefix}giphycreds`.").format(
|
||||||
|
prefix=ctx.prefix
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
|
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
|
||||||
GIPHY_API_KEY, keywords
|
giphy_api_key["api_key"], keywords
|
||||||
)
|
)
|
||||||
|
|
||||||
async with self.session.get(url) as r:
|
async with self.session.get(url) as r:
|
||||||
@@ -190,8 +197,17 @@ class Image(commands.Cog):
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
giphy_api_key = await ctx.bot.db.api_tokens.get_raw("GIPHY", default=None)
|
||||||
|
if not giphy_api_key:
|
||||||
|
await ctx.send(
|
||||||
|
_("An API key has not been set! Please set one with `{prefix}giphycreds`.").format(
|
||||||
|
prefix=ctx.prefix
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
|
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
|
||||||
GIPHY_API_KEY, keywords
|
giphy_api_key["api_key"], keywords
|
||||||
)
|
)
|
||||||
|
|
||||||
async with self.session.get(url) as r:
|
async with self.session.get(url) as r:
|
||||||
@@ -203,3 +219,21 @@ class Image(commands.Cog):
|
|||||||
await ctx.send(_("No results found."))
|
await ctx.send(_("No results found."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Error contacting the API."))
|
await ctx.send(_("Error contacting the API."))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@commands.command()
|
||||||
|
async def giphycreds(self, ctx):
|
||||||
|
"""Explain how to set Giphy API tokens"""
|
||||||
|
|
||||||
|
message = _(
|
||||||
|
"To get a Giphy API Key:\n"
|
||||||
|
"1. Login to a Giphy account.\n"
|
||||||
|
"2. Visit [this](https://developers.giphy.com/dashboard) page\n"
|
||||||
|
"3. Press `Create an App`\n"
|
||||||
|
"4. Write an app name, example: `Red Bot`\n"
|
||||||
|
"5. Write an app description, example: `Used for Red Bot`\n"
|
||||||
|
"6. Copy the API key shown.\n"
|
||||||
|
"7. Do `{prefix}set api GIPHY api_key,your_api_key`\n"
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
|
|
||||||
|
await ctx.maybe_send_embed(message)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .kickban import KickBanMixin
|
|||||||
from .movetocore import MoveToCore
|
from .movetocore import MoveToCore
|
||||||
from .mutes import MuteMixin
|
from .mutes import MuteMixin
|
||||||
from .names import ModInfo
|
from .names import ModInfo
|
||||||
|
from .slowmode import Slowmode
|
||||||
from .settings import ModSettings
|
from .settings import ModSettings
|
||||||
|
|
||||||
_ = T_ = Translator("Mod", __file__)
|
_ = T_ = Translator("Mod", __file__)
|
||||||
@@ -36,6 +37,7 @@ class Mod(
|
|||||||
MoveToCore,
|
MoveToCore,
|
||||||
MuteMixin,
|
MuteMixin,
|
||||||
ModInfo,
|
ModInfo,
|
||||||
|
Slowmode,
|
||||||
commands.Cog,
|
commands.Cog,
|
||||||
metaclass=CompositeMetaClass,
|
metaclass=CompositeMetaClass,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class ModInfo(MixinMeta):
|
|||||||
if voice_state and voice_state.channel:
|
if voice_state and voice_state.channel:
|
||||||
data.add_field(
|
data.add_field(
|
||||||
name=_("Current voice channel"),
|
name=_("Current voice channel"),
|
||||||
value="{0.name} (ID {0.id})".format(voice_state.channel),
|
value="{0.mention} ID: {0.id}".format(voice_state.channel),
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
data.set_footer(text=_("Member #{} | User ID: {}").format(member_number, user.id))
|
data.set_footer(text=_("Member #{} | User ID: {}").format(member_number, user.id))
|
||||||
@@ -164,7 +164,7 @@ class ModInfo(MixinMeta):
|
|||||||
await ctx.send(embed=data)
|
await ctx.send(embed=data)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def names(self, ctx: commands.Context, user: discord.Member):
|
async def names(self, ctx: commands.Context, *, user: discord.Member):
|
||||||
"""Show previous names and nicknames of a user."""
|
"""Show previous names and nicknames of a user."""
|
||||||
names, nicks = await self.get_names_and_nicks(user)
|
names, nicks = await self.get_names_and_nicks(user)
|
||||||
msg = ""
|
msg = ""
|
||||||
|
|||||||
41
redbot/cogs/mod/slowmode.py
Normal file
41
redbot/cogs/mod/slowmode.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import re
|
||||||
|
from .abc import MixinMeta
|
||||||
|
from datetime import timedelta
|
||||||
|
from redbot.core import commands, i18n, checks
|
||||||
|
from redbot.core.utils.chat_formatting import humanize_timedelta
|
||||||
|
|
||||||
|
_ = i18n.Translator("Mod", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
class Slowmode(MixinMeta):
|
||||||
|
"""
|
||||||
|
Commands regarding channel slowmode management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_channels=True)
|
||||||
|
@checks.admin_or_permissions(manage_channels=True)
|
||||||
|
async def slowmode(
|
||||||
|
self,
|
||||||
|
ctx,
|
||||||
|
*,
|
||||||
|
interval: commands.TimedeltaConverter(
|
||||||
|
minimum=timedelta(seconds=0), maximum=timedelta(hours=6)
|
||||||
|
) = timedelta(seconds=0),
|
||||||
|
):
|
||||||
|
"""Changes channel's slowmode setting.
|
||||||
|
|
||||||
|
Interval can be anything from 0 seconds to 6 hours.
|
||||||
|
Use without parameters to disable.
|
||||||
|
"""
|
||||||
|
seconds = interval.total_seconds()
|
||||||
|
await ctx.channel.edit(slowmode_delay=seconds)
|
||||||
|
if seconds > 0:
|
||||||
|
await ctx.send(
|
||||||
|
_("Slowmode interval is now {interval}.").format(
|
||||||
|
interval=humanize_timedelta(timedelta=interval)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Slowmode has been disabled."))
|
||||||
@@ -128,10 +128,9 @@ class Streams(commands.Cog):
|
|||||||
stream = PicartoStream(name=channel_name)
|
stream = PicartoStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@staticmethod
|
async def check_online(self, ctx: commands.Context, stream):
|
||||||
async def check_online(ctx: commands.Context, stream):
|
|
||||||
try:
|
try:
|
||||||
embed = await stream.is_online()
|
info = await stream.is_online()
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
await ctx.send(_("That user is offline."))
|
await ctx.send(_("That user is offline."))
|
||||||
except StreamNotFound:
|
except StreamNotFound:
|
||||||
@@ -155,6 +154,14 @@ class Streams(commands.Cog):
|
|||||||
_("Something went wrong whilst trying to contact the stream service's API.")
|
_("Something went wrong whilst trying to contact the stream service's API.")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
if isinstance(info, tuple):
|
||||||
|
embed, is_rerun = info
|
||||||
|
ignore_reruns = await self.db.guild(ctx.channel.guild).ignore_reruns()
|
||||||
|
if ignore_reruns and is_rerun:
|
||||||
|
await ctx.send(_("That user is offline."))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
embed = info
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@@ -309,18 +316,19 @@ class Streams(commands.Cog):
|
|||||||
@streamset.command()
|
@streamset.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def twitchtoken(self, ctx: commands.Context):
|
async def twitchtoken(self, ctx: commands.Context):
|
||||||
"""Explain how to set the twitch token"""
|
"""Explain how to set the twitch token."""
|
||||||
|
|
||||||
message = _(
|
message = _(
|
||||||
"To set the twitch API tokens, follow these steps:\n"
|
"To set the twitch API tokens, follow these steps:\n"
|
||||||
"1. Go to this page: https://dev.twitch.tv/dashboard/apps.\n"
|
"1. Go to this page: https://dev.twitch.tv/dashboard/apps.\n"
|
||||||
"2. Click *Register Your Application*\n"
|
"2. Click *Register Your Application*.\n"
|
||||||
"3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and \n"
|
"3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and "
|
||||||
"select an Application Category of your choosing."
|
"select an Application Category of your choosing.\n"
|
||||||
"4. Click *Register*, and on the following page, copy the Client ID.\n"
|
"4. Click *Register*.\n"
|
||||||
"5. do `{prefix}set api twitch client_id,your_client_id`\n\n"
|
"5. On the following page, copy the Client ID.\n"
|
||||||
|
"6. Run the command `{prefix}set api twitch client_id,<your_client_id_here>`\n\n"
|
||||||
"Note: These tokens are sensitive and should only be used in a private channel\n"
|
"Note: These tokens are sensitive and should only be used in a private channel\n"
|
||||||
"or in DM with the bot.)\n"
|
"or in DM with the bot.\n"
|
||||||
).format(prefix=ctx.prefix)
|
).format(prefix=ctx.prefix)
|
||||||
|
|
||||||
await ctx.maybe_send_embed(message)
|
await ctx.maybe_send_embed(message)
|
||||||
@@ -328,17 +336,18 @@ class Streams(commands.Cog):
|
|||||||
@streamset.command()
|
@streamset.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def youtubekey(self, ctx: commands.Context):
|
async def youtubekey(self, ctx: commands.Context):
|
||||||
"""Explain how to set the YouTube token"""
|
"""Explain how to set the YouTube token."""
|
||||||
|
|
||||||
message = _(
|
message = _(
|
||||||
"To get one, do the following:\n"
|
"To get one, do the following:\n"
|
||||||
"1. Create a project\n"
|
"1. Create a project\n"
|
||||||
"(see https://support.google.com/googleapi/answer/6251787 for details)\n"
|
"(see https://support.google.com/googleapi/answer/6251787 for details)\n"
|
||||||
"2. Enable the YouTube Data API v3 \n"
|
"2. Enable the YouTube Data API v3 \n"
|
||||||
"(see https://support.google.com/googleapi/answer/6158841for instructions)\n"
|
"(see https://support.google.com/googleapi/answer/6158841 for instructions)\n"
|
||||||
"3. Set up your API key \n"
|
"3. Set up your API key \n"
|
||||||
"(see https://support.google.com/googleapi/answer/6158862 for instructions)\n"
|
"(see https://support.google.com/googleapi/answer/6158862 for instructions)\n"
|
||||||
"4. Copy your API key and do `{prefix}set api youtube api_key,your_api_key`\n\n"
|
"4. Copy your API key and run the command "
|
||||||
|
"`{prefix}set api youtube api_key,<your_api_key_here>`\n\n"
|
||||||
"Note: These tokens are sensitive and should only be used in a private channel\n"
|
"Note: These tokens are sensitive and should only be used in a private channel\n"
|
||||||
"or in DM with the bot.\n"
|
"or in DM with the bot.\n"
|
||||||
).format(prefix=ctx.prefix)
|
).format(prefix=ctx.prefix)
|
||||||
@@ -538,7 +547,11 @@ class Streams(commands.Cog):
|
|||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
try:
|
try:
|
||||||
embed, is_rerun = await stream.is_online()
|
if stream.__class__.__name__ == "TwitchStream":
|
||||||
|
embed, is_rerun = await stream.is_online()
|
||||||
|
else:
|
||||||
|
embed = await stream.is_online()
|
||||||
|
is_rerun = False
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
if not stream._messages_cache:
|
if not stream._messages_cache:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class Trivia(commands.Cog):
|
|||||||
settings_dict = await settings.all()
|
settings_dict = await settings.all()
|
||||||
msg = box(
|
msg = box(
|
||||||
_(
|
_(
|
||||||
"**Current settings**\n"
|
"Current settings\n"
|
||||||
"Bot gains points: {bot_plays}\n"
|
"Bot gains points: {bot_plays}\n"
|
||||||
"Answer time limit: {delay} seconds\n"
|
"Answer time limit: {delay} seconds\n"
|
||||||
"Lack of response timeout: {timeout} seconds\n"
|
"Lack of response timeout: {timeout} seconds\n"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class Warnings(commands.Cog):
|
|||||||
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
||||||
await ctx.send(_("Action {name} has been added.").format(name=name))
|
await ctx.send(_("Action {name} has been added.").format(name=name))
|
||||||
|
|
||||||
@warnaction.command(name="del")
|
@warnaction.command(name="delete", aliases=["del", "remove"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def action_del(self, ctx: commands.Context, action_name: str):
|
async def action_del(self, ctx: commands.Context, action_name: str):
|
||||||
"""Delete the action with the specified name."""
|
"""Delete the action with the specified name."""
|
||||||
@@ -175,7 +175,7 @@ class Warnings(commands.Cog):
|
|||||||
|
|
||||||
await ctx.send(_("The new reason has been registered."))
|
await ctx.send(_("The new reason has been registered."))
|
||||||
|
|
||||||
@warnreason.command(name="del", aliases=["remove"])
|
@warnreason.command(name="delete", aliases=["remove", "del"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def reason_del(self, ctx: commands.Context, reason_name: str):
|
async def reason_del(self, ctx: commands.Context, reason_name: str):
|
||||||
"""Delete a warning reason."""
|
"""Delete a warning reason."""
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from .utils import common_filters
|
|||||||
|
|
||||||
CUSTOM_GROUPS = "CUSTOM_GROUPS"
|
CUSTOM_GROUPS = "CUSTOM_GROUPS"
|
||||||
|
|
||||||
|
log = logging.getLogger("redbot")
|
||||||
|
|
||||||
|
|
||||||
def _is_submodule(parent, child):
|
def _is_submodule(parent, child):
|
||||||
return parent == child or child.startswith(parent + ".")
|
return parent == child or child.startswith(parent + ".")
|
||||||
@@ -52,10 +54,16 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
custom_info=None,
|
custom_info=None,
|
||||||
help__page_char_limit=1000,
|
help__page_char_limit=1000,
|
||||||
help__max_pages_in_guild=2,
|
help__max_pages_in_guild=2,
|
||||||
|
help__use_menus=False,
|
||||||
|
help__show_hidden=False,
|
||||||
|
help__verify_checks=True,
|
||||||
|
help__verify_exists=False,
|
||||||
help__tagline="",
|
help__tagline="",
|
||||||
disabled_commands=[],
|
disabled_commands=[],
|
||||||
disabled_command_msg="That command is disabled.",
|
disabled_command_msg="That command is disabled.",
|
||||||
api_tokens={},
|
api_tokens={},
|
||||||
|
extra_owner_destinations=[],
|
||||||
|
owner_opt_out_list=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.register_guild(
|
self.db.register_guild(
|
||||||
@@ -253,8 +261,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
lib.setup(self)
|
lib.setup(self)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._remove_module_references(lib.__name__)
|
self._remove_module_references(lib.__name__)
|
||||||
self._call_module_finalizers(lib, key)
|
self._call_module_finalizers(lib, name)
|
||||||
raise errors.ExtensionFailed(key, e) from e
|
raise errors.CogLoadError() from e
|
||||||
else:
|
else:
|
||||||
self._BotBase__extensions[name] = lib
|
self._BotBase__extensions[name] = lib
|
||||||
|
|
||||||
@@ -460,6 +468,47 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
ctx.permission_state = commands.PermState.DENIED_BY_HOOK
|
ctx.permission_state = commands.PermState.DENIED_BY_HOOK
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_owner_notification_destinations(self) -> List[discord.abc.Messageable]:
|
||||||
|
"""
|
||||||
|
Gets the users and channels to send to
|
||||||
|
"""
|
||||||
|
destinations = []
|
||||||
|
opt_outs = await self.db.owner_opt_out_list()
|
||||||
|
for user_id in (self.owner_id, *self._co_owners):
|
||||||
|
if user_id not in opt_outs:
|
||||||
|
user = self.get_user(user_id)
|
||||||
|
if user:
|
||||||
|
destinations.append(user)
|
||||||
|
|
||||||
|
channel_ids = await self.db.extra_owner_destinations()
|
||||||
|
for channel_id in channel_ids:
|
||||||
|
channel = self.get_channel(channel_id)
|
||||||
|
if channel:
|
||||||
|
destinations.append(channel)
|
||||||
|
|
||||||
|
return destinations
|
||||||
|
|
||||||
|
async def send_to_owners(self, content=None, **kwargs):
|
||||||
|
"""
|
||||||
|
This sends something to all owners and their configured extra destinations.
|
||||||
|
|
||||||
|
This takes the same arguments as discord.abc.Messageable.send
|
||||||
|
|
||||||
|
This logs failing sends
|
||||||
|
"""
|
||||||
|
destinations = await self.get_owner_notification_destinations()
|
||||||
|
|
||||||
|
async def wrapped_send(location, content=None, **kwargs):
|
||||||
|
try:
|
||||||
|
await location.send(content, **kwargs)
|
||||||
|
except Exception as _exc:
|
||||||
|
log.exception(
|
||||||
|
f"I could not send an owner notification to ({location.id}){location}"
|
||||||
|
)
|
||||||
|
|
||||||
|
sends = [wrapped_send(d, content, **kwargs) for d in destinations]
|
||||||
|
await asyncio.gather(*sends)
|
||||||
|
|
||||||
|
|
||||||
class Red(RedBase, discord.AutoShardedClient):
|
class Red(RedBase, discord.AutoShardedClient):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Context(commands.Context):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **attrs):
|
def __init__(self, **attrs):
|
||||||
|
self.assume_yes = attrs.pop("assume_yes", False)
|
||||||
super().__init__(**attrs)
|
super().__init__(**attrs)
|
||||||
self.permission_state: PermState = PermState.NORMAL
|
self.permission_state: PermState = PermState.NORMAL
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,112 @@
|
|||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING
|
import functools
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import TYPE_CHECKING, Optional, List, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
from discord.ext import commands as dpy_commands
|
||||||
|
|
||||||
from . import BadArgument
|
from . import BadArgument
|
||||||
from ..i18n import Translator
|
from ..i18n import Translator
|
||||||
|
from ..utils.chat_formatting import humanize_timedelta
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
|
||||||
__all__ = ["GuildConverter"]
|
__all__ = [
|
||||||
|
"APIToken",
|
||||||
|
"DictConverter",
|
||||||
|
"GuildConverter",
|
||||||
|
"TimedeltaConverter",
|
||||||
|
"get_dict_converter",
|
||||||
|
"get_timedelta_converter",
|
||||||
|
"parse_timedelta",
|
||||||
|
]
|
||||||
|
|
||||||
_ = Translator("commands.converter", __file__)
|
_ = Translator("commands.converter", __file__)
|
||||||
|
|
||||||
ID_REGEX = re.compile(r"([0-9]{15,21})")
|
ID_REGEX = re.compile(r"([0-9]{15,21})")
|
||||||
|
|
||||||
|
|
||||||
|
# Taken with permission from
|
||||||
|
# https://github.com/mikeshardmind/SinbadCogs/blob/816f3bc2ba860243f75112904b82009a8a9e1f99/scheduler/time_utils.py#L9-L19
|
||||||
|
TIME_RE_STRING = r"\s?".join(
|
||||||
|
[
|
||||||
|
r"((?P<weeks>\d+?)\s?(weeks?|w))?",
|
||||||
|
r"((?P<days>\d+?)\s?(days?|d))?",
|
||||||
|
r"((?P<hours>\d+?)\s?(hours?|hrs|hr?))?",
|
||||||
|
r"((?P<minutes>\d+?)\s?(minutes?|mins?|m(?!o)))?", # prevent matching "months"
|
||||||
|
r"((?P<seconds>\d+?)\s?(seconds?|secs?|s))?",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
TIME_RE = re.compile(TIME_RE_STRING, re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timedelta(
|
||||||
|
argument: str,
|
||||||
|
*,
|
||||||
|
maximum: Optional[timedelta] = None,
|
||||||
|
minimum: Optional[timedelta] = None,
|
||||||
|
allowed_units: Optional[List[str]] = None,
|
||||||
|
) -> Optional[timedelta]:
|
||||||
|
"""
|
||||||
|
This converts a user provided string into a timedelta
|
||||||
|
|
||||||
|
The units should be in order from largest to smallest.
|
||||||
|
This works with or without whitespace.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
argument : str
|
||||||
|
The user provided input
|
||||||
|
maximum : Optional[timedelta]
|
||||||
|
If provided, any parsed value higher than this will raise an exception
|
||||||
|
minimum : Optional[timedelta]
|
||||||
|
If provided, any parsed value lower than this will raise an exception
|
||||||
|
allowed_units : Optional[List[str]]
|
||||||
|
If provided, you can constrain a user to expressing the amount of time
|
||||||
|
in specific units. The units you can chose to provide are the same as the
|
||||||
|
parser understands. `weeks` `days` `hours` `minutes` `seconds`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[timedelta]
|
||||||
|
If matched, the timedelta which was parsed. This can return `None`
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
BadArgument
|
||||||
|
If the argument passed uses a unit not allowed, but understood
|
||||||
|
or if the value is out of bounds.
|
||||||
|
"""
|
||||||
|
matches = TIME_RE.match(argument)
|
||||||
|
allowed_units = allowed_units or ["weeks", "days", "hours", "minutes", "seconds"]
|
||||||
|
if matches:
|
||||||
|
params = {k: int(v) for k, v in matches.groupdict().items() if v is not None}
|
||||||
|
for k in params.keys():
|
||||||
|
if k not in allowed_units:
|
||||||
|
raise BadArgument(
|
||||||
|
_("`{unit}` is not a valid unit of time for this command").format(unit=k)
|
||||||
|
)
|
||||||
|
if params:
|
||||||
|
delta = timedelta(**params)
|
||||||
|
if maximum and maximum < delta:
|
||||||
|
raise BadArgument(
|
||||||
|
_(
|
||||||
|
"This amount of time is too large for this command. (maximum: {maximum})"
|
||||||
|
).format(maximum=humanize_timedelta(timedelta=maximum))
|
||||||
|
)
|
||||||
|
if minimum and delta < minimum:
|
||||||
|
raise BadArgument(
|
||||||
|
_(
|
||||||
|
"This amount of time is too small for this command. (minimum: {minimum})"
|
||||||
|
).format(minimum=humanize_timedelta(timedelta=minimum))
|
||||||
|
)
|
||||||
|
return delta
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class GuildConverter(discord.Guild):
|
class GuildConverter(discord.Guild):
|
||||||
"""Converts to a `discord.Guild` object.
|
"""Converts to a `discord.Guild` object.
|
||||||
|
|
||||||
@@ -47,16 +138,20 @@ class APIToken(discord.ext.commands.Converter):
|
|||||||
This will parse the input argument separating the key value pairs into a
|
This will parse the input argument separating the key value pairs into a
|
||||||
format to be used for the core bots API token storage.
|
format to be used for the core bots API token storage.
|
||||||
|
|
||||||
This will split the argument by either `;` or `,` and return a dict
|
This will split the argument by either `;` ` `, or `,` and return a dict
|
||||||
to be stored. Since all API's are different and have different naming convention,
|
to be stored. Since all API's are different and have different naming convention,
|
||||||
this leaves the onus on the cog creator to clearly define how to setup the correct
|
this leaves the onus on the cog creator to clearly define how to setup the correct
|
||||||
credential names for their cogs.
|
credential names for their cogs.
|
||||||
|
|
||||||
|
Note: Core usage of this has been replaced with DictConverter use instead.
|
||||||
|
|
||||||
|
This may be removed at a later date (with warning)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx, argument) -> dict:
|
async def convert(self, ctx, argument) -> dict:
|
||||||
bot = ctx.bot
|
bot = ctx.bot
|
||||||
result = {}
|
result = {}
|
||||||
match = re.split(r";|,", argument)
|
match = re.split(r";|,| ", argument)
|
||||||
# provide two options to split incase for whatever reason one is part of the api key we're using
|
# provide two options to split incase for whatever reason one is part of the api key we're using
|
||||||
if len(match) > 1:
|
if len(match) > 1:
|
||||||
result[match[0]] = "".join(r for r in match[1:])
|
result[match[0]] = "".join(r for r in match[1:])
|
||||||
@@ -65,3 +160,125 @@ class APIToken(discord.ext.commands.Converter):
|
|||||||
if not result:
|
if not result:
|
||||||
raise BadArgument(_("The provided tokens are not in a valid format."))
|
raise BadArgument(_("The provided tokens are not in a valid format."))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class DictConverter(dpy_commands.Converter):
|
||||||
|
"""
|
||||||
|
Converts pairs of space seperated values to a dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *expected_keys: str, delims: Optional[List[str]] = None):
|
||||||
|
self.expected_keys = expected_keys
|
||||||
|
self.delims = delims or [" "]
|
||||||
|
self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
|
||||||
|
|
||||||
|
async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
|
||||||
|
|
||||||
|
ret: Dict[str, str] = {}
|
||||||
|
args = self.pattern.split(argument)
|
||||||
|
|
||||||
|
if len(args) % 2 != 0:
|
||||||
|
raise BadArgument()
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
ret[key] = next(iterator)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> type:
|
||||||
|
"""
|
||||||
|
Returns a typechecking safe `DictConverter` suitable for use with discord.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PartialMeta(type(DictConverter)):
|
||||||
|
__call__ = functools.partialmethod(
|
||||||
|
type(DictConverter).__call__, *expected_keys, delims=delims
|
||||||
|
)
|
||||||
|
|
||||||
|
class ValidatedConverter(DictConverter, metaclass=PartialMeta):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ValidatedConverter
|
||||||
|
|
||||||
|
|
||||||
|
class TimedeltaConverter(dpy_commands.Converter):
|
||||||
|
"""
|
||||||
|
This is a converter for timedeltas.
|
||||||
|
The units should be in order from largest to smallest.
|
||||||
|
This works with or without whitespace.
|
||||||
|
|
||||||
|
See `parse_timedelta` for more information about how this functions.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
maximum : Optional[timedelta]
|
||||||
|
If provided, any parsed value higher than this will raise an exception
|
||||||
|
minimum : Optional[timedelta]
|
||||||
|
If provided, any parsed value lower than this will raise an exception
|
||||||
|
allowed_units : Optional[List[str]]
|
||||||
|
If provided, you can constrain a user to expressing the amount of time
|
||||||
|
in specific units. The units you can chose to provide are the same as the
|
||||||
|
parser understands. `weeks` `days` `hours` `minutes` `seconds`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, minimum=None, maximum=None, allowed_units=None):
|
||||||
|
self.allowed_units = allowed_units
|
||||||
|
self.minimum = minimum
|
||||||
|
self.maximum = maximum
|
||||||
|
|
||||||
|
async def convert(self, ctx: "Context", argument: str) -> timedelta:
|
||||||
|
delta = parse_timedelta(
|
||||||
|
argument, minimum=self.minimum, maximum=self.maximum, allowed_units=self.allowed_units
|
||||||
|
)
|
||||||
|
if delta is not None:
|
||||||
|
return delta
|
||||||
|
raise BadArgument() # This allows this to be a required argument.
|
||||||
|
|
||||||
|
|
||||||
|
def get_timedelta_converter(
|
||||||
|
*,
|
||||||
|
maximum: Optional[timedelta] = None,
|
||||||
|
minimum: Optional[timedelta] = None,
|
||||||
|
allowed_units: Optional[List[str]] = None,
|
||||||
|
) -> type:
|
||||||
|
"""
|
||||||
|
This creates a type suitable for typechecking which works with discord.py's
|
||||||
|
commands.
|
||||||
|
|
||||||
|
See `parse_timedelta` for more information about how this functions.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
maximum : Optional[timedelta]
|
||||||
|
If provided, any parsed value higher than this will raise an exception
|
||||||
|
minimum : Optional[timedelta]
|
||||||
|
If provided, any parsed value lower than this will raise an exception
|
||||||
|
allowed_units : Optional[List[str]]
|
||||||
|
If provided, you can constrain a user to expressing the amount of time
|
||||||
|
in specific units. The units you can chose to provide are the same as the
|
||||||
|
parser understands. `weeks` `days` `hours` `minutes` `seconds`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
type
|
||||||
|
The converter class, which will be a subclass of `TimedeltaConverter`
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PartialMeta(type(TimedeltaConverter)):
|
||||||
|
__call__ = functools.partialmethod(
|
||||||
|
type(DictConverter).__call__,
|
||||||
|
allowed_units=allowed_units,
|
||||||
|
minimum=minimum,
|
||||||
|
maximum=maximum,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ValidatedConverter(TimedeltaConverter, metaclass=PartialMeta):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ValidatedConverter
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
# Additionally, this gives our users a bit more customization options including by
|
# Additionally, this gives our users a bit more customization options including by
|
||||||
# 3rd party cogs down the road.
|
# 3rd party cogs down the road.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Union, List, AsyncIterator, Iterable, cast
|
from typing import Union, List, AsyncIterator, Iterable, cast
|
||||||
|
|
||||||
@@ -77,14 +78,6 @@ class RedHelpFormatter:
|
|||||||
should not need or want a shared state.
|
should not need or want a shared state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Class vars for things which should be configurable at a later date but aren't now
|
|
||||||
# Technically, someone can just use a cog to switch these in real time for now.
|
|
||||||
|
|
||||||
USE_MENU = False
|
|
||||||
CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES = False
|
|
||||||
SHOW_HIDDEN = False
|
|
||||||
VERIFY_CHECKS = True
|
|
||||||
|
|
||||||
async def send_help(self, ctx: Context, help_for: HelpTarget = None):
|
async def send_help(self, ctx: Context, help_for: HelpTarget = None):
|
||||||
"""
|
"""
|
||||||
This delegates to other functions.
|
This delegates to other functions.
|
||||||
@@ -102,7 +95,7 @@ class RedHelpFormatter:
|
|||||||
await self.command_not_found(ctx, help_for)
|
await self.command_not_found(ctx, help_for)
|
||||||
return
|
return
|
||||||
except NoSubCommand as exc:
|
except NoSubCommand as exc:
|
||||||
if self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES:
|
if await ctx.bot.db.help.verify_exists():
|
||||||
await self.subcommand_not_found(ctx, exc.last, exc.not_found)
|
await self.subcommand_not_found(ctx, exc.last, exc.not_found)
|
||||||
return
|
return
|
||||||
help_for = exc.last
|
help_for = exc.last
|
||||||
@@ -138,7 +131,7 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
async def format_command_help(self, ctx: Context, obj: commands.Command):
|
async def format_command_help(self, ctx: Context, obj: commands.Command):
|
||||||
|
|
||||||
send = self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES
|
send = await ctx.bot.db.help.verify_exists()
|
||||||
if not send:
|
if not send:
|
||||||
async for _ in self.help_filter_func(ctx, (obj,), bypass_hidden=True):
|
async for _ in self.help_filter_func(ctx, (obj,), bypass_hidden=True):
|
||||||
# This is a really lazy option for not
|
# This is a really lazy option for not
|
||||||
@@ -174,7 +167,7 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
if command.help:
|
if command.help:
|
||||||
splitted = command.help.split("\n\n")
|
splitted = command.help.split("\n\n")
|
||||||
name = "__{0}__".format(splitted[0])
|
name = splitted[0]
|
||||||
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
||||||
if not value:
|
if not value:
|
||||||
value = EMPTY_STRING
|
value = EMPTY_STRING
|
||||||
@@ -182,8 +175,14 @@ class RedHelpFormatter:
|
|||||||
emb["fields"].append(field)
|
emb["fields"].append(field)
|
||||||
|
|
||||||
if subcommands:
|
if subcommands:
|
||||||
|
|
||||||
|
def shorten_line(a_line: str) -> str:
|
||||||
|
if len(a_line) < 70: # embed max width needs to be lower
|
||||||
|
return a_line
|
||||||
|
return a_line[:67] + "..."
|
||||||
|
|
||||||
subtext = "\n".join(
|
subtext = "\n".join(
|
||||||
f"**{name}** {command.short_doc}"
|
shorten_line(f"**{name}** {command.short_doc}")
|
||||||
for name, command in sorted(subcommands.items())
|
for name, command in sorted(subcommands.items())
|
||||||
)
|
)
|
||||||
for i, page in enumerate(pagify(subtext, page_length=1000, shorten_by=0)):
|
for i, page in enumerate(pagify(subtext, page_length=1000, shorten_by=0)):
|
||||||
@@ -208,7 +207,7 @@ class RedHelpFormatter:
|
|||||||
doc_max_width = 80 - max_width
|
doc_max_width = 80 - max_width
|
||||||
for nm, com in sorted(cmds):
|
for nm, com in sorted(cmds):
|
||||||
width_gap = discord.utils._string_width(nm) - len(nm)
|
width_gap = discord.utils._string_width(nm) - len(nm)
|
||||||
doc = command.short_doc
|
doc = com.short_doc
|
||||||
if len(doc) > doc_max_width:
|
if len(doc) > doc_max_width:
|
||||||
doc = doc[: doc_max_width - 3] + "..."
|
doc = doc[: doc_max_width - 3] + "..."
|
||||||
yield nm, doc, max_width - width_gap
|
yield nm, doc, max_width - width_gap
|
||||||
@@ -251,6 +250,12 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
author_info = {"name": f"{ctx.me.display_name} Help Menu", "icon_url": ctx.me.avatar_url}
|
author_info = {"name": f"{ctx.me.display_name} Help Menu", "icon_url": ctx.me.avatar_url}
|
||||||
|
|
||||||
|
if not field_groups: # This can happen on single command without a docstring
|
||||||
|
embed = discord.Embed(color=color, **embed_dict["embed"])
|
||||||
|
embed.set_author(**author_info)
|
||||||
|
embed.set_footer(**embed_dict["footer"])
|
||||||
|
pages.append(embed)
|
||||||
|
|
||||||
for i, group in enumerate(field_groups, 1):
|
for i, group in enumerate(field_groups, 1):
|
||||||
embed = discord.Embed(color=color, **embed_dict["embed"])
|
embed = discord.Embed(color=color, **embed_dict["embed"])
|
||||||
|
|
||||||
@@ -271,8 +276,8 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
async def format_cog_help(self, ctx: Context, obj: commands.Cog):
|
async def format_cog_help(self, ctx: Context, obj: commands.Cog):
|
||||||
|
|
||||||
commands = await self.get_cog_help_mapping(ctx, obj)
|
coms = await self.get_cog_help_mapping(ctx, obj)
|
||||||
if not (commands or self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES):
|
if not (coms or await ctx.bot.db.help.verify_exists()):
|
||||||
return
|
return
|
||||||
|
|
||||||
description = obj.help
|
description = obj.help
|
||||||
@@ -283,11 +288,24 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
emb["footer"]["text"] = tagline
|
emb["footer"]["text"] = tagline
|
||||||
if description:
|
if description:
|
||||||
emb["embed"]["title"] = f"*{description[:2044]}*"
|
splitted = description.split("\n\n")
|
||||||
|
name = splitted[0]
|
||||||
|
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
||||||
|
if not value:
|
||||||
|
value = EMPTY_STRING
|
||||||
|
field = EmbedField(name[:252], value[:1024], False)
|
||||||
|
emb["fields"].append(field)
|
||||||
|
|
||||||
|
if coms:
|
||||||
|
|
||||||
|
def shorten_line(a_line: str) -> str:
|
||||||
|
if len(a_line) < 70: # embed max width needs to be lower
|
||||||
|
return a_line
|
||||||
|
return a_line[:67] + "..."
|
||||||
|
|
||||||
if commands:
|
|
||||||
command_text = "\n".join(
|
command_text = "\n".join(
|
||||||
f"**{name}** {command.short_doc}" for name, command in sorted(commands.items())
|
shorten_line(f"**{name}** {command.short_doc}")
|
||||||
|
for name, command in sorted(coms.items())
|
||||||
)
|
)
|
||||||
for i, page in enumerate(pagify(command_text, page_length=1000, shorten_by=0)):
|
for i, page in enumerate(pagify(command_text, page_length=1000, shorten_by=0)):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
@@ -300,11 +318,11 @@ class RedHelpFormatter:
|
|||||||
await self.make_and_send_embeds(ctx, emb)
|
await self.make_and_send_embeds(ctx, emb)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
commands_text = None
|
subtext = None
|
||||||
commands_header = None
|
subtext_header = None
|
||||||
if commands:
|
if coms:
|
||||||
subtext_header = "Commands:"
|
subtext_header = "Commands:"
|
||||||
max_width = max(discord.utils._string_width(name) for name in commands.keys())
|
max_width = max(discord.utils._string_width(name) for name in coms.keys())
|
||||||
|
|
||||||
def width_maker(cmds):
|
def width_maker(cmds):
|
||||||
doc_max_width = 80 - max_width
|
doc_max_width = 80 - max_width
|
||||||
@@ -316,20 +334,17 @@ class RedHelpFormatter:
|
|||||||
yield nm, doc, max_width - width_gap
|
yield nm, doc, max_width - width_gap
|
||||||
|
|
||||||
subtext = "\n".join(
|
subtext = "\n".join(
|
||||||
f" {name:<{width}} {doc}"
|
f" {name:<{width}} {doc}" for name, doc, width in width_maker(coms.items())
|
||||||
for name, doc, width in width_maker(commands.items())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
to_page = "\n\n".join(
|
to_page = "\n\n".join(filter(None, (description, subtext_header, subtext)))
|
||||||
filter(None, (description, signature[1:-1], subtext_header, subtext))
|
|
||||||
)
|
|
||||||
pages = [box(p) for p in pagify(to_page)]
|
pages = [box(p) for p in pagify(to_page)]
|
||||||
await self.send_pages(ctx, pages, embed=False)
|
await self.send_pages(ctx, pages, embed=False)
|
||||||
|
|
||||||
async def format_bot_help(self, ctx: Context):
|
async def format_bot_help(self, ctx: Context):
|
||||||
|
|
||||||
commands = await self.get_bot_help_mapping(ctx)
|
coms = await self.get_bot_help_mapping(ctx)
|
||||||
if not commands:
|
if not coms:
|
||||||
return
|
return
|
||||||
|
|
||||||
description = ctx.bot.description or ""
|
description = ctx.bot.description or ""
|
||||||
@@ -343,15 +358,21 @@ class RedHelpFormatter:
|
|||||||
if description:
|
if description:
|
||||||
emb["embed"]["title"] = f"*{description[:2044]}*"
|
emb["embed"]["title"] = f"*{description[:2044]}*"
|
||||||
|
|
||||||
for cog_name, data in commands:
|
for cog_name, data in coms:
|
||||||
|
|
||||||
if cog_name:
|
if cog_name:
|
||||||
title = f"**__{cog_name}:__**"
|
title = f"**__{cog_name}:__**"
|
||||||
else:
|
else:
|
||||||
title = f"**__No Category:__**"
|
title = f"**__No Category:__**"
|
||||||
|
|
||||||
|
def shorten_line(a_line: str) -> str:
|
||||||
|
if len(a_line) < 70: # embed max width needs to be lower
|
||||||
|
return a_line
|
||||||
|
return a_line[:67] + "..."
|
||||||
|
|
||||||
cog_text = "\n".join(
|
cog_text = "\n".join(
|
||||||
f"**{name}** {command.short_doc}" for name, command in sorted(data.items())
|
shorten_line(f"**{name}** {command.short_doc}")
|
||||||
|
for name, command in sorted(data.items())
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)):
|
for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)):
|
||||||
@@ -362,11 +383,12 @@ class RedHelpFormatter:
|
|||||||
await self.make_and_send_embeds(ctx, emb)
|
await self.make_and_send_embeds(ctx, emb)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
to_join = []
|
||||||
if description:
|
if description:
|
||||||
to_join = [f"{description}\n"]
|
to_join.append(f"{description}\n")
|
||||||
|
|
||||||
names = []
|
names = []
|
||||||
for k, v in commands:
|
for k, v in coms:
|
||||||
names.extend(list(v.name for v in v.values()))
|
names.extend(list(v.name for v in v.values()))
|
||||||
|
|
||||||
max_width = max(
|
max_width = max(
|
||||||
@@ -382,7 +404,7 @@ class RedHelpFormatter:
|
|||||||
doc = doc[: doc_max_width - 3] + "..."
|
doc = doc[: doc_max_width - 3] + "..."
|
||||||
yield nm, doc, max_width - width_gap
|
yield nm, doc, max_width - width_gap
|
||||||
|
|
||||||
for cog_name, data in commands:
|
for cog_name, data in coms:
|
||||||
|
|
||||||
title = f"{cog_name}:" if cog_name else "No Category:"
|
title = f"{cog_name}:" if cog_name else "No Category:"
|
||||||
to_join.append(title)
|
to_join.append(title)
|
||||||
@@ -401,17 +423,25 @@ class RedHelpFormatter:
|
|||||||
"""
|
"""
|
||||||
This does most of actual filtering.
|
This does most of actual filtering.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
show_hidden = bypass_hidden or await ctx.bot.db.help.show_hidden()
|
||||||
|
verify_checks = await ctx.bot.db.help.verify_checks()
|
||||||
|
|
||||||
# TODO: Settings for this in core bot db
|
# TODO: Settings for this in core bot db
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
if self.VERIFY_CHECKS and not (self.SHOW_HIDDEN or bypass_hidden):
|
if verify_checks and not show_hidden:
|
||||||
# Default Red behavior, can_see includes a can_run check.
|
# Default Red behavior, can_see includes a can_run check.
|
||||||
if await obj.can_see(ctx):
|
if await obj.can_see(ctx):
|
||||||
yield obj
|
yield obj
|
||||||
elif self.VERIFY_CHECKS:
|
elif verify_checks:
|
||||||
if await obj.can_run(ctx):
|
try:
|
||||||
|
can_run = await obj.can_run(ctx)
|
||||||
|
except discord.DiscordException:
|
||||||
|
can_run = False
|
||||||
|
if can_run:
|
||||||
yield obj
|
yield obj
|
||||||
elif not (self.SHOW_HIDDEN or bypass_hidden):
|
elif not show_hidden:
|
||||||
if getattr(obj, "hidden", False): # Cog compatibility
|
if not getattr(obj, "hidden", False): # Cog compatibility
|
||||||
yield obj
|
yield obj
|
||||||
else:
|
else:
|
||||||
yield obj
|
yield obj
|
||||||
@@ -426,17 +456,17 @@ class RedHelpFormatter:
|
|||||||
if fuzzy_commands:
|
if fuzzy_commands:
|
||||||
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
ret.set_author()
|
ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
||||||
tagline = (await ctx.bot.db.help.tagline()) or self.get_default_tagline(ctx)
|
tagline = (await ctx.bot.db.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
ret.set_footer(text=tagline)
|
ret.set_footer(text=tagline)
|
||||||
await ctx.send(embed=ret)
|
await ctx.send(embed=ret)
|
||||||
else:
|
else:
|
||||||
await ctx.send(ret)
|
await ctx.send(ret)
|
||||||
elif self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES:
|
elif await ctx.bot.db.help.verify_exists():
|
||||||
ret = T_("Command *{command_name}* not found.").format(command_name=command_name)
|
ret = T_("Help topic for *{command_name}* not found.").format(command_name=help_for)
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
emb = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
||||||
emb.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
||||||
tagline = (await ctx.bot.db.help.tagline()) or self.get_default_tagline(ctx)
|
tagline = (await ctx.bot.db.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
ret.set_footer(text=tagline)
|
ret.set_footer(text=tagline)
|
||||||
await ctx.send(embed=ret)
|
await ctx.send(embed=ret)
|
||||||
@@ -447,10 +477,17 @@ class RedHelpFormatter:
|
|||||||
"""
|
"""
|
||||||
Sends an error
|
Sends an error
|
||||||
"""
|
"""
|
||||||
ret = T_("Command *{command_name}* has no subcommands.").format(
|
ret = T_("Command *{command_name}* has no subcommand named *{not_found}*.").format(
|
||||||
command_name=command.qualified_name
|
command_name=command.qualified_name, not_found=not_found[0]
|
||||||
)
|
)
|
||||||
await ctx.send(ret)
|
if await ctx.embed_requested():
|
||||||
|
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
||||||
|
ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
||||||
|
tagline = (await ctx.bot.db.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
|
ret.set_footer(text=tagline)
|
||||||
|
await ctx.send(embed=ret)
|
||||||
|
else:
|
||||||
|
await ctx.send(ret)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_command(ctx, help_for: str):
|
def parse_command(ctx, help_for: str):
|
||||||
@@ -489,19 +526,43 @@ class RedHelpFormatter:
|
|||||||
Sends pages based on settings.
|
Sends pages based on settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.USE_MENU:
|
if not (
|
||||||
|
ctx.channel.permissions_for(ctx.me).add_reactions and await ctx.bot.db.help.use_menus()
|
||||||
|
):
|
||||||
|
|
||||||
max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild()
|
max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild()
|
||||||
destination = ctx.author if len(pages) > max_pages_in_guild else ctx
|
destination = ctx.author if len(pages) > max_pages_in_guild else ctx
|
||||||
|
|
||||||
if embed:
|
if embed:
|
||||||
for page in pages:
|
for page in pages:
|
||||||
await destination.send(embed=page)
|
try:
|
||||||
|
await destination.send(embed=page)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(
|
||||||
|
T_(
|
||||||
|
"I couldn't send the help message to you in DM. "
|
||||||
|
"Either you blocked me or you disabled DMs in this server."
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for page in pages:
|
for page in pages:
|
||||||
await destination.send(page)
|
try:
|
||||||
|
await destination.send(page)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(
|
||||||
|
T_(
|
||||||
|
"I couldn't send the help message to you in DM. "
|
||||||
|
"Either you blocked me or you disabled DMs in this server."
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS)
|
# Specifically ensuring the menu's message is sent prior to returning
|
||||||
|
m = await (ctx.send(embed=pages[0]) if embed else ctx.send(pages[0]))
|
||||||
|
c = menus.DEFAULT_CONTROLS if len(pages) > 1 else {"\N{CROSS MARK}": menus.close_menu}
|
||||||
|
# Allow other things to happen during menu timeout/interaction.
|
||||||
|
asyncio.create_task(menus.menu(ctx, pages, c, message=m))
|
||||||
|
# menu needs reactions added manually since we fed it a messsage
|
||||||
|
menus.start_adding_reactions(m, c.keys())
|
||||||
|
|
||||||
|
|
||||||
@commands.command(name="help", hidden=True, i18n=T_)
|
@commands.command(name="help", hidden=True, i18n=T_)
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
import platform
|
||||||
|
import getpass
|
||||||
|
import pip
|
||||||
import tarfile
|
import tarfile
|
||||||
import traceback
|
import traceback
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@@ -30,7 +33,7 @@ from redbot.core import (
|
|||||||
i18n,
|
i18n,
|
||||||
)
|
)
|
||||||
from .utils.predicates import MessagePredicate
|
from .utils.predicates import MessagePredicate
|
||||||
from .utils.chat_formatting import pagify, box, inline
|
from .utils.chat_formatting import humanize_timedelta, pagify, box, inline
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
@@ -42,6 +45,8 @@ log = logging.getLogger("red")
|
|||||||
|
|
||||||
_ = i18n.Translator("Core", __file__)
|
_ = i18n.Translator("Core", __file__)
|
||||||
|
|
||||||
|
TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"])
|
||||||
|
|
||||||
|
|
||||||
class CoreLogic:
|
class CoreLogic:
|
||||||
def __init__(self, bot: "Red"):
|
def __init__(self, bot: "Red"):
|
||||||
@@ -328,28 +333,12 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
async def uptime(self, ctx: commands.Context):
|
async def uptime(self, ctx: commands.Context):
|
||||||
"""Shows Red's uptime"""
|
"""Shows Red's uptime"""
|
||||||
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
passed = self.get_bot_uptime()
|
delta = datetime.datetime.utcnow() - self.bot.uptime
|
||||||
await ctx.send(_("Been up for: **{}** (since {} UTC)").format(passed, since))
|
await ctx.send(
|
||||||
|
_("Been up for: **{}** (since {} UTC)").format(
|
||||||
def get_bot_uptime(self, *, brief: bool = False):
|
humanize_timedelta(timedelta=delta), since
|
||||||
# Courtesy of Danny
|
)
|
||||||
now = datetime.datetime.utcnow()
|
)
|
||||||
delta = now - self.bot.uptime
|
|
||||||
hours, remainder = divmod(int(delta.total_seconds()), 3600)
|
|
||||||
minutes, seconds = divmod(remainder, 60)
|
|
||||||
days, hours = divmod(hours, 24)
|
|
||||||
|
|
||||||
if not brief:
|
|
||||||
if days:
|
|
||||||
fmt = _("{d} days, {h} hours, {m} minutes, and {s} seconds")
|
|
||||||
else:
|
|
||||||
fmt = _("{h} hours, {m} minutes, and {s} seconds")
|
|
||||||
else:
|
|
||||||
fmt = _("{h}h {m}m {s}s")
|
|
||||||
if days:
|
|
||||||
fmt = _("{d}d ") + fmt
|
|
||||||
|
|
||||||
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
async def embedset(self, ctx: commands.Context):
|
async def embedset(self, ctx: commands.Context):
|
||||||
@@ -529,6 +518,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""Loads packages"""
|
"""Loads packages"""
|
||||||
if not cogs:
|
if not cogs:
|
||||||
return await ctx.send_help()
|
return await ctx.send_help()
|
||||||
|
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
loaded, failed, not_found, already_loaded, failed_with_reason = await self._load(cogs)
|
loaded, failed, not_found, already_loaded, failed_with_reason = await self._load(cogs)
|
||||||
|
|
||||||
@@ -571,6 +561,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""Unloads packages"""
|
"""Unloads packages"""
|
||||||
if not cogs:
|
if not cogs:
|
||||||
return await ctx.send_help()
|
return await ctx.send_help()
|
||||||
|
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
|
||||||
unloaded, failed = await self._unload(cogs)
|
unloaded, failed = await self._unload(cogs)
|
||||||
|
|
||||||
if unloaded:
|
if unloaded:
|
||||||
@@ -589,6 +580,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""Reloads packages"""
|
"""Reloads packages"""
|
||||||
if not cogs:
|
if not cogs:
|
||||||
return await ctx.send_help()
|
return await ctx.send_help()
|
||||||
|
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
loaded, failed, not_found, already_loaded, failed_with_reason = await self._reload(
|
loaded, failed, not_found, already_loaded, failed_with_reason = await self._reload(
|
||||||
cogs
|
cogs
|
||||||
@@ -1057,7 +1049,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
@_set.command()
|
@_set.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def api(self, ctx: commands.Context, service: str, *tokens: commands.converter.APIToken):
|
async def api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter):
|
||||||
"""Set various external API tokens.
|
"""Set 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.
|
||||||
@@ -1070,8 +1062,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
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()
|
||||||
entry = {k: v for t in tokens for k, v in t.items()}
|
await ctx.bot.db.api_tokens.set_raw(service, value=tokens)
|
||||||
await ctx.bot.db.api_tokens.set_raw(service, value=entry)
|
|
||||||
await ctx.send(_("`{service}` API tokens have been set.").format(service=service))
|
await ctx.send(_("`{service}` API tokens have been set.").format(service=service))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@@ -1080,6 +1071,80 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""Manage settings for the help command."""
|
"""Manage settings for the help command."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@helpset.command(name="usemenus")
|
||||||
|
async def helpset_usemenus(self, ctx: commands.Context, use_menus: bool = None):
|
||||||
|
"""
|
||||||
|
Allows the help command to be sent as a paginated menu instead of seperate
|
||||||
|
messages.
|
||||||
|
|
||||||
|
This defaults to False.
|
||||||
|
Using this without a setting will toggle.
|
||||||
|
"""
|
||||||
|
if use_menus is None:
|
||||||
|
use_menus = not await ctx.bot.db.help.use_menus()
|
||||||
|
await ctx.bot.db.help.use_menus.set(use_menus)
|
||||||
|
if use_menus:
|
||||||
|
await ctx.send(_("Help will use menus."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Help will not use menus."))
|
||||||
|
|
||||||
|
@helpset.command(name="showhidden")
|
||||||
|
async def helpset_showhidden(self, ctx: commands.Context, show_hidden: bool = None):
|
||||||
|
"""
|
||||||
|
This allows the help command to show hidden commands
|
||||||
|
|
||||||
|
This defaults to False.
|
||||||
|
Using this without a setting will toggle.
|
||||||
|
"""
|
||||||
|
if show_hidden is None:
|
||||||
|
show_hidden = not await ctx.bot.db.help.show_hidden()
|
||||||
|
await ctx.bot.db.help.show_hidden.set(show_hidden)
|
||||||
|
if show_hidden:
|
||||||
|
await ctx.send(_("Help will not filter hidden commands"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Help will filter hidden commands."))
|
||||||
|
|
||||||
|
@helpset.command(name="verifychecks")
|
||||||
|
async def helpset_permfilter(self, ctx: commands.Context, verify: bool = None):
|
||||||
|
"""
|
||||||
|
Sets if commands which can't be run in the current context should be
|
||||||
|
filtered from help
|
||||||
|
|
||||||
|
Defaults to True.
|
||||||
|
Using this without a setting will toggle.
|
||||||
|
"""
|
||||||
|
if verify is None:
|
||||||
|
verify = not await ctx.bot.db.help.verify_checks()
|
||||||
|
await ctx.bot.db.help.verify_checks.set(verify)
|
||||||
|
if verify:
|
||||||
|
await ctx.send(_("Help will only show for commands which can be run."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Help will show up without checking if the commands can be run."))
|
||||||
|
|
||||||
|
@helpset.command(name="verifyexists")
|
||||||
|
async def helpset_verifyexists(self, ctx: commands.Context, verify: bool = None):
|
||||||
|
"""
|
||||||
|
This allows the bot to respond indicating the existence of a specific
|
||||||
|
help topic even if the user can't use it.
|
||||||
|
|
||||||
|
Note: This setting on it's own does not fully prevent command enumeration.
|
||||||
|
|
||||||
|
Defaults to False.
|
||||||
|
Using this without a setting will toggle.
|
||||||
|
"""
|
||||||
|
if verify is None:
|
||||||
|
verify = not await ctx.bot.db.help.verify_exists()
|
||||||
|
await ctx.bot.db.help.verify_exists.set(verify)
|
||||||
|
if verify:
|
||||||
|
await ctx.send(_("Help will verify the existence of help topics."))
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Help will only verify the existence of "
|
||||||
|
"help topics via fuzzy help (if enabled)."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@helpset.command(name="pagecharlimit")
|
@helpset.command(name="pagecharlimit")
|
||||||
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
|
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
|
||||||
"""Set the character limit for each page in the help message.
|
"""Set the character limit for each page in the help message.
|
||||||
@@ -1270,7 +1335,6 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
async def contact(self, ctx: commands.Context, *, message: str):
|
async def contact(self, ctx: commands.Context, *, message: str):
|
||||||
"""Sends a message to the owner"""
|
"""Sends a message to the owner"""
|
||||||
guild = ctx.message.guild
|
guild = ctx.message.guild
|
||||||
owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
|
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
footer = _("User ID: {}").format(author.id)
|
footer = _("User ID: {}").format(author.id)
|
||||||
|
|
||||||
@@ -1291,41 +1355,81 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
description = _("Sent by {} {}").format(author, source)
|
description = _("Sent by {} {}").format(author, source)
|
||||||
|
|
||||||
if isinstance(author, discord.Member):
|
destinations = await ctx.bot.get_owner_notification_destinations()
|
||||||
colour = author.colour
|
|
||||||
else:
|
|
||||||
colour = discord.Colour.red()
|
|
||||||
|
|
||||||
if await ctx.embed_requested():
|
if not destinations:
|
||||||
e = discord.Embed(colour=colour, description=message)
|
await ctx.send(_("I've been configured not to send this anywhere."))
|
||||||
if author.avatar_url:
|
return
|
||||||
e.set_author(name=description, icon_url=author.avatar_url)
|
|
||||||
else:
|
|
||||||
e.set_author(name=description)
|
|
||||||
e.set_footer(text=footer)
|
|
||||||
|
|
||||||
try:
|
successful = False
|
||||||
await owner.send(content, embed=e)
|
|
||||||
except discord.InvalidArgument:
|
for destination in destinations:
|
||||||
await ctx.send(
|
|
||||||
_("I cannot send your message, I'm unable to find my owner... *sigh*")
|
is_dm = isinstance(destination, discord.User)
|
||||||
)
|
send_embed = None
|
||||||
except discord.HTTPException:
|
|
||||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
if is_dm:
|
||||||
|
send_embed = await ctx.bot.db.user(destination).embeds()
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Your message has been sent."))
|
if not destination.permissions_for(destination.guild.me).send_messages:
|
||||||
|
continue
|
||||||
|
if destination.permissions_for(destination.guild.me).embed_links:
|
||||||
|
send_embed = await ctx.bot.db.guild(destination.guild).embeds()
|
||||||
|
else:
|
||||||
|
send_embed = False
|
||||||
|
|
||||||
|
if send_embed is None:
|
||||||
|
send_embed = await ctx.bot.db.embeds()
|
||||||
|
|
||||||
|
if send_embed:
|
||||||
|
|
||||||
|
if not is_dm and await self.bot.db.guild(destination.guild).use_bot_color():
|
||||||
|
color = destination.guild.me.color
|
||||||
|
else:
|
||||||
|
color = ctx.bot.color
|
||||||
|
|
||||||
|
e = discord.Embed(colour=color, description=message)
|
||||||
|
if author.avatar_url:
|
||||||
|
e.set_author(name=description, icon_url=author.avatar_url)
|
||||||
|
else:
|
||||||
|
e.set_author(name=description)
|
||||||
|
|
||||||
|
e.set_footer(text=footer)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await destination.send(embed=e)
|
||||||
|
except discord.Forbidden:
|
||||||
|
log.exception(f"Contact failed to {destination}({destination.id})")
|
||||||
|
# Should this automatically opt them out?
|
||||||
|
except discord.HTTPException:
|
||||||
|
log.exception(
|
||||||
|
f"An unexpected error happened while attempting to"
|
||||||
|
f" send contact to {destination}({destination.id})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
successful = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
msg_text = "{}\nMessage:\n\n{}\n{}".format(description, message, footer)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await destination.send("{}\n{}".format(content, box(msg_text)))
|
||||||
|
except discord.Forbidden:
|
||||||
|
log.exception(f"Contact failed to {destination}({destination.id})")
|
||||||
|
# Should this automatically opt them out?
|
||||||
|
except discord.HTTPException:
|
||||||
|
log.exception(
|
||||||
|
f"An unexpected error happened while attempting to"
|
||||||
|
f" send contact to {destination}({destination.id})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
successful = True
|
||||||
|
|
||||||
|
if successful:
|
||||||
|
await ctx.send(_("Your message has been sent."))
|
||||||
else:
|
else:
|
||||||
msg_text = "{}\nMessage:\n\n{}\n{}".format(description, message, footer)
|
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||||
try:
|
|
||||||
await owner.send("{}\n{}".format(content, box(msg_text)))
|
|
||||||
except discord.InvalidArgument:
|
|
||||||
await ctx.send(
|
|
||||||
_("I cannot send your message, I'm unable to find my owner... *sigh*")
|
|
||||||
)
|
|
||||||
except discord.HTTPException:
|
|
||||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Your message has been sent."))
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -1390,6 +1494,59 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
msg = _("Data path: {path}").format(path=data_dir)
|
msg = _("Data path: {path}").format(path=data_dir)
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
@checks.is_owner()
|
||||||
|
async def debuginfo(self, ctx: commands.Context):
|
||||||
|
"""Shows debug information useful for debugging.."""
|
||||||
|
|
||||||
|
if sys.platform == "linux":
|
||||||
|
import distro
|
||||||
|
|
||||||
|
IS_WINDOWS = os.name == "nt"
|
||||||
|
IS_MAC = sys.platform == "darwin"
|
||||||
|
IS_LINUX = sys.platform == "linux"
|
||||||
|
|
||||||
|
pyver = "{}.{}.{} ({})".format(*sys.version_info[:3], platform.architecture()[0])
|
||||||
|
pipver = pip.__version__
|
||||||
|
redver = red_version_info
|
||||||
|
dpy_version = discord.__version__
|
||||||
|
if IS_WINDOWS:
|
||||||
|
os_info = platform.uname()
|
||||||
|
osver = "{} {} (version {})".format(os_info.system, os_info.release, os_info.version)
|
||||||
|
elif IS_MAC:
|
||||||
|
os_info = platform.mac_ver()
|
||||||
|
osver = "Mac OSX {} {}".format(os_info[0], os_info[2])
|
||||||
|
elif IS_LINUX:
|
||||||
|
os_info = distro.linux_distribution()
|
||||||
|
osver = "{} {}".format(os_info[0], os_info[1]).strip()
|
||||||
|
else:
|
||||||
|
osver = "Could not parse OS, report this on Github."
|
||||||
|
user_who_ran = getpass.getuser()
|
||||||
|
|
||||||
|
if await ctx.embed_requested():
|
||||||
|
e = discord.Embed(color=await ctx.embed_colour())
|
||||||
|
e.title = "Debug Info for Red"
|
||||||
|
e.add_field(name="Red version", value=redver, inline=True)
|
||||||
|
e.add_field(name="Python version", value=pyver, inline=True)
|
||||||
|
e.add_field(name="Discord.py version", value=dpy_version, inline=True)
|
||||||
|
e.add_field(name="Pip version", value=pipver, inline=True)
|
||||||
|
e.add_field(name="System arch", value=platform.machine(), inline=True)
|
||||||
|
e.add_field(name="User", value=user_who_ran, inline=True)
|
||||||
|
e.add_field(name="OS version", value=osver, inline=False)
|
||||||
|
await ctx.send(embed=e)
|
||||||
|
else:
|
||||||
|
info = (
|
||||||
|
"Debug Info for Red\n\n"
|
||||||
|
+ "Red version: {}\n".format(redver)
|
||||||
|
+ "Python version: {}\n".format(pyver)
|
||||||
|
+ "Discord.py version: {}\n".format(dpy_version)
|
||||||
|
+ "Pip version: {}\n".format(pipver)
|
||||||
|
+ "System arch: {}\n".format(platform.machine())
|
||||||
|
+ "User: {}\n".format(user_who_ran)
|
||||||
|
+ "OS version: {}\n".format(osver)
|
||||||
|
)
|
||||||
|
await ctx.send(box(info))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def whitelist(self, ctx: commands.Context):
|
async def whitelist(self, ctx: commands.Context):
|
||||||
@@ -1602,7 +1759,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
user = isinstance(user_or_role, discord.Member)
|
user = isinstance(user_or_role, discord.Member)
|
||||||
|
|
||||||
if user and await ctx.bot.is_owner(obj):
|
if user and await ctx.bot.is_owner(user_or_role):
|
||||||
await ctx.send(_("You cannot blacklist an owner!"))
|
await ctx.send(_("You cannot blacklist an owner!"))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1871,6 +2028,102 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("They are not Immune"))
|
await ctx.send(_("They are not Immune"))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@_set.group()
|
||||||
|
async def ownernotifications(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Commands for configuring owner notifications.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@ownernotifications.command()
|
||||||
|
async def optin(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Opt-in on recieving owner notifications.
|
||||||
|
|
||||||
|
This is the default state.
|
||||||
|
"""
|
||||||
|
async with ctx.bot.db.owner_opt_out_list() as opt_outs:
|
||||||
|
if ctx.author.id in opt_outs:
|
||||||
|
opt_outs.remove(ctx.author.id)
|
||||||
|
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@ownernotifications.command()
|
||||||
|
async def optout(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Opt-out of recieving owner notifications.
|
||||||
|
"""
|
||||||
|
async with ctx.bot.db.owner_opt_out_list() as opt_outs:
|
||||||
|
if ctx.author.id not in opt_outs:
|
||||||
|
opt_outs.append(ctx.author.id)
|
||||||
|
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@ownernotifications.command()
|
||||||
|
async def adddestination(
|
||||||
|
self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Adds a destination text channel to recieve owner notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel_id = channel.id
|
||||||
|
except AttributeError:
|
||||||
|
channel_id = channel
|
||||||
|
|
||||||
|
async with ctx.bot.db.extra_owner_destinations() as extras:
|
||||||
|
if channel_id not in extras:
|
||||||
|
extras.append(channel_id)
|
||||||
|
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@ownernotifications.command(aliases=["remdestination", "deletedestination", "deldestination"])
|
||||||
|
async def removedestination(
|
||||||
|
self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Removes a destination text channel from recieving owner notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel_id = channel.id
|
||||||
|
except AttributeError:
|
||||||
|
channel_id = channel
|
||||||
|
|
||||||
|
async with ctx.bot.db.extra_owner_destinations() as extras:
|
||||||
|
if channel_id in extras:
|
||||||
|
extras.remove(channel_id)
|
||||||
|
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@ownernotifications.command()
|
||||||
|
async def listdestinations(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Lists the configured extra destinations for owner notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel_ids = await ctx.bot.db.extra_owner_destinations()
|
||||||
|
|
||||||
|
if not channel_ids:
|
||||||
|
await ctx.send(_("There are no extra channels being sent to."))
|
||||||
|
return
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for channel_id in channel_ids:
|
||||||
|
channel = ctx.bot.get_channel(channel_id)
|
||||||
|
if channel:
|
||||||
|
# This includes the channel name in case the user can't see the channel.
|
||||||
|
data.append(f"{channel.mention} {channel} ({channel.id})")
|
||||||
|
else:
|
||||||
|
data.append(_("Unknown channel with id: {id}").format(id=channel_id))
|
||||||
|
|
||||||
|
output = "\n".join(data)
|
||||||
|
for page in pagify(output):
|
||||||
|
await ctx.send(page)
|
||||||
|
|
||||||
# RPC handlers
|
# RPC handlers
|
||||||
async def rpc_load(self, request):
|
async def rpc_load(self, request):
|
||||||
cog_name = request.params[0]
|
cog_name = request.params[0]
|
||||||
|
|||||||
@@ -120,24 +120,12 @@ def init_events(bot, cli_flags):
|
|||||||
"but you're using {}".format(data["info"]["version"], red_version)
|
"but you're using {}".format(data["info"]["version"], red_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
owners = []
|
await bot.send_to_owners(
|
||||||
owner = bot.get_user(bot.owner_id)
|
"Your Red instance is out of date! {} is the current "
|
||||||
if owner is not None:
|
"version, however you are using {}!".format(
|
||||||
owners.append(owner)
|
data["info"]["version"], red_version
|
||||||
|
)
|
||||||
for co_owner in bot._co_owners:
|
)
|
||||||
co_owner = await bot.get_user(co_owner)
|
|
||||||
if co_owner is not None:
|
|
||||||
owners.append(co_owner)
|
|
||||||
|
|
||||||
for owner in owners:
|
|
||||||
with contextlib.suppress(discord.HTTPException):
|
|
||||||
await owner.send(
|
|
||||||
"Your Red instance is out of date! {} is the current "
|
|
||||||
"version, however you are using {}!".format(
|
|
||||||
data["info"]["version"], red_version
|
|
||||||
)
|
|
||||||
)
|
|
||||||
INFO2 = []
|
INFO2 = []
|
||||||
|
|
||||||
mongo_enabled = storage_type() != "JSON"
|
mongo_enabled = storage_type() != "JSON"
|
||||||
@@ -191,7 +179,7 @@ def init_events(bot, cli_flags):
|
|||||||
await ctx.send(error.args[0])
|
await ctx.send(error.args[0])
|
||||||
else:
|
else:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
elif isinstance(error, commands.BadArgument):
|
elif isinstance(error, commands.UserInputError):
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
elif isinstance(error, commands.DisabledCommand):
|
elif isinstance(error, commands.DisabledCommand):
|
||||||
disabled_message = await bot.db.disabled_command_msg()
|
disabled_message = await bot.db.disabled_command_msg()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
from typing import Union, Iterable, Optional
|
from typing import Union, Iterable, Optional
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
@@ -60,7 +61,10 @@ async def menu(
|
|||||||
):
|
):
|
||||||
raise RuntimeError("All pages must be of the same type")
|
raise RuntimeError("All pages must be of the same type")
|
||||||
for key, value in controls.items():
|
for key, value in controls.items():
|
||||||
if not asyncio.iscoroutinefunction(value):
|
maybe_coro = value
|
||||||
|
if isinstance(value, functools.partial):
|
||||||
|
maybe_coro = value.func
|
||||||
|
if not asyncio.iscoroutinefunction(maybe_coro):
|
||||||
raise RuntimeError("Function must be a coroutine")
|
raise RuntimeError("Function must be a coroutine")
|
||||||
current_page = pages[page]
|
current_page = pages[page]
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ async def reset_red():
|
|||||||
"please select option 5 in the launcher."
|
"please select option 5 in the launcher."
|
||||||
)
|
)
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
print("\nIf you continue you will remove these instanes.\n")
|
print("\nIf you continue you will remove these instances.\n")
|
||||||
for instance in list(instances.keys()):
|
for instance in list(instances.keys()):
|
||||||
print(" - {}".format(instance))
|
print(" - {}".format(instance))
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ async def remove_instance(instance):
|
|||||||
collection = await db.get_collection(name)
|
collection = await db.get_collection(name)
|
||||||
await collection.drop()
|
await collection.drop()
|
||||||
else:
|
else:
|
||||||
pth = Path(instance_data["DATA_PATH"])
|
pth = Path(instance_vals["DATA_PATH"])
|
||||||
safe_delete(pth)
|
safe_delete(pth)
|
||||||
save_config(instance, {}, remove=True)
|
save_config(instance, {}, remove=True)
|
||||||
print("The instance {} has been removed\n".format(instance))
|
print("The instance {} has been removed\n".format(instance))
|
||||||
|
|||||||
Reference in New Issue
Block a user