mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -05:00
Compare commits
42 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 |
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
|
||||
- Reaction roles
|
||||
- Slow Mode
|
||||
- Anilist
|
||||
- AniList
|
||||
- And much, much more!
|
||||
|
||||
Feel free to take a [peek](https://cogboard.red/t/approved-repositories/210) at a list of
|
||||
|
||||
@@ -169,7 +169,7 @@ Trivia
|
||||
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`` - 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`_)
|
||||
|
||||
@@ -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
|
||||
``python3.7 -m pip`` commands.
|
||||
|
||||
To install without audio support:
|
||||
To install without MongoDB support:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
python3.7 -m pip install -U Red-DiscordBot
|
||||
|
||||
Or, to install with audio support:
|
||||
Or, to install with MongoDB support:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
python3.7 -m pip install -U Red-DiscordBot[voice]
|
||||
|
||||
Or, install with audio and MongoDB support:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
python3.7 -m pip install -U Red-DiscordBot[voice,mongo]
|
||||
python3.7 -m pip install -U Red-DiscordBot[mongo]
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -62,23 +62,17 @@ Installing Red
|
||||
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||
``pip`` commands.
|
||||
|
||||
* No audio:
|
||||
* No MongoDB support:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
python -m pip install -U Red-DiscordBot
|
||||
|
||||
* With audio:
|
||||
* With MongoDB support:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
python -m pip install -U Red-DiscordBot[voice]
|
||||
|
||||
* With audio and MongoDB support:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
python -m pip install -U Red-DiscordBot[voice,mongo]
|
||||
python -m pip install -U Red-DiscordBot[mongo]
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ class VersionInfo:
|
||||
)
|
||||
|
||||
|
||||
__version__ = "3.1.1"
|
||||
__version__ = "3.1.2"
|
||||
version_info = VersionInfo.from_str(__version__)
|
||||
|
||||
# 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))
|
||||
|
||||
@checks.mod_or_permissions(manage_guild=True)
|
||||
@alias.command(name="del")
|
||||
@alias.command(name="delete", aliases=["del", "remove"])
|
||||
@commands.guild_only()
|
||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""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))
|
||||
|
||||
@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):
|
||||
"""Delete an existing global alias."""
|
||||
aliases = await self.unloaded_global_aliases()
|
||||
|
||||
@@ -592,15 +592,15 @@ class Audio(commands.Cog):
|
||||
async def spotifyapi(self, ctx):
|
||||
"""Instructions to set the Spotify API tokens."""
|
||||
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"
|
||||
'2. Click "Create An App"\n'
|
||||
'2. Click "Create An App".\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"
|
||||
"6. Copy your client ID and your client secret into:\n"
|
||||
"`{prefix}set api spotify client_id,your_client_id "
|
||||
"client_secret,your_client_secret`"
|
||||
"`{prefix}set api spotify client_id,<your_client_id_here> "
|
||||
"client_secret,<your_client_secret_here>`"
|
||||
).format(prefix=ctx.prefix)
|
||||
await ctx.maybe_send_embed(message)
|
||||
|
||||
@@ -660,7 +660,7 @@ class Audio(commands.Cog):
|
||||
"6. Click on Create Credential at the top.\n"
|
||||
'7. At the top click the link for "API key".\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)
|
||||
await ctx.maybe_send_embed(message)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import aiohttp
|
||||
from redbot.core import data_manager
|
||||
|
||||
JAR_VERSION = "3.2.0.3"
|
||||
JAR_BUILD = 772
|
||||
JAR_BUILD = 796
|
||||
LAVALINK_DOWNLOAD_URL = (
|
||||
f"https://github.com/Cog-Creators/Lavalink-Jars/releases/download/{JAR_VERSION}_{JAR_BUILD}/"
|
||||
f"Lavalink.jar"
|
||||
|
||||
@@ -33,6 +33,9 @@ class Cleanup(commands.Cog):
|
||||
Tries its best to cleanup after itself if the response is positive.
|
||||
"""
|
||||
|
||||
if ctx.assume_yes:
|
||||
return True
|
||||
|
||||
prompt = await ctx.send(
|
||||
_("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())
|
||||
|
||||
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)
|
||||
if not ccinfo:
|
||||
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)
|
||||
async def cc_delete(self, ctx, command: str.lower):
|
||||
"""Delete a custom command.
|
||||
|
||||
@@ -199,7 +199,8 @@ class Downloader(commands.Cog):
|
||||
if not deps:
|
||||
return await ctx.send_help()
|
||||
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:
|
||||
await ctx.send(_("Libraries installed."))
|
||||
@@ -245,7 +246,7 @@ class Downloader(commands.Cog):
|
||||
if repo.install_msg is not None:
|
||||
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):
|
||||
"""Remove a repo and its files."""
|
||||
await self._repo_manager.delete_repo(repo.name)
|
||||
@@ -423,35 +424,39 @@ class Downloader(commands.Cog):
|
||||
return await ctx.send(
|
||||
_("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:
|
||||
with contextlib.suppress(discord.Forbidden):
|
||||
await query.clear_reactions()
|
||||
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
||||
else:
|
||||
if can_react:
|
||||
await query.delete()
|
||||
# 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:
|
||||
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>")
|
||||
async def _cog_list(self, ctx, repo: Repo):
|
||||
|
||||
@@ -114,6 +114,8 @@ class Installable(RepoJSONMixin):
|
||||
if self._location.is_file():
|
||||
copy_func = shutil.copy2
|
||||
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
|
||||
|
||||
# noinspection PyBroadException
|
||||
|
||||
@@ -236,7 +236,7 @@ class Filter(commands.Cog):
|
||||
else:
|
||||
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):
|
||||
"""Remove words from the filter.
|
||||
|
||||
|
||||
@@ -307,14 +307,17 @@ class General(commands.Cog):
|
||||
messages = []
|
||||
for ud in data["list"]:
|
||||
ud.setdefault("example", "N/A")
|
||||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||
if len(description) > 2048:
|
||||
description = "{}...".format(description[:2045])
|
||||
|
||||
message = _(
|
||||
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
||||
"{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)
|
||||
|
||||
if messages is not None and len(messages) > 0:
|
||||
|
||||
@@ -7,8 +7,6 @@ from redbot.core import checks, Config, commands
|
||||
|
||||
_ = Translator("Image", __file__)
|
||||
|
||||
GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Image(commands.Cog):
|
||||
@@ -138,20 +136,20 @@ class Image(commands.Cog):
|
||||
@checks.is_owner()
|
||||
@commands.command()
|
||||
async def imgurcreds(self, ctx):
|
||||
"""Explain how to set imgur API tokens"""
|
||||
"""Explain how to set imgur API tokens."""
|
||||
|
||||
message = _(
|
||||
"To get an Imgur Client ID:\n"
|
||||
"1. Login to an Imgur account.\n"
|
||||
"2. Visit [this](https://api.imgur.com/oauth2/addclient) page\n"
|
||||
"3. Enter a name for your application\n"
|
||||
"4. Select *Anonymous usage without user authorization* for the auth type\n"
|
||||
"5. Set the authorization callback URL to `https://localhost`\n"
|
||||
"6. Leave the app website blank\n"
|
||||
"7. Enter a valid email address and a description\n"
|
||||
"8. Check the captcha box and click next\n"
|
||||
"2. Visit this page https://api.imgur.com/oauth2/addclient.\n"
|
||||
"3. Enter a name for your application.\n"
|
||||
"4. Select *Anonymous usage without user authorization* for the auth type.\n"
|
||||
"5. Set the authorization callback URL to `https://localhost`.\n"
|
||||
"6. Leave the app website blank.\n"
|
||||
"7. Enter a valid email address and a description.\n"
|
||||
"8. Check the captcha box and click next.\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)
|
||||
|
||||
await ctx.maybe_send_embed(message)
|
||||
@@ -166,8 +164,17 @@ class Image(commands.Cog):
|
||||
await ctx.send_help()
|
||||
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(
|
||||
GIPHY_API_KEY, keywords
|
||||
giphy_api_key["api_key"], keywords
|
||||
)
|
||||
|
||||
async with self.session.get(url) as r:
|
||||
@@ -190,8 +197,17 @@ class Image(commands.Cog):
|
||||
await ctx.send_help()
|
||||
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(
|
||||
GIPHY_API_KEY, keywords
|
||||
giphy_api_key["api_key"], keywords
|
||||
)
|
||||
|
||||
async with self.session.get(url) as r:
|
||||
@@ -203,3 +219,21 @@ class Image(commands.Cog):
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
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 .mutes import MuteMixin
|
||||
from .names import ModInfo
|
||||
from .slowmode import Slowmode
|
||||
from .settings import ModSettings
|
||||
|
||||
_ = T_ = Translator("Mod", __file__)
|
||||
@@ -36,6 +37,7 @@ class Mod(
|
||||
MoveToCore,
|
||||
MuteMixin,
|
||||
ModInfo,
|
||||
Slowmode,
|
||||
commands.Cog,
|
||||
metaclass=CompositeMetaClass,
|
||||
):
|
||||
|
||||
@@ -145,7 +145,7 @@ class ModInfo(MixinMeta):
|
||||
if voice_state and voice_state.channel:
|
||||
data.add_field(
|
||||
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,
|
||||
)
|
||||
data.set_footer(text=_("Member #{} | User ID: {}").format(member_number, user.id))
|
||||
@@ -164,7 +164,7 @@ class ModInfo(MixinMeta):
|
||||
await ctx.send(embed=data)
|
||||
|
||||
@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."""
|
||||
names, nicks = await self.get_names_and_nicks(user)
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@staticmethod
|
||||
async def check_online(ctx: commands.Context, stream):
|
||||
async def check_online(self, ctx: commands.Context, stream):
|
||||
try:
|
||||
embed = await stream.is_online()
|
||||
info = await stream.is_online()
|
||||
except OfflineStream:
|
||||
await ctx.send(_("That user is offline."))
|
||||
except StreamNotFound:
|
||||
@@ -155,6 +154,14 @@ class Streams(commands.Cog):
|
||||
_("Something went wrong whilst trying to contact the stream service's API.")
|
||||
)
|
||||
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)
|
||||
|
||||
@commands.group()
|
||||
@@ -309,18 +316,19 @@ class Streams(commands.Cog):
|
||||
@streamset.command()
|
||||
@checks.is_owner()
|
||||
async def twitchtoken(self, ctx: commands.Context):
|
||||
"""Explain how to set the twitch token"""
|
||||
"""Explain how to set the twitch token."""
|
||||
|
||||
message = _(
|
||||
"To set the twitch API tokens, follow these steps:\n"
|
||||
"1. Go to this page: https://dev.twitch.tv/dashboard/apps.\n"
|
||||
"2. Click *Register Your Application*\n"
|
||||
"3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and \n"
|
||||
"select an Application Category of your choosing."
|
||||
"4. Click *Register*, and on the following page, copy the Client ID.\n"
|
||||
"5. do `{prefix}set api twitch client_id,your_client_id`\n\n"
|
||||
"2. Click *Register Your Application*.\n"
|
||||
"3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and "
|
||||
"select an Application Category of your choosing.\n"
|
||||
"4. Click *Register*.\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"
|
||||
"or in DM with the bot.)\n"
|
||||
"or in DM with the bot.\n"
|
||||
).format(prefix=ctx.prefix)
|
||||
|
||||
await ctx.maybe_send_embed(message)
|
||||
@@ -328,17 +336,18 @@ class Streams(commands.Cog):
|
||||
@streamset.command()
|
||||
@checks.is_owner()
|
||||
async def youtubekey(self, ctx: commands.Context):
|
||||
"""Explain how to set the YouTube token"""
|
||||
"""Explain how to set the YouTube token."""
|
||||
|
||||
message = _(
|
||||
"To get one, do the following:\n"
|
||||
"1. Create a project\n"
|
||||
"(see https://support.google.com/googleapi/answer/6251787 for details)\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"
|
||||
"(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"
|
||||
"or in DM with the bot.\n"
|
||||
).format(prefix=ctx.prefix)
|
||||
@@ -538,7 +547,11 @@ class Streams(commands.Cog):
|
||||
for stream in self.streams:
|
||||
with contextlib.suppress(Exception):
|
||||
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:
|
||||
if not stream._messages_cache:
|
||||
continue
|
||||
|
||||
@@ -57,7 +57,7 @@ class Trivia(commands.Cog):
|
||||
settings_dict = await settings.all()
|
||||
msg = box(
|
||||
_(
|
||||
"**Current settings**\n"
|
||||
"Current settings\n"
|
||||
"Bot gains points: {bot_plays}\n"
|
||||
"Answer time limit: {delay} 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)
|
||||
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()
|
||||
async def action_del(self, ctx: commands.Context, action_name: str):
|
||||
"""Delete the action with the specified name."""
|
||||
@@ -175,7 +175,7 @@ class Warnings(commands.Cog):
|
||||
|
||||
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()
|
||||
async def reason_del(self, ctx: commands.Context, reason_name: str):
|
||||
"""Delete a warning reason."""
|
||||
|
||||
@@ -20,6 +20,8 @@ from .utils import common_filters
|
||||
|
||||
CUSTOM_GROUPS = "CUSTOM_GROUPS"
|
||||
|
||||
log = logging.getLogger("redbot")
|
||||
|
||||
|
||||
def _is_submodule(parent, child):
|
||||
return parent == child or child.startswith(parent + ".")
|
||||
@@ -52,10 +54,16 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
custom_info=None,
|
||||
help__page_char_limit=1000,
|
||||
help__max_pages_in_guild=2,
|
||||
help__use_menus=False,
|
||||
help__show_hidden=False,
|
||||
help__verify_checks=True,
|
||||
help__verify_exists=False,
|
||||
help__tagline="",
|
||||
disabled_commands=[],
|
||||
disabled_command_msg="That command is disabled.",
|
||||
api_tokens={},
|
||||
extra_owner_destinations=[],
|
||||
owner_opt_out_list=[],
|
||||
)
|
||||
|
||||
self.db.register_guild(
|
||||
@@ -253,8 +261,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
lib.setup(self)
|
||||
except Exception as e:
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, key)
|
||||
raise errors.ExtensionFailed(key, e) from e
|
||||
self._call_module_finalizers(lib, name)
|
||||
raise errors.CogLoadError() from e
|
||||
else:
|
||||
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
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -23,6 +23,7 @@ class Context(commands.Context):
|
||||
"""
|
||||
|
||||
def __init__(self, **attrs):
|
||||
self.assume_yes = attrs.pop("assume_yes", False)
|
||||
super().__init__(**attrs)
|
||||
self.permission_state: PermState = PermState.NORMAL
|
||||
|
||||
|
||||
@@ -1,21 +1,112 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
import functools
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Optional, List, Dict
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as dpy_commands
|
||||
|
||||
from . import BadArgument
|
||||
from ..i18n import Translator
|
||||
from ..utils.chat_formatting import humanize_timedelta
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
|
||||
__all__ = ["GuildConverter"]
|
||||
__all__ = [
|
||||
"APIToken",
|
||||
"DictConverter",
|
||||
"GuildConverter",
|
||||
"TimedeltaConverter",
|
||||
"get_dict_converter",
|
||||
"get_timedelta_converter",
|
||||
"parse_timedelta",
|
||||
]
|
||||
|
||||
_ = Translator("commands.converter", __file__)
|
||||
|
||||
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):
|
||||
"""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
|
||||
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,
|
||||
this leaves the onus on the cog creator to clearly define how to setup the correct
|
||||
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:
|
||||
bot = ctx.bot
|
||||
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
|
||||
if len(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:
|
||||
raise BadArgument(_("The provided tokens are not in a valid format."))
|
||||
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
|
||||
# 3rd party cogs down the road.
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
from typing import Union, List, AsyncIterator, Iterable, cast
|
||||
|
||||
@@ -77,14 +78,6 @@ class RedHelpFormatter:
|
||||
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):
|
||||
"""
|
||||
This delegates to other functions.
|
||||
@@ -102,7 +95,7 @@ class RedHelpFormatter:
|
||||
await self.command_not_found(ctx, help_for)
|
||||
return
|
||||
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)
|
||||
return
|
||||
help_for = exc.last
|
||||
@@ -138,7 +131,7 @@ class RedHelpFormatter:
|
||||
|
||||
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:
|
||||
async for _ in self.help_filter_func(ctx, (obj,), bypass_hidden=True):
|
||||
# This is a really lazy option for not
|
||||
@@ -174,7 +167,7 @@ class RedHelpFormatter:
|
||||
|
||||
if command.help:
|
||||
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)
|
||||
if not value:
|
||||
value = EMPTY_STRING
|
||||
@@ -182,8 +175,14 @@ class RedHelpFormatter:
|
||||
emb["fields"].append(field)
|
||||
|
||||
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(
|
||||
f"**{name}** {command.short_doc}"
|
||||
shorten_line(f"**{name}** {command.short_doc}")
|
||||
for name, command in sorted(subcommands.items())
|
||||
)
|
||||
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
|
||||
for nm, com in sorted(cmds):
|
||||
width_gap = discord.utils._string_width(nm) - len(nm)
|
||||
doc = command.short_doc
|
||||
doc = com.short_doc
|
||||
if len(doc) > doc_max_width:
|
||||
doc = doc[: doc_max_width - 3] + "..."
|
||||
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}
|
||||
|
||||
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):
|
||||
embed = discord.Embed(color=color, **embed_dict["embed"])
|
||||
|
||||
@@ -272,7 +277,7 @@ class RedHelpFormatter:
|
||||
async def format_cog_help(self, ctx: Context, obj: commands.Cog):
|
||||
|
||||
coms = await self.get_cog_help_mapping(ctx, obj)
|
||||
if not (coms or self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES):
|
||||
if not (coms or await ctx.bot.db.help.verify_exists()):
|
||||
return
|
||||
|
||||
description = obj.help
|
||||
@@ -283,11 +288,24 @@ class RedHelpFormatter:
|
||||
|
||||
emb["footer"]["text"] = tagline
|
||||
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] + "..."
|
||||
|
||||
command_text = "\n".join(
|
||||
f"**{name}** {command.short_doc}" for name, command in sorted(coms.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)):
|
||||
if i == 0:
|
||||
@@ -347,8 +365,14 @@ class RedHelpFormatter:
|
||||
else:
|
||||
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(
|
||||
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)):
|
||||
@@ -399,17 +423,25 @@ class RedHelpFormatter:
|
||||
"""
|
||||
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
|
||||
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.
|
||||
if await obj.can_see(ctx):
|
||||
yield obj
|
||||
elif self.VERIFY_CHECKS:
|
||||
if await obj.can_run(ctx):
|
||||
elif verify_checks:
|
||||
try:
|
||||
can_run = await obj.can_run(ctx)
|
||||
except discord.DiscordException:
|
||||
can_run = False
|
||||
if can_run:
|
||||
yield obj
|
||||
elif not (self.SHOW_HIDDEN or bypass_hidden):
|
||||
if getattr(obj, "hidden", False): # Cog compatibility
|
||||
elif not show_hidden:
|
||||
if not getattr(obj, "hidden", False): # Cog compatibility
|
||||
yield obj
|
||||
else:
|
||||
yield obj
|
||||
@@ -430,8 +462,8 @@ class RedHelpFormatter:
|
||||
await ctx.send(embed=ret)
|
||||
else:
|
||||
await ctx.send(ret)
|
||||
elif self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES:
|
||||
ret = T_("Command *{command_name}* not found.").format(command_name=help_for)
|
||||
elif await ctx.bot.db.help.verify_exists():
|
||||
ret = T_("Help topic for *{command_name}* not found.").format(command_name=help_for)
|
||||
if use_embeds:
|
||||
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)
|
||||
@@ -445,10 +477,17 @@ class RedHelpFormatter:
|
||||
"""
|
||||
Sends an error
|
||||
"""
|
||||
ret = T_("Command *{command_name}* has no subcommands.").format(
|
||||
command_name=command.qualified_name
|
||||
ret = T_("Command *{command_name}* has no subcommand named *{not_found}*.").format(
|
||||
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
|
||||
def parse_command(ctx, help_for: str):
|
||||
@@ -487,19 +526,43 @@ class RedHelpFormatter:
|
||||
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()
|
||||
destination = ctx.author if len(pages) > max_pages_in_guild else ctx
|
||||
|
||||
if embed:
|
||||
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:
|
||||
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:
|
||||
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_)
|
||||
|
||||
@@ -8,6 +8,9 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import platform
|
||||
import getpass
|
||||
import pip
|
||||
import tarfile
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
@@ -30,7 +33,7 @@ from redbot.core import (
|
||||
i18n,
|
||||
)
|
||||
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:
|
||||
from redbot.core.bot import Red
|
||||
@@ -42,6 +45,8 @@ log = logging.getLogger("red")
|
||||
|
||||
_ = i18n.Translator("Core", __file__)
|
||||
|
||||
TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"])
|
||||
|
||||
|
||||
class CoreLogic:
|
||||
def __init__(self, bot: "Red"):
|
||||
@@ -328,28 +333,12 @@ class Core(commands.Cog, CoreLogic):
|
||||
async def uptime(self, ctx: commands.Context):
|
||||
"""Shows Red's uptime"""
|
||||
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
passed = self.get_bot_uptime()
|
||||
await ctx.send(_("Been up for: **{}** (since {} UTC)").format(passed, since))
|
||||
|
||||
def get_bot_uptime(self, *, brief: bool = False):
|
||||
# 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)
|
||||
delta = datetime.datetime.utcnow() - self.bot.uptime
|
||||
await ctx.send(
|
||||
_("Been up for: **{}** (since {} UTC)").format(
|
||||
humanize_timedelta(timedelta=delta), since
|
||||
)
|
||||
)
|
||||
|
||||
@commands.group()
|
||||
async def embedset(self, ctx: commands.Context):
|
||||
@@ -529,6 +518,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
"""Loads packages"""
|
||||
if not cogs:
|
||||
return await ctx.send_help()
|
||||
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
|
||||
async with ctx.typing():
|
||||
loaded, failed, not_found, already_loaded, failed_with_reason = await self._load(cogs)
|
||||
|
||||
@@ -571,6 +561,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
"""Unloads packages"""
|
||||
if not cogs:
|
||||
return await ctx.send_help()
|
||||
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
|
||||
unloaded, failed = await self._unload(cogs)
|
||||
|
||||
if unloaded:
|
||||
@@ -589,6 +580,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
"""Reloads packages"""
|
||||
if not cogs:
|
||||
return await ctx.send_help()
|
||||
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
|
||||
async with ctx.typing():
|
||||
loaded, failed, not_found, already_loaded, failed_with_reason = await self._reload(
|
||||
cogs
|
||||
@@ -1057,7 +1049,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command()
|
||||
@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.
|
||||
|
||||
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:
|
||||
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=entry)
|
||||
await ctx.bot.db.api_tokens.set_raw(service, value=tokens)
|
||||
await ctx.send(_("`{service}` API tokens have been set.").format(service=service))
|
||||
|
||||
@commands.group()
|
||||
@@ -1080,6 +1071,80 @@ class Core(commands.Cog, CoreLogic):
|
||||
"""Manage settings for the help command."""
|
||||
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")
|
||||
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
|
||||
"""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):
|
||||
"""Sends a message to the owner"""
|
||||
guild = ctx.message.guild
|
||||
owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
|
||||
author = ctx.message.author
|
||||
footer = _("User ID: {}").format(author.id)
|
||||
|
||||
@@ -1291,41 +1355,81 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
description = _("Sent by {} {}").format(author, source)
|
||||
|
||||
if isinstance(author, discord.Member):
|
||||
colour = author.colour
|
||||
else:
|
||||
colour = discord.Colour.red()
|
||||
destinations = await ctx.bot.get_owner_notification_destinations()
|
||||
|
||||
if await ctx.embed_requested():
|
||||
e = discord.Embed(colour=colour, 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)
|
||||
if not destinations:
|
||||
await ctx.send(_("I've been configured not to send this anywhere."))
|
||||
return
|
||||
|
||||
try:
|
||||
await owner.send(content, embed=e)
|
||||
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."))
|
||||
successful = False
|
||||
|
||||
for destination in destinations:
|
||||
|
||||
is_dm = isinstance(destination, discord.User)
|
||||
send_embed = None
|
||||
|
||||
if is_dm:
|
||||
send_embed = await ctx.bot.db.user(destination).embeds()
|
||||
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:
|
||||
msg_text = "{}\nMessage:\n\n{}\n{}".format(description, message, footer)
|
||||
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."))
|
||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
@@ -1390,6 +1494,59 @@ class Core(commands.Cog, CoreLogic):
|
||||
msg = _("Data path: {path}").format(path=data_dir)
|
||||
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()
|
||||
@checks.is_owner()
|
||||
async def whitelist(self, ctx: commands.Context):
|
||||
@@ -1602,7 +1759,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
"""
|
||||
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!"))
|
||||
return
|
||||
|
||||
@@ -1871,6 +2028,102 @@ class Core(commands.Cog, CoreLogic):
|
||||
else:
|
||||
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
|
||||
async def rpc_load(self, request):
|
||||
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)
|
||||
)
|
||||
|
||||
owners = []
|
||||
owner = bot.get_user(bot.owner_id)
|
||||
if owner is not None:
|
||||
owners.append(owner)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
await bot.send_to_owners(
|
||||
"Your Red instance is out of date! {} is the current "
|
||||
"version, however you are using {}!".format(
|
||||
data["info"]["version"], red_version
|
||||
)
|
||||
)
|
||||
INFO2 = []
|
||||
|
||||
mongo_enabled = storage_type() != "JSON"
|
||||
@@ -191,7 +179,7 @@ def init_events(bot, cli_flags):
|
||||
await ctx.send(error.args[0])
|
||||
else:
|
||||
await ctx.send_help()
|
||||
elif isinstance(error, commands.BadArgument):
|
||||
elif isinstance(error, commands.UserInputError):
|
||||
await ctx.send_help()
|
||||
elif isinstance(error, commands.DisabledCommand):
|
||||
disabled_message = await bot.db.disabled_command_msg()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
||||
import asyncio
|
||||
import contextlib
|
||||
import functools
|
||||
from typing import Union, Iterable, Optional
|
||||
import discord
|
||||
|
||||
@@ -60,7 +61,10 @@ async def menu(
|
||||
):
|
||||
raise RuntimeError("All pages must be of the same type")
|
||||
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")
|
||||
current_page = pages[page]
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ async def reset_red():
|
||||
"please select option 5 in the launcher."
|
||||
)
|
||||
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()):
|
||||
print(" - {}".format(instance))
|
||||
await asyncio.sleep(3)
|
||||
|
||||
@@ -440,7 +440,7 @@ async def remove_instance(instance):
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
else:
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
pth = Path(instance_vals["DATA_PATH"])
|
||||
safe_delete(pth)
|
||||
save_config(instance, {}, remove=True)
|
||||
print("The instance {} has been removed\n".format(instance))
|
||||
|
||||
Reference in New Issue
Block a user