mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-08 10:22:31 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d3c72f356 | ||
|
|
97a9fde5fd | ||
|
|
a664615a2d | ||
|
|
3d4f9500e9 | ||
|
|
a8450580e8 | ||
|
|
8654924869 | ||
|
|
068585379a | ||
|
|
fc5fc08962 | ||
|
|
41fdcb2ae8 | ||
|
|
de4804863a | ||
|
|
2ac4dde729 | ||
|
|
498d0d22fb | ||
|
|
2a38777379 | ||
|
|
01c1fdfd16 | ||
|
|
0a8e7f5663 | ||
|
|
1755334124 | ||
|
|
40c0d8d83b | ||
|
|
ee53d50c3a | ||
|
|
8570971f68 | ||
|
|
e1a110b1bf | ||
|
|
77235f7750 | ||
|
|
c7fd64e0c8 | ||
|
|
8f04fd436f | ||
|
|
b085c1501f | ||
|
|
7f390df879 | ||
|
|
54e65082bc | ||
|
|
826dae129e |
@@ -14,11 +14,11 @@ In order to create the service file, you will first need the location of your :c
|
|||||||
|
|
||||||
# If redbot is installed in a virtualenv
|
# If redbot is installed in a virtualenv
|
||||||
source redenv/bin/activate
|
source redenv/bin/activate
|
||||||
|
which python
|
||||||
|
|
||||||
# If you are using pyenv
|
# If you are using pyenv
|
||||||
pyenv shell <name>
|
pyenv shell <name>
|
||||||
|
pyenv which python
|
||||||
which python
|
|
||||||
|
|
||||||
Then create the new service file:
|
Then create the new service file:
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ Removals
|
|||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
- ``[p]set owner`` and ``[p]set token`` have been removed in favor of managing server side. (`#2928 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2928>`_)
|
- ``[p]set owner`` and ``[p]set token`` have been removed in favor of managing server side. (`#2928 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2928>`_)
|
||||||
- Shared libraries are marked for removal in Red 3.3. (`#3106 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3106>`_)
|
- Shared libraries are marked for removal in Red 3.4. (`#3106 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3106>`_)
|
||||||
- Removed ``[p]backup``. Use the cli command ``redbot-setup backup`` instead. (`#3235 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3235>`_)
|
- Removed ``[p]backup``. Use the cli command ``redbot-setup backup`` instead. (`#3235 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3235>`_)
|
||||||
- Removed the functions ``safe_delete``, ``fuzzy_command_search``, ``format_fuzzy_results`` and ``create_backup`` from ``redbot.core.utils``. (`#3240 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3240>`_)
|
- Removed the functions ``safe_delete``, ``fuzzy_command_search``, ``format_fuzzy_results`` and ``create_backup`` from ``redbot.core.utils``. (`#3240 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3240>`_)
|
||||||
- Removed a lot of the launcher's handled behavior. (`#3289 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3289>`_)
|
- Removed a lot of the launcher's handled behavior. (`#3289 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3289>`_)
|
||||||
|
|||||||
68
docs/changelog_3_3_0.rst
Normal file
68
docs/changelog_3_3_0.rst
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.. 3.3.x Changelogs
|
||||||
|
|
||||||
|
Redbot 3.3.0 (2020-01-26)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Core Bot
|
||||||
|
--------
|
||||||
|
|
||||||
|
- The bot's description is now configurable.
|
||||||
|
- We now use discord.py 1.3.1, this comes with added teams support.
|
||||||
|
- The commands module has been slightly restructured to provide more useful data to developers.
|
||||||
|
- Help is now self consistent in the extra formatting used.
|
||||||
|
|
||||||
|
Core Commands
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Slowmode should no longer error on nonsensical time quantities.
|
||||||
|
- Embed use can be configured per channel as well.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- We've made some small fixes to inaccurate instructions about installing with pyenv.
|
||||||
|
- Notes about deprecating in 3.3 have been altered to 3.4 to match the intended timeframe.
|
||||||
|
|
||||||
|
Admin
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Gives feedback when adding or removing a role doesn't make sense.
|
||||||
|
|
||||||
|
Audio
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Playlist finding is more intuitive.
|
||||||
|
- disconnect and repeat commands no longer interfere with eachother.
|
||||||
|
|
||||||
|
CustomCom
|
||||||
|
---------
|
||||||
|
|
||||||
|
- No longer errors when exiting an interactive menu.
|
||||||
|
|
||||||
|
Cleanup
|
||||||
|
-------
|
||||||
|
|
||||||
|
- A rare edge case involving messages which are deleted during cleanup and are the only message was fixed.
|
||||||
|
|
||||||
|
Downloader
|
||||||
|
----------
|
||||||
|
|
||||||
|
- Some user facing messages were improved.
|
||||||
|
- Downloader's initialization can no longer time out at startup.
|
||||||
|
|
||||||
|
General
|
||||||
|
-------
|
||||||
|
|
||||||
|
- Roll command will no longer attempt to roll obscenely large amounts.
|
||||||
|
|
||||||
|
Mod
|
||||||
|
---
|
||||||
|
|
||||||
|
- You can set a default amount of days to clean up when banning.
|
||||||
|
- Ban and hackban now use that default.
|
||||||
|
- Users can now optionally be DMed their ban reason.
|
||||||
|
|
||||||
|
Permissions
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- Now has stronger enforcement of prioritizing botwide settings.
|
||||||
@@ -6,7 +6,7 @@ Shared API Keys
|
|||||||
|
|
||||||
Red has a central API key storage utilising the core bots config. This allows cog creators to add a single location to store API keys for their cogs which may be shared between other cogs.
|
Red has a central API key storage utilising the core bots config. This allows cog creators to add a single location to store API keys for their cogs which may be shared between other cogs.
|
||||||
|
|
||||||
There needs to be some consistency between cog creators when using shared API keys between cogs. To help make this easier service should be all **lowercase** and the key names should match the naming convetion of the API being accessed.
|
There needs to be some consistency between cog creators when using shared API keys between cogs. To help make this easier service should be all **lowercase** and the key names should match the naming convention of the API being accessed.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Commands Package
|
|||||||
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
||||||
all of the attributes from discord.py's are also in ours.
|
all of the attributes from discord.py's are also in ours.
|
||||||
Some of these attributes, however, have been slightly modified, while others have been added to
|
Some of these attributes, however, have been slightly modified, while others have been added to
|
||||||
extend functionlities used throughout the bot, as outlined below.
|
extend functionalities used throughout the bot, as outlined below.
|
||||||
|
|
||||||
.. autofunction:: redbot.core.commands.command
|
.. autofunction:: redbot.core.commands.command
|
||||||
|
|
||||||
@@ -23,5 +23,14 @@ extend functionlities used throughout the bot, as outlined below.
|
|||||||
.. autoclass:: redbot.core.commands.Context
|
.. autoclass:: redbot.core.commands.Context
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.commands.GuildContext
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.commands.DMContext
|
||||||
|
|
||||||
.. automodule:: redbot.core.commands.requires
|
.. automodule:: redbot.core.commands.requires
|
||||||
:members: PrivilegeLevel, PermState, Requires
|
:members: PrivilegeLevel, PermState, Requires
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.commands.converter
|
||||||
|
:members:
|
||||||
|
:exclude-members: convert
|
||||||
|
:no-undoc-members:
|
||||||
|
|||||||
@@ -81,5 +81,5 @@ Keys specific to the cog info.json (case sensitive)
|
|||||||
``SHARED_LIBRARY``. If ``SHARED_LIBRARY`` then ``hidden`` will be ``True``.
|
``SHARED_LIBRARY``. If ``SHARED_LIBRARY`` then ``hidden`` will be ``True``.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
Shared libraries are deprecated since version 3.2 and are marked for removal in version 3.3.
|
Shared libraries are deprecated since version 3.2 and are marked for removal in version 3.4.
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Changelogs:
|
:caption: Changelogs:
|
||||||
|
|
||||||
|
changelog_3_3_0
|
||||||
release_notes_3_2_0
|
release_notes_3_2_0
|
||||||
changelog_3_2_0
|
changelog_3_2_0
|
||||||
changelog_3_1_0
|
changelog_3_1_0
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
|
|||||||
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.2.3"
|
__version__ = "3.3.0"
|
||||||
version_info = VersionInfo.from_str(__version__)
|
version_info = VersionInfo.from_str(__version__)
|
||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
|
|||||||
@@ -121,8 +121,13 @@ class Admin(commands.Cog):
|
|||||||
async def _addrole(
|
async def _addrole(
|
||||||
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
|
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
|
||||||
):
|
):
|
||||||
if member is None:
|
if role in member.roles:
|
||||||
member = ctx.author
|
await ctx.send(
|
||||||
|
_("{member.display_name} already has the role {role.name}.").format(
|
||||||
|
role=role, member=member
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
if check_user and not self.pass_user_hierarchy_check(ctx, role):
|
if check_user and not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await ctx.send(_(USER_HIERARCHY_ISSUE_ADD).format(role=role, member=member))
|
await ctx.send(_(USER_HIERARCHY_ISSUE_ADD).format(role=role, member=member))
|
||||||
return
|
return
|
||||||
@@ -146,8 +151,13 @@ class Admin(commands.Cog):
|
|||||||
async def _removerole(
|
async def _removerole(
|
||||||
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
|
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
|
||||||
):
|
):
|
||||||
if member is None:
|
if role not in member.roles:
|
||||||
member = ctx.author
|
await ctx.send(
|
||||||
|
_("{member.display_name} does not have the role {role.name}.").format(
|
||||||
|
role=role, member=member
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
if check_user and not self.pass_user_hierarchy_check(ctx, role):
|
if check_user and not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await ctx.send(_(USER_HIERARCHY_ISSUE_REMOVE).format(role=role, member=member))
|
await ctx.send(_(USER_HIERARCHY_ISSUE_REMOVE).format(role=role, member=member))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class Alias(commands.Cog):
|
|||||||
|
|
||||||
def is_command(self, alias_name: str) -> bool:
|
def is_command(self, alias_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
The logic here is that if this returns true, the name shouldnt be used for an alias
|
The logic here is that if this returns true, the name should not be used for an alias
|
||||||
The function name can be changed when alias is reworked
|
The function name can be changed when alias is reworked
|
||||||
"""
|
"""
|
||||||
command = self.bot.get_command(alias_name)
|
command = self.bot.get_command(alias_name)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ from .utils import *
|
|||||||
|
|
||||||
_ = Translator("Audio", __file__)
|
_ = Translator("Audio", __file__)
|
||||||
|
|
||||||
__version__ = "1.1.0"
|
__version__ = "1.1.1"
|
||||||
__author__ = ["aikaterna", "Draper"]
|
__author__ = ["aikaterna", "Draper"]
|
||||||
|
|
||||||
log = logging.getLogger("red.audio")
|
log = logging.getLogger("red.audio")
|
||||||
@@ -705,7 +705,6 @@ class Audio(commands.Cog):
|
|||||||
msg += _("Auto-disconnection at queue end: {true_or_false}.").format(
|
msg += _("Auto-disconnection at queue end: {true_or_false}.").format(
|
||||||
true_or_false=_("Enabled") if not disconnect else _("Disabled")
|
true_or_false=_("Enabled") if not disconnect else _("Disabled")
|
||||||
)
|
)
|
||||||
await self.config.guild(ctx.guild).repeat.set(not disconnect)
|
|
||||||
if disconnect is not True and autoplay is True:
|
if disconnect is not True and autoplay is True:
|
||||||
msg += _("\nAuto-play has been disabled.")
|
msg += _("\nAuto-play has been disabled.")
|
||||||
await self.config.guild(ctx.guild).auto_play.set(False)
|
await self.config.guild(ctx.guild).auto_play.set(False)
|
||||||
@@ -1151,11 +1150,11 @@ class Audio(commands.Cog):
|
|||||||
`[p]audioset autoplay PersonalPlaylist --scope User --author Draper`
|
`[p]audioset autoplay PersonalPlaylist --scope User --author Draper`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
|
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -3834,7 +3833,7 @@ class Audio(commands.Cog):
|
|||||||
author: discord.User,
|
author: discord.User,
|
||||||
guild: discord.Guild,
|
guild: discord.Guild,
|
||||||
specified_user: bool = False,
|
specified_user: bool = False,
|
||||||
) -> Tuple[Optional[int], str]:
|
) -> Tuple[Optional[int], str, str]:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -3863,34 +3862,57 @@ class Audio(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
correct_scope_matches: List[Playlist]
|
correct_scope_matches: List[Playlist]
|
||||||
original_input = matches.get("arg")
|
original_input = matches.get("arg")
|
||||||
|
lazy_match = False
|
||||||
|
if scope is None:
|
||||||
|
correct_scope_matches_temp: MutableMapping = matches.get("all")
|
||||||
|
lazy_match = True
|
||||||
|
else:
|
||||||
correct_scope_matches_temp: MutableMapping = matches.get(scope)
|
correct_scope_matches_temp: MutableMapping = matches.get(scope)
|
||||||
|
|
||||||
guild_to_query = guild.id
|
guild_to_query = guild.id
|
||||||
user_to_query = author.id
|
user_to_query = author.id
|
||||||
|
correct_scope_matches_user = []
|
||||||
|
correct_scope_matches_guild = []
|
||||||
|
correct_scope_matches_global = []
|
||||||
|
|
||||||
if not correct_scope_matches_temp:
|
if not correct_scope_matches_temp:
|
||||||
return None, original_input
|
return None, original_input, scope or PlaylistScope.GUILD.value
|
||||||
if scope == PlaylistScope.USER.value:
|
if lazy_match or (scope == PlaylistScope.USER.value):
|
||||||
correct_scope_matches = [
|
correct_scope_matches_user = [
|
||||||
p for p in correct_scope_matches_temp if user_to_query == p.scope_id
|
p for p in matches.get(PlaylistScope.USER.value) if user_to_query == p.scope_id
|
||||||
]
|
]
|
||||||
elif scope == PlaylistScope.GUILD.value:
|
if lazy_match or (scope == PlaylistScope.GUILD.value and not correct_scope_matches_user):
|
||||||
if specified_user:
|
if specified_user:
|
||||||
correct_scope_matches = [
|
correct_scope_matches_guild = [
|
||||||
p
|
p
|
||||||
for p in correct_scope_matches_temp
|
for p in matches.get(PlaylistScope.GUILD.value)
|
||||||
if guild_to_query == p.scope_id and p.author == user_to_query
|
if guild_to_query == p.scope_id and p.author == user_to_query
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
correct_scope_matches = [
|
correct_scope_matches_guild = [
|
||||||
p for p in correct_scope_matches_temp if guild_to_query == p.scope_id
|
p
|
||||||
|
for p in matches.get(PlaylistScope.GUILD.value)
|
||||||
|
if guild_to_query == p.scope_id
|
||||||
]
|
]
|
||||||
else:
|
if lazy_match or (
|
||||||
|
scope == PlaylistScope.GLOBAL.value
|
||||||
|
and not correct_scope_matches_user
|
||||||
|
and not correct_scope_matches_guild
|
||||||
|
):
|
||||||
if specified_user:
|
if specified_user:
|
||||||
correct_scope_matches = [
|
correct_scope_matches_global = [
|
||||||
p for p in correct_scope_matches_temp if p.author == user_to_query
|
p
|
||||||
|
for p in matches.get(PlaylistScope.USGLOBALER.value)
|
||||||
|
if p.author == user_to_query
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
correct_scope_matches = [p for p in correct_scope_matches_temp]
|
correct_scope_matches_global = [p for p in matches.get(PlaylistScope.GLOBAL.value)]
|
||||||
|
|
||||||
|
correct_scope_matches = [
|
||||||
|
*correct_scope_matches_global,
|
||||||
|
*correct_scope_matches_guild,
|
||||||
|
*correct_scope_matches_user,
|
||||||
|
]
|
||||||
match_count = len(correct_scope_matches)
|
match_count = len(correct_scope_matches)
|
||||||
if match_count > 1:
|
if match_count > 1:
|
||||||
correct_scope_matches2 = [
|
correct_scope_matches2 = [
|
||||||
@@ -3917,14 +3939,15 @@ class Audio(commands.Cog):
|
|||||||
).format(match_count=match_count, original_input=original_input)
|
).format(match_count=match_count, original_input=original_input)
|
||||||
)
|
)
|
||||||
elif match_count == 1:
|
elif match_count == 1:
|
||||||
return correct_scope_matches[0].id, original_input
|
return correct_scope_matches[0].id, original_input, correct_scope_matches[0].scope
|
||||||
elif match_count == 0:
|
elif match_count == 0:
|
||||||
return None, original_input
|
return None, original_input, scope
|
||||||
|
|
||||||
# TODO : Convert this section to a new paged reaction menu when Toby Menus are Merged
|
# TODO : Convert this section to a new paged reaction menu when Toby Menus are Merged
|
||||||
pos_len = 3
|
pos_len = 3
|
||||||
playlists = f"{'#':{pos_len}}\n"
|
playlists = f"{'#':{pos_len}}\n"
|
||||||
number = 0
|
number = 0
|
||||||
|
correct_scope_matches = sorted(correct_scope_matches, key=lambda x: x.name.lower())
|
||||||
for number, playlist in enumerate(correct_scope_matches, 1):
|
for number, playlist in enumerate(correct_scope_matches, 1):
|
||||||
author = self.bot.get_user(playlist.author) or playlist.author or _("Unknown")
|
author = self.bot.get_user(playlist.author) or playlist.author or _("Unknown")
|
||||||
line = _(
|
line = _(
|
||||||
@@ -3937,7 +3960,7 @@ class Audio(commands.Cog):
|
|||||||
).format(
|
).format(
|
||||||
number=number,
|
number=number,
|
||||||
playlist=playlist,
|
playlist=playlist,
|
||||||
scope=humanize_scope(scope),
|
scope=humanize_scope(playlist.scope),
|
||||||
tracks=len(playlist.tracks),
|
tracks=len(playlist.tracks),
|
||||||
author=author,
|
author=author,
|
||||||
)
|
)
|
||||||
@@ -3973,7 +3996,11 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
with contextlib.suppress(discord.HTTPException):
|
with contextlib.suppress(discord.HTTPException):
|
||||||
await msg.delete()
|
await msg.delete()
|
||||||
return correct_scope_matches[pred.result].id, original_input
|
return (
|
||||||
|
correct_scope_matches[pred.result].id,
|
||||||
|
original_input,
|
||||||
|
correct_scope_matches[pred.result].scope,
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -4036,12 +4063,12 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global --Author Draper#6666`
|
`[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global --Author Draper#6666`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
(scope, author, guild, specified_user) = scope_data
|
(scope, author, guild, specified_user) = scope_data
|
||||||
if not await self._playlist_check(ctx):
|
if not await self._playlist_check(ctx):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
(playlist_id, playlist_arg) = await self._get_correct_playlist_id(
|
(playlist_id, playlist_arg, scope) = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -4223,7 +4250,7 @@ class Audio(commands.Cog):
|
|||||||
) = scope_data
|
) = scope_data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, from_scope, from_author, from_guild, specified_from_user
|
ctx, playlist_matches, from_scope, from_author, from_guild, specified_from_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -4401,11 +4428,11 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist delete MyPersonalPlaylist --scope User`
|
`[p]playlist delete MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -4489,19 +4516,18 @@ class Audio(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
scope_name = humanize_scope(
|
|
||||||
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
ctx.command.reset_cooldown(ctx)
|
ctx.command.reset_cooldown(ctx)
|
||||||
return await self._embed_msg(ctx, title=str(e))
|
return await self._embed_msg(ctx, title=str(e))
|
||||||
|
scope_name = humanize_scope(
|
||||||
|
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
||||||
|
)
|
||||||
if playlist_id is None:
|
if playlist_id is None:
|
||||||
ctx.command.reset_cooldown(ctx)
|
ctx.command.reset_cooldown(ctx)
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
@@ -4632,11 +4658,11 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist download MyPersonalPlaylist --scope User`
|
`[p]playlist download MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -4772,19 +4798,19 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist info MyPersonalPlaylist --scope User`
|
`[p]playlist info MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
scope_name = humanize_scope(
|
|
||||||
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
ctx.command.reset_cooldown(ctx)
|
ctx.command.reset_cooldown(ctx)
|
||||||
return await self._embed_msg(ctx, title=str(e))
|
return await self._embed_msg(ctx, title=str(e))
|
||||||
|
scope_name = humanize_scope(
|
||||||
|
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
||||||
|
)
|
||||||
|
|
||||||
if playlist_id is None:
|
if playlist_id is None:
|
||||||
ctx.command.reset_cooldown(ctx)
|
ctx.command.reset_cooldown(ctx)
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
@@ -5132,18 +5158,18 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope User`
|
`[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
scope_name = humanize_scope(
|
|
||||||
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
return await self._embed_msg(ctx, title=str(e))
|
return await self._embed_msg(ctx, title=str(e))
|
||||||
|
scope_name = humanize_scope(
|
||||||
|
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
||||||
|
)
|
||||||
if playlist_id is None:
|
if playlist_id is None:
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -5339,7 +5365,7 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist start MyPersonalPlaylist --scope User`
|
`[p]playlist start MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
dj_enabled = self._dj_status_cache.setdefault(
|
dj_enabled = self._dj_status_cache.setdefault(
|
||||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||||
@@ -5355,7 +5381,7 @@ class Audio(commands.Cog):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -5510,10 +5536,10 @@ class Audio(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
@@ -5789,7 +5815,7 @@ class Audio(commands.Cog):
|
|||||||
`[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User`
|
`[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [None, ctx.author, ctx.guild, False]
|
||||||
scope, author, guild, specified_user = scope_data
|
scope, author, guild, specified_user = scope_data
|
||||||
|
|
||||||
new_name = new_name.split(" ")[0].strip('"')[:32]
|
new_name = new_name.split(" ")[0].strip('"')[:32]
|
||||||
@@ -5805,7 +5831,7 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
)
|
)
|
||||||
except TooManyMatches as e:
|
except TooManyMatches as e:
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ class PlaylistConverter(commands.Converter):
|
|||||||
PlaylistScope.GLOBAL.value: global_matches,
|
PlaylistScope.GLOBAL.value: global_matches,
|
||||||
PlaylistScope.GUILD.value: guild_matches,
|
PlaylistScope.GUILD.value: guild_matches,
|
||||||
PlaylistScope.USER.value: user_matches,
|
PlaylistScope.USER.value: user_matches,
|
||||||
|
"all": [*global_matches, *guild_matches, *user_matches],
|
||||||
"arg": arg,
|
"arg": arg,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ class NoExitParser(argparse.ArgumentParser):
|
|||||||
class ScopeParser(commands.Converter):
|
class ScopeParser(commands.Converter):
|
||||||
async def convert(
|
async def convert(
|
||||||
self, ctx: commands.Context, argument: str
|
self, ctx: commands.Context, argument: str
|
||||||
) -> Tuple[str, discord.User, Optional[discord.Guild], bool]:
|
) -> Tuple[Optional[str], discord.User, Optional[discord.Guild], bool]:
|
||||||
|
|
||||||
target_scope: Optional[str] = None
|
target_scope: Optional[str] = None
|
||||||
target_user: Optional[Union[discord.Member, discord.User]] = None
|
target_user: Optional[Union[discord.Member, discord.User]] = None
|
||||||
@@ -261,7 +262,7 @@ class ScopeParser(commands.Converter):
|
|||||||
elif any(x in argument for x in ["--author", "--user", "--member"]):
|
elif any(x in argument for x in ["--author", "--user", "--member"]):
|
||||||
raise commands.ArgParserFailure("--scope", "Nothing", custom_help=_USER_HELP)
|
raise commands.ArgParserFailure("--scope", "Nothing", custom_help=_USER_HELP)
|
||||||
|
|
||||||
target_scope: str = target_scope or PlaylistScope.GUILD.value
|
target_scope: str = target_scope or None
|
||||||
target_user: Union[discord.Member, discord.User] = target_user or ctx.author
|
target_user: Union[discord.Member, discord.User] = target_user or ctx.author
|
||||||
target_guild: discord.Guild = target_guild or ctx.guild
|
target_guild: discord.Guild = target_guild or ctx.guild
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,9 @@ class CustomCommands(commands.Cog):
|
|||||||
await ctx.send(_("There already exists a bot command with the same name."))
|
await ctx.send(_("There already exists a bot command with the same name."))
|
||||||
return
|
return
|
||||||
responses = await self.commandobj.get_responses(ctx=ctx)
|
responses = await self.commandobj.get_responses(ctx=ctx)
|
||||||
|
if not responses:
|
||||||
|
await ctx.send(_("Custom command process cancelled."))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ from .downloader import Downloader
|
|||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
cog = Downloader(bot)
|
cog = Downloader(bot)
|
||||||
await cog.initialize()
|
|
||||||
bot.add_cog(cog)
|
bot.add_cog(cog)
|
||||||
|
cog.create_init_task()
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class InstalledCog(InstalledModule):
|
|||||||
|
|
||||||
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
||||||
if cog is None:
|
if cog is None:
|
||||||
raise commands.BadArgument(_("That cog is not installed"))
|
raise commands.BadArgument(
|
||||||
|
_("Cog `{cog_name}` is not installed.").format(cog_name=arg)
|
||||||
|
)
|
||||||
|
|
||||||
return cog
|
return cog
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ _ = Translator("Downloader", __file__)
|
|||||||
|
|
||||||
DEPRECATION_NOTICE = _(
|
DEPRECATION_NOTICE = _(
|
||||||
"\n**WARNING:** The following repos are using shared libraries"
|
"\n**WARNING:** The following repos are using shared libraries"
|
||||||
" which are marked for removal in Red 3.3: {repo_list}.\n"
|
" which are marked for removal in Red 3.4: {repo_list}.\n"
|
||||||
" You should inform maintainers of these repos about this message."
|
" You should inform maintainers of these repos about this message."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,6 +53,9 @@ class Downloader(commands.Cog):
|
|||||||
self._create_lib_folder()
|
self._create_lib_folder()
|
||||||
|
|
||||||
self._repo_manager = RepoManager()
|
self._repo_manager = RepoManager()
|
||||||
|
self._ready = asyncio.Event()
|
||||||
|
self._init_task = None
|
||||||
|
self._ready_raised = False
|
||||||
|
|
||||||
def _create_lib_folder(self, *, remove_first: bool = False) -> None:
|
def _create_lib_folder(self, *, remove_first: bool = False) -> None:
|
||||||
if remove_first:
|
if remove_first:
|
||||||
@@ -62,9 +65,38 @@ class Downloader(commands.Cog):
|
|||||||
with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
|
with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def cog_before_invoke(self, ctx: commands.Context) -> None:
|
||||||
|
async with ctx.typing():
|
||||||
|
await self._ready.wait()
|
||||||
|
if self._ready_raised:
|
||||||
|
await ctx.send(
|
||||||
|
"There was an error during Downloader's initialization."
|
||||||
|
" Check logs for more information."
|
||||||
|
)
|
||||||
|
raise commands.CheckFailure()
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
if self._init_task is not None:
|
||||||
|
self._init_task.cancel()
|
||||||
|
|
||||||
|
def create_init_task(self):
|
||||||
|
def _done_callback(task: asyncio.Task) -> None:
|
||||||
|
exc = task.exception()
|
||||||
|
if exc is not None:
|
||||||
|
log.error(
|
||||||
|
"An unexpected error occurred during Downloader's initialization.",
|
||||||
|
exc_info=exc,
|
||||||
|
)
|
||||||
|
self._ready_raised = True
|
||||||
|
self._ready.set()
|
||||||
|
|
||||||
|
self._init_task = asyncio.create_task(self.initialize())
|
||||||
|
self._init_task.add_done_callback(_done_callback)
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
await self._repo_manager.initialize()
|
await self._repo_manager.initialize()
|
||||||
await self._maybe_update_config()
|
await self._maybe_update_config()
|
||||||
|
self._ready.set()
|
||||||
|
|
||||||
async def _maybe_update_config(self) -> None:
|
async def _maybe_update_config(self) -> None:
|
||||||
schema_version = await self.conf.schema_version()
|
schema_version = await self.conf.schema_version()
|
||||||
@@ -205,7 +237,7 @@ class Downloader(commands.Cog):
|
|||||||
await self.conf.installed_libraries.set(installed_libraries)
|
await self.conf.installed_libraries.set(installed_libraries)
|
||||||
|
|
||||||
async def _shared_lib_load_check(self, cog_name: str) -> Optional[Repo]:
|
async def _shared_lib_load_check(self, cog_name: str) -> Optional[Repo]:
|
||||||
# remove in Red 3.3
|
# remove in Red 3.4
|
||||||
is_installed, cog = await self.is_installed(cog_name)
|
is_installed, cog = await self.is_installed(cog_name)
|
||||||
# it's not gonna be None when `is_installed` is True
|
# it's not gonna be None when `is_installed` is True
|
||||||
# if we'll use typing_extensions in future, `Literal` can solve this
|
# if we'll use typing_extensions in future, `Literal` can solve this
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import datetime
|
|||||||
import time
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from random import randint, choice
|
from random import randint, choice
|
||||||
|
from typing import Final
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
@@ -31,6 +32,9 @@ class RPSParser:
|
|||||||
self.choice = None
|
self.choice = None
|
||||||
|
|
||||||
|
|
||||||
|
MAX_ROLL: Final[int] = 2 ** 64 - 1
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class General(commands.Cog):
|
class General(commands.Cog):
|
||||||
"""General commands."""
|
"""General commands."""
|
||||||
@@ -87,15 +91,21 @@ class General(commands.Cog):
|
|||||||
`<number>` defaults to 100.
|
`<number>` defaults to 100.
|
||||||
"""
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if number > 1:
|
if 1 < number <= MAX_ROLL:
|
||||||
n = randint(1, number)
|
n = randint(1, number)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"{author.mention} :game_die: {n} :game_die:".format(
|
"{author.mention} :game_die: {n} :game_die:".format(
|
||||||
author=author, n=humanize_number(n)
|
author=author, n=humanize_number(n)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
elif number <= 1:
|
||||||
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
|
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_("{author.mention} Max allowed number is {maxamount}.").format(
|
||||||
|
author=author, maxamount=humanize_number(MAX_ROLL)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def flip(self, ctx, user: discord.Member = None):
|
async def flip(self, ctx, user: discord.Member = None):
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class Events(MixinMeta):
|
|||||||
while None in name_list: # clean out null entries from a bug
|
while None in name_list: # clean out null entries from a bug
|
||||||
name_list.remove(None)
|
name_list.remove(None)
|
||||||
if after.name in name_list:
|
if after.name in name_list:
|
||||||
# Ensure order is maintained without duplicates occuring
|
# Ensure order is maintained without duplicates occurring
|
||||||
name_list.remove(after.name)
|
name_list.remove(after.name)
|
||||||
name_list.append(after.name)
|
name_list.append(after.name)
|
||||||
while len(name_list) > 20:
|
while len(name_list) > 20:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import cast, Optional, Union
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, i18n, checks, modlog
|
from redbot.core import commands, i18n, checks, modlog
|
||||||
from redbot.core.utils.chat_formatting import pagify, humanize_number
|
from redbot.core.utils.chat_formatting import pagify, humanize_number, bold
|
||||||
from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
|
from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
|
||||||
from .abc import MixinMeta
|
from .abc import MixinMeta
|
||||||
from .converters import RawUserIds
|
from .converters import RawUserIds
|
||||||
@@ -82,6 +82,19 @@ class KickBanMixin(MixinMeta):
|
|||||||
elif not (0 <= days <= 7):
|
elif not (0 <= days <= 7):
|
||||||
return _("Invalid days. Must be between 0 and 7.")
|
return _("Invalid days. Must be between 0 and 7.")
|
||||||
|
|
||||||
|
toggle = await self.settings.guild(guild).dm_on_kickban()
|
||||||
|
if toggle:
|
||||||
|
with contextlib.suppress(discord.HTTPException):
|
||||||
|
em = discord.Embed(
|
||||||
|
title=bold(_("You have been banned from {guild}.").format(guild=guild))
|
||||||
|
)
|
||||||
|
em.add_field(
|
||||||
|
name=_("**Reason**"),
|
||||||
|
value=reason if reason is not None else _("No reason was given."),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await user.send(embed=em)
|
||||||
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
audit_reason = get_audit_reason(author, reason)
|
||||||
|
|
||||||
queue_entry = (guild.id, user.id)
|
queue_entry = (guild.id, user.id)
|
||||||
@@ -95,7 +108,7 @@ class KickBanMixin(MixinMeta):
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
return _("I'm not allowed to do that.")
|
return _("I'm not allowed to do that.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return e # TODO: impproper return type? Is this intended to be re-raised?
|
return e # TODO: improper return type? Is this intended to be re-raised?
|
||||||
|
|
||||||
if create_modlog_case:
|
if create_modlog_case:
|
||||||
try:
|
try:
|
||||||
@@ -186,6 +199,18 @@ class KickBanMixin(MixinMeta):
|
|||||||
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
|
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
|
||||||
return
|
return
|
||||||
audit_reason = get_audit_reason(author, reason)
|
audit_reason = get_audit_reason(author, reason)
|
||||||
|
toggle = await self.settings.guild(guild).dm_on_kickban()
|
||||||
|
if toggle:
|
||||||
|
with contextlib.suppress(discord.HTTPException):
|
||||||
|
em = discord.Embed(
|
||||||
|
title=bold(_("You have been kicked from {guild}.").format(guild=guild))
|
||||||
|
)
|
||||||
|
em.add_field(
|
||||||
|
name=_("**Reason**"),
|
||||||
|
value=reason if reason is not None else _("No reason was given."),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await user.send(embed=em)
|
||||||
try:
|
try:
|
||||||
await guild.kick(user, reason=audit_reason)
|
await guild.kick(user, reason=audit_reason)
|
||||||
log.info("{}({}) kicked {}({})".format(author.name, author.id, user.name, user.id))
|
log.info("{}({}) kicked {}({})".format(author.name, author.id, user.name, user.id))
|
||||||
@@ -218,14 +243,19 @@ class KickBanMixin(MixinMeta):
|
|||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
user: discord.Member,
|
user: discord.Member,
|
||||||
days: Optional[int] = 0,
|
days: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
reason: str = None,
|
reason: str = None,
|
||||||
):
|
):
|
||||||
"""Ban a user from this server and optionally delete days of messages.
|
"""Ban a user from this server and optionally delete days of messages.
|
||||||
|
|
||||||
If days is not a number, it's treated as the first word of the reason.
|
If days is not a number, it's treated as the first word of the reason.
|
||||||
Minimum 0 days, maximum 7. Defaults to 0."""
|
|
||||||
|
Minimum 0 days, maximum 7. If not specified, defaultdays setting will be used instead."""
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
if days is None:
|
||||||
|
days = await self.settings.guild(guild).default_days()
|
||||||
|
|
||||||
result = await self.ban_user(
|
result = await self.ban_user(
|
||||||
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
|
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
|
||||||
@@ -244,7 +274,7 @@ class KickBanMixin(MixinMeta):
|
|||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
user_ids: commands.Greedy[RawUserIds],
|
user_ids: commands.Greedy[RawUserIds],
|
||||||
days: Optional[int] = 0,
|
days: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
reason: str = None,
|
reason: str = None,
|
||||||
):
|
):
|
||||||
@@ -252,7 +282,6 @@ class KickBanMixin(MixinMeta):
|
|||||||
|
|
||||||
User IDs need to be provided in order to ban
|
User IDs need to be provided in order to ban
|
||||||
using this command"""
|
using this command"""
|
||||||
days = cast(int, days)
|
|
||||||
banned = []
|
banned = []
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
@@ -300,6 +329,9 @@ class KickBanMixin(MixinMeta):
|
|||||||
await show_results()
|
await show_results()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if days is None:
|
||||||
|
days = await self.settings.guild(guild).default_days()
|
||||||
|
|
||||||
for user_id in user_ids:
|
for user_id in user_ids:
|
||||||
user = guild.get_member(user_id)
|
user = guild.get_member(user_id)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class Mod(
|
|||||||
"delete_delay": -1,
|
"delete_delay": -1,
|
||||||
"reinvite_on_unban": False,
|
"reinvite_on_unban": False,
|
||||||
"current_tempbans": [],
|
"current_tempbans": [],
|
||||||
|
"dm_on_kickban": False,
|
||||||
|
"default_days": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
default_channel_settings = {"ignored": False}
|
default_channel_settings = {"ignored": False}
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ class ModSettings(MixinMeta):
|
|||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
# Display current settings
|
# Display current settings
|
||||||
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
data = await self.settings.guild(guild).all()
|
||||||
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
delete_repeats = data["delete_repeats"]
|
||||||
respect_hierarchy = await self.settings.guild(guild).respect_hierarchy()
|
ban_mention_spam = data["ban_mention_spam"]
|
||||||
delete_delay = await self.settings.guild(guild).delete_delay()
|
respect_hierarchy = data["respect_hierarchy"]
|
||||||
reinvite_on_unban = await self.settings.guild(guild).reinvite_on_unban()
|
delete_delay = data["delete_delay"]
|
||||||
|
reinvite_on_unban = data["reinvite_on_unban"]
|
||||||
|
dm_on_kickban = data["dm_on_kickban"]
|
||||||
|
default_days = data["default_days"]
|
||||||
msg = ""
|
msg = ""
|
||||||
msg += _("Delete repeats: {num_repeats}\n").format(
|
msg += _("Delete repeats: {num_repeats}\n").format(
|
||||||
num_repeats=_("after {num} repeats").format(num=delete_repeats)
|
num_repeats=_("after {num} repeats").format(num=delete_repeats)
|
||||||
@@ -48,6 +51,15 @@ class ModSettings(MixinMeta):
|
|||||||
msg += _("Reinvite on unban: {yes_or_no}\n").format(
|
msg += _("Reinvite on unban: {yes_or_no}\n").format(
|
||||||
yes_or_no=_("Yes") if reinvite_on_unban else _("No")
|
yes_or_no=_("Yes") if reinvite_on_unban else _("No")
|
||||||
)
|
)
|
||||||
|
msg += _("Send message to users on kick/ban: {yes_or_no}\n").format(
|
||||||
|
yes_or_no=_("Yes") if dm_on_kickban else _("No")
|
||||||
|
)
|
||||||
|
if default_days:
|
||||||
|
msg += _(
|
||||||
|
"Default message history delete on ban: Previous {num_days} days\n"
|
||||||
|
).format(num_days=default_days)
|
||||||
|
else:
|
||||||
|
msg += _("Default message history delete on ban: Don't delete any\n")
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
@modset.command()
|
@modset.command()
|
||||||
@@ -199,3 +211,44 @@ class ModSettings(MixinMeta):
|
|||||||
command=f"{ctx.prefix}unban"
|
command=f"{ctx.prefix}unban"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@modset.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def dm(self, ctx: commands.Context, enabled: bool = None):
|
||||||
|
"""Toggle whether to send a message to a user when they are
|
||||||
|
kicked/banned.
|
||||||
|
|
||||||
|
If this option is enabled, the bot will attempt to DM the user with the guild name
|
||||||
|
and reason as to why they were kicked/banned.
|
||||||
|
"""
|
||||||
|
guild = ctx.guild
|
||||||
|
if enabled is None:
|
||||||
|
setting = await self.settings.guild(guild).dm_on_kickban()
|
||||||
|
await ctx.send(
|
||||||
|
_("DM when kicked/banned is currently set to: {setting}").format(setting=setting)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await self.settings.guild(guild).dm_on_kickban.set(enabled)
|
||||||
|
if enabled:
|
||||||
|
await ctx.send(_("Bot will now attempt to send a DM to user before kick and ban."))
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_("Bot will no longer attempt to send a DM to user before kick and ban.")
|
||||||
|
)
|
||||||
|
|
||||||
|
@modset.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def defaultdays(self, ctx: commands.Context, days: int = 0):
|
||||||
|
"""Set the default number of days worth of messages to be deleted when a user is banned.
|
||||||
|
|
||||||
|
The number of days must be between 0 and 7.
|
||||||
|
"""
|
||||||
|
guild = ctx.guild
|
||||||
|
if not (0 <= days <= 7):
|
||||||
|
return await ctx.send(_("Invalid number of days. Must be between 0 and 7."))
|
||||||
|
await self.settings.guild(guild).default_days.set(days)
|
||||||
|
await ctx.send(
|
||||||
|
_("{days} days worth of messages will be deleted when a user is banned.").format(
|
||||||
|
days=days
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ At which Arena can you unlock X-Bow?:
|
|||||||
- 6
|
- 6
|
||||||
- Builder's Workshop
|
- Builder's Workshop
|
||||||
At which Arena do you get a chance for Legendary cards to appear in the shop?:
|
At which Arena do you get a chance for Legendary cards to appear in the shop?:
|
||||||
- Hog Mountian
|
- Hog Mountain
|
||||||
- A10
|
- A10
|
||||||
- 10
|
- 10
|
||||||
- Arena 10
|
- Arena 10
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ Porky Pig had a girlfriend named ________?:
|
|||||||
Randy Travis said his love was 'deeper than the ______'?:
|
Randy Travis said his love was 'deeper than the ______'?:
|
||||||
- Holler
|
- Holler
|
||||||
Richard Strauss' majestic overture "Also Sprach Zarathustra" was the theme music for which Stanley Kubrick film?:
|
Richard Strauss' majestic overture "Also Sprach Zarathustra" was the theme music for which Stanley Kubrick film?:
|
||||||
- "2001: A Space Odyessy"
|
- "2001: A Space Odyssey"
|
||||||
Rolling Stones first hit was written by what group?:
|
Rolling Stones first hit was written by what group?:
|
||||||
- The Beatles
|
- The Beatles
|
||||||
Russian modernist Igor _________?:
|
Russian modernist Igor _________?:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class SharedLibImportWarner(MetaPathFinder):
|
|||||||
return None
|
return None
|
||||||
msg = (
|
msg = (
|
||||||
"One of cogs uses shared libraries which are"
|
"One of cogs uses shared libraries which are"
|
||||||
" deprecated and scheduled for removal in Red 3.3.\n"
|
" deprecated and scheduled for removal in Red 3.4.\n"
|
||||||
"You should inform author of the cog about this message."
|
"You should inform author of the cog about this message."
|
||||||
)
|
)
|
||||||
warnings.warn(msg, SharedLibDeprecationWarning, stacklevel=2)
|
warnings.warn(msg, SharedLibDeprecationWarning, stacklevel=2)
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ from typing import (
|
|||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
from discord.ext import commands as dpy_commands
|
||||||
from discord.ext.commands import when_mentioned_or
|
from discord.ext.commands import when_mentioned_or
|
||||||
|
from discord.ext.commands.bot import BotBase
|
||||||
|
|
||||||
from . import Config, i18n, commands, errors, drivers, modlog, bank
|
from . import Config, i18n, commands, errors, drivers, modlog, bank
|
||||||
from .cog_manager import CogManager, CogManagerUI
|
from .cog_manager import CogManager, CogManagerUI
|
||||||
@@ -59,7 +61,9 @@ def _is_submodule(parent, child):
|
|||||||
|
|
||||||
|
|
||||||
# barely spurious warning caused by our intentional shadowing
|
# barely spurious warning caused by our intentional shadowing
|
||||||
class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: disable=no-member
|
class RedBase(
|
||||||
|
commands.GroupMixin, dpy_commands.bot.BotBase, RPCMixin
|
||||||
|
): # pylint: disable=no-member
|
||||||
"""Mixin for the main bot class.
|
"""Mixin for the main bot class.
|
||||||
|
|
||||||
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
||||||
@@ -88,6 +92,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
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__delete_delay=0,
|
||||||
help__use_menus=False,
|
help__use_menus=False,
|
||||||
help__show_hidden=False,
|
help__show_hidden=False,
|
||||||
help__verify_checks=True,
|
help__verify_checks=True,
|
||||||
@@ -119,6 +124,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
autoimmune_ids=[],
|
autoimmune_ids=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._config.register_channel(embeds=None)
|
||||||
self._config.register_user(embeds=None)
|
self._config.register_user(embeds=None)
|
||||||
|
|
||||||
self._config.init_custom(CUSTOM_GROUPS, 2)
|
self._config.init_custom(CUSTOM_GROUPS, 2)
|
||||||
@@ -149,6 +155,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
|
|
||||||
self._main_dir = bot_dir
|
self._main_dir = bot_dir
|
||||||
self._cog_mgr = CogManager()
|
self._cog_mgr = CogManager()
|
||||||
|
self._use_team_features = cli_flags.use_team_features
|
||||||
super().__init__(*args, help_command=None, **kwargs)
|
super().__init__(*args, help_command=None, **kwargs)
|
||||||
# Do not manually use the help formatter attribute here, see `send_help_for`,
|
# Do not manually use the help formatter attribute here, see `send_help_for`,
|
||||||
# for a documented API. The internals of this object are still subject to change.
|
# for a documented API. The internals of this object are still subject to change.
|
||||||
@@ -159,6 +166,16 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
self._red_ready = asyncio.Event()
|
self._red_ready = asyncio.Event()
|
||||||
self._red_before_invoke_objs: Set[PreInvokeCoroutine] = set()
|
self._red_before_invoke_objs: Set[PreInvokeCoroutine] = set()
|
||||||
|
|
||||||
|
def get_command(self, name: str) -> Optional[commands.Command]:
|
||||||
|
com = super().get_command(name)
|
||||||
|
assert com is None or isinstance(com, commands.Command)
|
||||||
|
return com
|
||||||
|
|
||||||
|
def get_cog(self, name: str) -> Optional[commands.Cog]:
|
||||||
|
cog = super().get_cog(name)
|
||||||
|
assert cog is None or isinstance(cog, commands.Cog)
|
||||||
|
return cog
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _before_invoke(self): # DEP-WARN
|
def _before_invoke(self): # DEP-WARN
|
||||||
return self._red_before_invoke_method
|
return self._red_before_invoke_method
|
||||||
@@ -619,6 +636,9 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
if user_setting is not None:
|
if user_setting is not None:
|
||||||
return user_setting
|
return user_setting
|
||||||
else:
|
else:
|
||||||
|
channel_setting = await self._config.channel(channel).embeds()
|
||||||
|
if channel_setting is not None:
|
||||||
|
return channel_setting
|
||||||
guild_setting = await self._config.guild(channel.guild).embeds()
|
guild_setting = await self._config.guild(channel.guild).embeds()
|
||||||
if guild_setting is not None:
|
if guild_setting is not None:
|
||||||
return guild_setting
|
return guild_setting
|
||||||
@@ -626,10 +646,42 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
global_setting = await self._config.embeds()
|
global_setting = await self._config.embeds()
|
||||||
return global_setting
|
return global_setting
|
||||||
|
|
||||||
async def is_owner(self, user) -> bool:
|
async def is_owner(self, user: Union[discord.User, discord.Member]) -> bool:
|
||||||
|
"""
|
||||||
|
Determines if the user should be considered a bot owner.
|
||||||
|
|
||||||
|
This takes into account CLI flags and application ownership.
|
||||||
|
|
||||||
|
By default,
|
||||||
|
application team members are not considered owners,
|
||||||
|
while individual application owners are.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
user: Union[discord.User, discord.Member]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
"""
|
||||||
if user.id in self._co_owners:
|
if user.id in self._co_owners:
|
||||||
return True
|
return True
|
||||||
return await super().is_owner(user)
|
|
||||||
|
if self.owner_id:
|
||||||
|
return self.owner_id == user.id
|
||||||
|
elif self.owner_ids:
|
||||||
|
return user.id in self.owner_ids
|
||||||
|
else:
|
||||||
|
app = await self.application_info()
|
||||||
|
if app.team:
|
||||||
|
if self._use_team_features:
|
||||||
|
self.owner_ids = ids = {m.id for m in app.team.members}
|
||||||
|
return user.id in ids
|
||||||
|
else:
|
||||||
|
self.owner_id = owner_id = app.owner.id
|
||||||
|
return user.id == owner_id
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
async def is_admin(self, member: discord.Member) -> bool:
|
async def is_admin(self, member: discord.Member) -> bool:
|
||||||
"""Checks if a member is an admin of their guild."""
|
"""Checks if a member is an admin of their guild."""
|
||||||
@@ -1068,10 +1120,11 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
await self.wait_until_red_ready()
|
await self.wait_until_red_ready()
|
||||||
destinations = []
|
destinations = []
|
||||||
opt_outs = await self._config.owner_opt_out_list()
|
opt_outs = await self._config.owner_opt_out_list()
|
||||||
for user_id in (self.owner_id, *self._co_owners):
|
team_ids = () if not self._use_team_features else self.owner_ids
|
||||||
|
for user_id in set((self.owner_id, *self._co_owners, *team_ids)):
|
||||||
if user_id not in opt_outs:
|
if user_id not in opt_outs:
|
||||||
user = self.get_user(user_id)
|
user = self.get_user(user_id)
|
||||||
if user:
|
if user and not user.bot: # user.bot is possible with flags and teams
|
||||||
destinations.append(user)
|
destinations.append(user)
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
|
|||||||
@@ -200,6 +200,18 @@ def parse_cli_flags(args):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--team-members-are-owners",
|
||||||
|
action="store_true",
|
||||||
|
dest="use_team_features",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Treat application team members as owners. "
|
||||||
|
"This is off by default. Owners can load and run arbitrary code. "
|
||||||
|
"Do not enable if you would not trust all of your team members with "
|
||||||
|
"all of the data on the host machine."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args(args)
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,145 @@
|
|||||||
from discord.ext.commands import *
|
########## SENSITIVE SECTION WARNING ###########
|
||||||
from .commands import *
|
################################################
|
||||||
from .context import *
|
# Any edits of any of the exported names #
|
||||||
from .converter import *
|
# may result in a breaking change. #
|
||||||
from .errors import *
|
# Ensure no names are removed without warning. #
|
||||||
from .requires import *
|
################################################
|
||||||
from .help import *
|
|
||||||
|
from .commands import (
|
||||||
|
Cog as Cog,
|
||||||
|
CogMixin as CogMixin,
|
||||||
|
CogCommandMixin as CogCommandMixin,
|
||||||
|
CogGroupMixin as CogGroupMixin,
|
||||||
|
Command as Command,
|
||||||
|
Group as Group,
|
||||||
|
GroupMixin as GroupMixin,
|
||||||
|
command as command,
|
||||||
|
group as group,
|
||||||
|
RESERVED_COMMAND_NAMES as RESERVED_COMMAND_NAMES,
|
||||||
|
)
|
||||||
|
from .context import Context as Context, GuildContext as GuildContext, DMContext as DMContext
|
||||||
|
from .converter import (
|
||||||
|
APIToken as APIToken,
|
||||||
|
DictConverter as DictConverter,
|
||||||
|
GuildConverter as GuildConverter,
|
||||||
|
TimedeltaConverter as TimedeltaConverter,
|
||||||
|
get_dict_converter as get_dict_converter,
|
||||||
|
get_timedelta_converter as get_timedelta_converter,
|
||||||
|
parse_timedelta as parse_timedelta,
|
||||||
|
NoParseOptional as NoParseOptional,
|
||||||
|
UserInputOptional as UserInputOptional,
|
||||||
|
Literal as Literal,
|
||||||
|
)
|
||||||
|
from .errors import (
|
||||||
|
ConversionFailure as ConversionFailure,
|
||||||
|
BotMissingPermissions as BotMissingPermissions,
|
||||||
|
UserFeedbackCheckFailure as UserFeedbackCheckFailure,
|
||||||
|
ArgParserFailure as ArgParserFailure,
|
||||||
|
)
|
||||||
|
from .help import (
|
||||||
|
red_help as red_help,
|
||||||
|
RedHelpFormatter as RedHelpFormatter,
|
||||||
|
HelpSettings as HelpSettings,
|
||||||
|
)
|
||||||
|
from .requires import (
|
||||||
|
CheckPredicate as CheckPredicate,
|
||||||
|
DM_PERMS as DM_PERMS,
|
||||||
|
GlobalPermissionModel as GlobalPermissionModel,
|
||||||
|
GuildPermissionModel as GuildPermissionModel,
|
||||||
|
PermissionModel as PermissionModel,
|
||||||
|
PrivilegeLevel as PrivilegeLevel,
|
||||||
|
PermState as PermState,
|
||||||
|
Requires as Requires,
|
||||||
|
permissions_check as permissions_check,
|
||||||
|
bot_has_permissions as bot_has_permissions,
|
||||||
|
has_permissions as has_permissions,
|
||||||
|
has_guild_permissions as has_guild_permissions,
|
||||||
|
is_owner as is_owner,
|
||||||
|
guildowner as guildowner,
|
||||||
|
guildowner_or_permissions as guildowner_or_permissions,
|
||||||
|
admin as admin,
|
||||||
|
admin_or_permissions as admin_or_permissions,
|
||||||
|
mod as mod,
|
||||||
|
mod_or_permissions as mod_or_permissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._dpy_reimplements import (
|
||||||
|
check as check,
|
||||||
|
guild_only as guild_only,
|
||||||
|
cooldown as cooldown,
|
||||||
|
dm_only as dm_only,
|
||||||
|
is_nsfw as is_nsfw,
|
||||||
|
has_role as has_role,
|
||||||
|
has_any_role as has_any_role,
|
||||||
|
bot_has_role as bot_has_role,
|
||||||
|
when_mentioned_or as when_mentioned_or,
|
||||||
|
when_mentioned as when_mentioned,
|
||||||
|
bot_has_any_role as bot_has_any_role,
|
||||||
|
)
|
||||||
|
|
||||||
|
### DEP-WARN: Check this *every* discord.py update
|
||||||
|
from discord.ext.commands import (
|
||||||
|
BadArgument as BadArgument,
|
||||||
|
EmojiConverter as EmojiConverter,
|
||||||
|
InvalidEndOfQuotedStringError as InvalidEndOfQuotedStringError,
|
||||||
|
MemberConverter as MemberConverter,
|
||||||
|
BotMissingRole as BotMissingRole,
|
||||||
|
PrivateMessageOnly as PrivateMessageOnly,
|
||||||
|
HelpCommand as HelpCommand,
|
||||||
|
MinimalHelpCommand as MinimalHelpCommand,
|
||||||
|
DisabledCommand as DisabledCommand,
|
||||||
|
ExtensionFailed as ExtensionFailed,
|
||||||
|
Bot as Bot,
|
||||||
|
NotOwner as NotOwner,
|
||||||
|
CategoryChannelConverter as CategoryChannelConverter,
|
||||||
|
CogMeta as CogMeta,
|
||||||
|
ConversionError as ConversionError,
|
||||||
|
UserInputError as UserInputError,
|
||||||
|
Converter as Converter,
|
||||||
|
InviteConverter as InviteConverter,
|
||||||
|
ExtensionError as ExtensionError,
|
||||||
|
Cooldown as Cooldown,
|
||||||
|
CheckFailure as CheckFailure,
|
||||||
|
MessageConverter as MessageConverter,
|
||||||
|
MissingPermissions as MissingPermissions,
|
||||||
|
BadUnionArgument as BadUnionArgument,
|
||||||
|
DefaultHelpCommand as DefaultHelpCommand,
|
||||||
|
ExtensionNotFound as ExtensionNotFound,
|
||||||
|
UserConverter as UserConverter,
|
||||||
|
MissingRole as MissingRole,
|
||||||
|
CommandOnCooldown as CommandOnCooldown,
|
||||||
|
MissingAnyRole as MissingAnyRole,
|
||||||
|
ExtensionNotLoaded as ExtensionNotLoaded,
|
||||||
|
clean_content as clean_content,
|
||||||
|
CooldownMapping as CooldownMapping,
|
||||||
|
ArgumentParsingError as ArgumentParsingError,
|
||||||
|
RoleConverter as RoleConverter,
|
||||||
|
CommandError as CommandError,
|
||||||
|
TextChannelConverter as TextChannelConverter,
|
||||||
|
UnexpectedQuoteError as UnexpectedQuoteError,
|
||||||
|
Paginator as Paginator,
|
||||||
|
BucketType as BucketType,
|
||||||
|
NoEntryPointError as NoEntryPointError,
|
||||||
|
CommandInvokeError as CommandInvokeError,
|
||||||
|
TooManyArguments as TooManyArguments,
|
||||||
|
Greedy as Greedy,
|
||||||
|
ExpectedClosingQuoteError as ExpectedClosingQuoteError,
|
||||||
|
ColourConverter as ColourConverter,
|
||||||
|
VoiceChannelConverter as VoiceChannelConverter,
|
||||||
|
NSFWChannelRequired as NSFWChannelRequired,
|
||||||
|
IDConverter as IDConverter,
|
||||||
|
MissingRequiredArgument as MissingRequiredArgument,
|
||||||
|
GameConverter as GameConverter,
|
||||||
|
CommandNotFound as CommandNotFound,
|
||||||
|
BotMissingAnyRole as BotMissingAnyRole,
|
||||||
|
NoPrivateMessage as NoPrivateMessage,
|
||||||
|
AutoShardedBot as AutoShardedBot,
|
||||||
|
ExtensionAlreadyLoaded as ExtensionAlreadyLoaded,
|
||||||
|
PartialEmojiConverter as PartialEmojiConverter,
|
||||||
|
check_any as check_any,
|
||||||
|
max_concurrency as max_concurrency,
|
||||||
|
CheckAnyFailure as CheckAnyFailure,
|
||||||
|
MaxConcurrency as MaxConcurrency,
|
||||||
|
MaxConcurrencyReached as MaxConcurrencyReached,
|
||||||
|
bot_has_guild_permissions as bot_has_guild_permissions,
|
||||||
|
)
|
||||||
|
|||||||
126
redbot/core/commands/_dpy_reimplements.py
Normal file
126
redbot/core/commands/_dpy_reimplements.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import inspect
|
||||||
|
import functools
|
||||||
|
from typing import (
|
||||||
|
TypeVar,
|
||||||
|
Callable,
|
||||||
|
Awaitable,
|
||||||
|
Coroutine,
|
||||||
|
Union,
|
||||||
|
Type,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
List,
|
||||||
|
Any,
|
||||||
|
Generator,
|
||||||
|
Protocol,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands as dpy_commands
|
||||||
|
|
||||||
|
# So much of this can be stripped right back out with proper stubs.
|
||||||
|
if not TYPE_CHECKING:
|
||||||
|
from discord.ext.commands import (
|
||||||
|
check as check,
|
||||||
|
guild_only as guild_only,
|
||||||
|
dm_only as dm_only,
|
||||||
|
is_nsfw as is_nsfw,
|
||||||
|
has_role as has_role,
|
||||||
|
has_any_role as has_any_role,
|
||||||
|
bot_has_role as bot_has_role,
|
||||||
|
bot_has_any_role as bot_has_any_role,
|
||||||
|
cooldown as cooldown,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..i18n import Translator
|
||||||
|
from .context import Context
|
||||||
|
from .commands import Command
|
||||||
|
|
||||||
|
|
||||||
|
_ = Translator("nah", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Anything here is either a reimplementation or re-export
|
||||||
|
of a discord.py funtion or class with more lies for mypy
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"check",
|
||||||
|
# "check_any", # discord.py 1.3
|
||||||
|
"guild_only",
|
||||||
|
"dm_only",
|
||||||
|
"is_nsfw",
|
||||||
|
"has_role",
|
||||||
|
"has_any_role",
|
||||||
|
"bot_has_role",
|
||||||
|
"bot_has_any_role",
|
||||||
|
"when_mentioned_or",
|
||||||
|
"cooldown",
|
||||||
|
"when_mentioned",
|
||||||
|
]
|
||||||
|
|
||||||
|
_CT = TypeVar("_CT", bound=Context)
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
_F = TypeVar("_F")
|
||||||
|
CheckType = Union[Callable[[_CT], bool], Callable[[_CT], Coroutine[Any, Any, bool]]]
|
||||||
|
CoroLike = Callable[..., Union[Awaitable[_T], Generator[Any, None, _T]]]
|
||||||
|
|
||||||
|
|
||||||
|
class CheckDecorator(Protocol):
|
||||||
|
predicate: Coroutine[Any, Any, bool]
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __call__(self, func: _CT) -> _CT:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __call__(self, func: CoroLike) -> CoroLike:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
def check(predicate: CheckType) -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def guild_only() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def dm_only() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def is_nsfw() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def has_role() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def has_any_role() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def bot_has_role() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def bot_has_any_role() -> CheckDecorator:
|
||||||
|
...
|
||||||
|
|
||||||
|
def cooldown(rate: int, per: float, type: dpy_commands.BucketType = ...) -> Callable[[_F], _F]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
PrefixCallable = Callable[[dpy_commands.bot.BotBase, discord.Message], List[str]]
|
||||||
|
|
||||||
|
|
||||||
|
def when_mentioned(bot: dpy_commands.bot.BotBase, msg: discord.Message) -> List[str]:
|
||||||
|
return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "]
|
||||||
|
|
||||||
|
|
||||||
|
def when_mentioned_or(*prefixes) -> PrefixCallable:
|
||||||
|
def inner(bot: dpy_commands.bot.BotBase, msg: discord.Message) -> List[str]:
|
||||||
|
r = list(prefixes)
|
||||||
|
r = when_mentioned(bot, msg) + r
|
||||||
|
return r
|
||||||
|
|
||||||
|
return inner
|
||||||
@@ -1,24 +1,53 @@
|
|||||||
"""Module for command helpers and classes.
|
"""Module for command helpers and classes.
|
||||||
|
|
||||||
This module contains extended classes and functions which are intended to
|
This module contains extended classes and functions which are intended to
|
||||||
replace those from the `discord.ext.commands` module.
|
be used instead of those from the `discord.ext.commands` module.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import weakref
|
import weakref
|
||||||
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
from typing import (
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Coroutine,
|
||||||
|
TypeVar,
|
||||||
|
Type,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
MutableMapping,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext.commands import (
|
||||||
|
BadArgument,
|
||||||
|
CommandError,
|
||||||
|
CheckFailure,
|
||||||
|
DisabledCommand,
|
||||||
|
command as dpy_command_deco,
|
||||||
|
Command as DPYCommand,
|
||||||
|
Cog as DPYCog,
|
||||||
|
CogMeta as DPYCogMeta,
|
||||||
|
Group as DPYGroup,
|
||||||
|
Greedy,
|
||||||
|
)
|
||||||
|
|
||||||
from . import converter as converters
|
from . import converter as converters
|
||||||
from .errors import ConversionFailure
|
from .errors import ConversionFailure
|
||||||
from .requires import PermState, PrivilegeLevel, Requires
|
from .requires import PermState, PrivilegeLevel, Requires, PermStateAllowedStates
|
||||||
from ..i18n import Translator
|
from ..i18n import Translator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
# circular import avoidance
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Cog",
|
"Cog",
|
||||||
"CogMixin",
|
"CogMixin",
|
||||||
@@ -38,11 +67,17 @@ RESERVED_COMMAND_NAMES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_ = Translator("commands.commands", __file__)
|
_ = Translator("commands.commands", __file__)
|
||||||
|
DisablerDictType = MutableMapping[discord.Guild, Callable[["Context"], Awaitable[bool]]]
|
||||||
|
|
||||||
|
|
||||||
class CogCommandMixin:
|
class CogCommandMixin:
|
||||||
"""A mixin for cogs and commands."""
|
"""A mixin for cogs and commands."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help(self) -> str:
|
||||||
|
"""To be defined by subclasses"""
|
||||||
|
...
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if isinstance(self, Command):
|
if isinstance(self, Command):
|
||||||
@@ -58,6 +93,45 @@ class CogCommandMixin:
|
|||||||
checks=getattr(decorated, "__requires_checks__", []),
|
checks=getattr(decorated, "__requires_checks__", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def format_text_for_context(self, ctx: "Context", text: str) -> str:
|
||||||
|
"""
|
||||||
|
This formats text based on values in context
|
||||||
|
|
||||||
|
The steps are (currently, roughly) the following:
|
||||||
|
|
||||||
|
- substitute ``[p]`` with ``ctx.clean_prefix``
|
||||||
|
- substitute ``[botname]`` with ``ctx.me.display_name``
|
||||||
|
|
||||||
|
More steps may be added at a later time.
|
||||||
|
|
||||||
|
Cog creators should only override this if they want
|
||||||
|
help text to be modified, and may also want to
|
||||||
|
look at `format_help_for_context` and (for commands only)
|
||||||
|
``format_shortdoc_for_context``
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx: Context
|
||||||
|
text: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
text which has had some portions replaced based on context
|
||||||
|
"""
|
||||||
|
formatting_pattern = re.compile(r"\[p\]|\[botname\]")
|
||||||
|
|
||||||
|
def replacement(m: re.Match) -> str:
|
||||||
|
s = m.group(0)
|
||||||
|
if s == "[p]":
|
||||||
|
return ctx.clean_prefix
|
||||||
|
if s == "[botname]":
|
||||||
|
return ctx.me.display_name
|
||||||
|
# We shouldnt get here:
|
||||||
|
return s
|
||||||
|
|
||||||
|
return formatting_pattern.sub(replacement, text)
|
||||||
|
|
||||||
def format_help_for_context(self, ctx: "Context") -> str:
|
def format_help_for_context(self, ctx: "Context") -> str:
|
||||||
"""
|
"""
|
||||||
This formats the help string based on values in context
|
This formats the help string based on values in context
|
||||||
@@ -88,18 +162,7 @@ class CogCommandMixin:
|
|||||||
# Short circuit out on an empty help string
|
# Short circuit out on an empty help string
|
||||||
return help_str
|
return help_str
|
||||||
|
|
||||||
formatting_pattern = re.compile(r"\[p\]|\[botname\]")
|
return self.format_text_for_context(ctx, help_str)
|
||||||
|
|
||||||
def replacement(m: re.Match) -> str:
|
|
||||||
s = m.group(0)
|
|
||||||
if s == "[p]":
|
|
||||||
return ctx.clean_prefix
|
|
||||||
if s == "[botname]":
|
|
||||||
return ctx.me.display_name
|
|
||||||
# We shouldnt get here:
|
|
||||||
return s
|
|
||||||
|
|
||||||
return formatting_pattern.sub(replacement, help_str)
|
|
||||||
|
|
||||||
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
|
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
|
||||||
"""Actively allow this command for the given model.
|
"""Actively allow this command for the given model.
|
||||||
@@ -182,7 +245,7 @@ class CogCommandMixin:
|
|||||||
self.deny_to(Requires.DEFAULT, guild_id=guild_id)
|
self.deny_to(Requires.DEFAULT, guild_id=guild_id)
|
||||||
|
|
||||||
|
|
||||||
class Command(CogCommandMixin, commands.Command):
|
class Command(CogCommandMixin, DPYCommand):
|
||||||
"""Command class for Red.
|
"""Command class for Red.
|
||||||
|
|
||||||
This should not be created directly, and instead via the decorator.
|
This should not be created directly, and instead via the decorator.
|
||||||
@@ -198,10 +261,21 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
`Requires.checks`.
|
`Requires.checks`.
|
||||||
translator : Translator
|
translator : Translator
|
||||||
A translator for this command's help docstring.
|
A translator for this command's help docstring.
|
||||||
|
ignore_optional_for_conversion : bool
|
||||||
|
A value which can be set to not have discord.py's
|
||||||
|
argument parsing behavior for ``typing.Optional``
|
||||||
|
(type used will be of the inner type instead)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if self.cog:
|
||||||
|
# We need to inject cog as self here
|
||||||
|
return self.callback(self.cog, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return self.callback(*args, **kwargs)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.ignore_optional_for_conversion = kwargs.pop("ignore_optional_for_conversion", False)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._help_override = kwargs.pop("help_override", None)
|
self._help_override = kwargs.pop("help_override", None)
|
||||||
self.translator = kwargs.pop("i18n", None)
|
self.translator = kwargs.pop("i18n", None)
|
||||||
@@ -222,8 +296,62 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
|
|
||||||
# Red specific
|
# Red specific
|
||||||
other.requires = self.requires
|
other.requires = self.requires
|
||||||
|
other.ignore_optional_for_conversion = self.ignore_optional_for_conversion
|
||||||
return other
|
return other
|
||||||
|
|
||||||
|
@property
|
||||||
|
def callback(self):
|
||||||
|
return self._callback
|
||||||
|
|
||||||
|
@callback.setter
|
||||||
|
def callback(self, function):
|
||||||
|
"""
|
||||||
|
Below should be mostly the same as discord.py
|
||||||
|
The only (current) change is to filter out typing.Optional
|
||||||
|
if a user has specified the desire for this behavior
|
||||||
|
"""
|
||||||
|
self._callback = function
|
||||||
|
self.module = function.__module__
|
||||||
|
|
||||||
|
signature = inspect.signature(function)
|
||||||
|
self.params = signature.parameters.copy()
|
||||||
|
|
||||||
|
# PEP-563 allows postponing evaluation of annotations with a __future__
|
||||||
|
# import. When postponed, Parameter.annotation will be a string and must
|
||||||
|
# be replaced with the real value for the converters to work later on
|
||||||
|
for key, value in self.params.items():
|
||||||
|
if isinstance(value.annotation, str):
|
||||||
|
self.params[key] = value = value.replace(
|
||||||
|
annotation=eval(value.annotation, function.__globals__)
|
||||||
|
)
|
||||||
|
|
||||||
|
# fail early for when someone passes an unparameterized Greedy type
|
||||||
|
if value.annotation is Greedy:
|
||||||
|
raise TypeError("Unparameterized Greedy[...] is disallowed in signature.")
|
||||||
|
|
||||||
|
if not self.ignore_optional_for_conversion:
|
||||||
|
continue # reduces indentation compared to alternative
|
||||||
|
|
||||||
|
try:
|
||||||
|
vtype = value.annotation.__origin__
|
||||||
|
if vtype is Union:
|
||||||
|
_NoneType = type if TYPE_CHECKING else type(None)
|
||||||
|
args = value.annotation.__args__
|
||||||
|
if _NoneType in args:
|
||||||
|
args = tuple(a for a in args if a is not _NoneType)
|
||||||
|
if len(args) == 1:
|
||||||
|
# can't have a union of 1 or 0 items
|
||||||
|
# 1 prevents this from becoming 0
|
||||||
|
# we need to prevent 2 become 1
|
||||||
|
# (Don't change that to becoming, it's intentional :musical_note:)
|
||||||
|
self.params[key] = value = value.replace(annotation=args[0])
|
||||||
|
else:
|
||||||
|
# and mypy wretches at the correct Union[args]
|
||||||
|
temp_type = type if TYPE_CHECKING else Union[args]
|
||||||
|
self.params[key] = value = value.replace(annotation=temp_type)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def help(self):
|
def help(self):
|
||||||
"""Help string for this command.
|
"""Help string for this command.
|
||||||
@@ -304,7 +432,7 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
for parent in reversed(self.parents):
|
for parent in reversed(self.parents):
|
||||||
try:
|
try:
|
||||||
result = await parent.can_run(ctx, change_permission_state=True)
|
result = await parent.can_run(ctx, change_permission_state=True)
|
||||||
except commands.CommandError:
|
except CommandError:
|
||||||
result = False
|
result = False
|
||||||
|
|
||||||
if result is False:
|
if result is False:
|
||||||
@@ -323,14 +451,24 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
if not change_permission_state:
|
if not change_permission_state:
|
||||||
ctx.permission_state = original_state
|
ctx.permission_state = original_state
|
||||||
|
|
||||||
async def _verify_checks(self, ctx):
|
async def prepare(self, ctx):
|
||||||
if not self.enabled:
|
ctx.command = self
|
||||||
raise commands.DisabledCommand(f"{self.name} command is disabled")
|
|
||||||
|
|
||||||
if not (await self.can_run(ctx, change_permission_state=True)):
|
if not self.enabled:
|
||||||
raise commands.CheckFailure(
|
raise DisabledCommand(f"{self.name} command is disabled")
|
||||||
f"The check functions for command {self.qualified_name} failed."
|
|
||||||
)
|
if not await self.can_run(ctx, change_permission_state=True):
|
||||||
|
raise CheckFailure(f"The check functions for command {self.qualified_name} failed.")
|
||||||
|
|
||||||
|
if self.cooldown_after_parsing:
|
||||||
|
await self._parse_arguments(ctx)
|
||||||
|
self._prepare_cooldowns(ctx)
|
||||||
|
else:
|
||||||
|
self._prepare_cooldowns(ctx)
|
||||||
|
await self._parse_arguments(ctx)
|
||||||
|
if self._max_concurrency is not None:
|
||||||
|
await self._max_concurrency.acquire(ctx)
|
||||||
|
await self.call_before_hooks(ctx)
|
||||||
|
|
||||||
async def do_conversion(
|
async def do_conversion(
|
||||||
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
||||||
@@ -354,7 +492,7 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await super().do_conversion(ctx, converter, argument, param)
|
return await super().do_conversion(ctx, converter, argument, param)
|
||||||
except commands.BadArgument as exc:
|
except BadArgument as exc:
|
||||||
raise ConversionFailure(converter, argument, param, *exc.args) from exc
|
raise ConversionFailure(converter, argument, param, *exc.args) from exc
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
# Some common converters need special treatment...
|
# Some common converters need special treatment...
|
||||||
@@ -389,7 +527,7 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
can_run = await self.can_run(
|
can_run = await self.can_run(
|
||||||
ctx, check_all_parents=True, change_permission_state=False
|
ctx, check_all_parents=True, change_permission_state=False
|
||||||
)
|
)
|
||||||
except (commands.CheckFailure, commands.errors.DisabledCommand):
|
except (CheckFailure, DisabledCommand):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if can_run is False:
|
if can_run is False:
|
||||||
@@ -509,6 +647,28 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
"""
|
"""
|
||||||
return super().error(coro)
|
return super().error(coro)
|
||||||
|
|
||||||
|
def format_shortdoc_for_context(self, ctx: "Context") -> str:
|
||||||
|
"""
|
||||||
|
This formats the short version of the help
|
||||||
|
tring based on values in context
|
||||||
|
|
||||||
|
See ``format_text_for_context`` for the actual implementation details
|
||||||
|
|
||||||
|
Cog creators may override this in their own command classes
|
||||||
|
as long as the method signature stays the same.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx: Context
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Localized help with some formatting
|
||||||
|
"""
|
||||||
|
sh = self.short_doc
|
||||||
|
return self.format_text_for_context(ctx, sh) if sh else sh
|
||||||
|
|
||||||
|
|
||||||
class GroupMixin(discord.ext.commands.GroupMixin):
|
class GroupMixin(discord.ext.commands.GroupMixin):
|
||||||
"""Mixin for `Group` and `Red` classes.
|
"""Mixin for `Group` and `Red` classes.
|
||||||
@@ -545,10 +705,9 @@ class GroupMixin(discord.ext.commands.GroupMixin):
|
|||||||
|
|
||||||
class CogGroupMixin:
|
class CogGroupMixin:
|
||||||
requires: Requires
|
requires: Requires
|
||||||
all_commands: Dict[str, Command]
|
|
||||||
|
|
||||||
def reevaluate_rules_for(
|
def reevaluate_rules_for(
|
||||||
self, model_id: Union[str, int], guild_id: Optional[int]
|
self, model_id: Union[str, int], guild_id: int = 0
|
||||||
) -> Tuple[PermState, bool]:
|
) -> Tuple[PermState, bool]:
|
||||||
"""Re-evaluate a rule by checking subcommand rules.
|
"""Re-evaluate a rule by checking subcommand rules.
|
||||||
|
|
||||||
@@ -571,15 +730,16 @@ class CogGroupMixin:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
if cur_rule in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
|
if cur_rule not in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
|
||||||
# These three states are unaffected by subcommand rules
|
# The above three states are unaffected by subcommand rules
|
||||||
return cur_rule, False
|
|
||||||
else:
|
|
||||||
# Remaining states can be changed if there exists no actively-allowed
|
# Remaining states can be changed if there exists no actively-allowed
|
||||||
# subcommand (this includes subcommands multiple levels below)
|
# subcommand (this includes subcommands multiple levels below)
|
||||||
|
|
||||||
|
all_commands: Dict[str, Command] = getattr(self, "all_commands", {})
|
||||||
|
|
||||||
if any(
|
if any(
|
||||||
cmd.requires.get_rule(model_id, guild_id=guild_id) in PermState.ALLOWED_STATES
|
cmd.requires.get_rule(model_id, guild_id=guild_id) in PermStateAllowedStates
|
||||||
for cmd in self.all_commands.values()
|
for cmd in all_commands.values()
|
||||||
):
|
):
|
||||||
return cur_rule, False
|
return cur_rule, False
|
||||||
elif cur_rule is PermState.PASSIVE_ALLOW:
|
elif cur_rule is PermState.PASSIVE_ALLOW:
|
||||||
@@ -589,8 +749,11 @@ class CogGroupMixin:
|
|||||||
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
|
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
|
||||||
return PermState.ACTIVE_DENY, True
|
return PermState.ACTIVE_DENY, True
|
||||||
|
|
||||||
|
# Default return value
|
||||||
|
return cur_rule, False
|
||||||
|
|
||||||
class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
|
|
||||||
|
class Group(GroupMixin, Command, CogGroupMixin, DPYGroup):
|
||||||
"""Group command class for Red.
|
"""Group command class for Red.
|
||||||
|
|
||||||
This class inherits from `Command`, with :class:`GroupMixin` and
|
This class inherits from `Command`, with :class:`GroupMixin` and
|
||||||
@@ -618,14 +781,14 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
|
|||||||
|
|
||||||
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
||||||
if self.autohelp and not self.invoke_without_command:
|
if self.autohelp and not self.invoke_without_command:
|
||||||
await self._verify_checks(ctx)
|
await self.can_run(ctx, change_permission_state=True)
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
elif self.invoke_without_command:
|
elif self.invoke_without_command:
|
||||||
# So invoke_without_command when a subcommand of this group is invoked
|
# So invoke_without_command when a subcommand of this group is invoked
|
||||||
# will skip the the invokation of *this* command. However, because of
|
# will skip the the invokation of *this* command. However, because of
|
||||||
# how our permissions system works, we don't want it to skip the checks
|
# how our permissions system works, we don't want it to skip the checks
|
||||||
# as well.
|
# as well.
|
||||||
await self._verify_checks(ctx)
|
await self.can_run(ctx, change_permission_state=True)
|
||||||
# this is actually why we don't prepare earlier.
|
# this is actually why we don't prepare earlier.
|
||||||
|
|
||||||
await super().invoke(ctx)
|
await super().invoke(ctx)
|
||||||
@@ -634,14 +797,6 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
|
|||||||
class CogMixin(CogGroupMixin, CogCommandMixin):
|
class CogMixin(CogGroupMixin, CogCommandMixin):
|
||||||
"""Mixin class for a cog, intended for use with discord.py's cog class"""
|
"""Mixin class for a cog, intended for use with discord.py's cog class"""
|
||||||
|
|
||||||
@property
|
|
||||||
def all_commands(self) -> Dict[str, Command]:
|
|
||||||
"""
|
|
||||||
This does not have identical behavior to
|
|
||||||
Group.all_commands but should return what you expect
|
|
||||||
"""
|
|
||||||
return {cmd.name: cmd for cmd in self.__cog_commands__}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def help(self):
|
def help(self):
|
||||||
doc = self.__doc__
|
doc = self.__doc__
|
||||||
@@ -670,7 +825,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
can_run = await self.requires.verify(ctx)
|
can_run = await self.requires.verify(ctx)
|
||||||
except commands.CommandError:
|
except CommandError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return can_run
|
return can_run
|
||||||
@@ -699,16 +854,22 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
|
|||||||
return await self.can_run(ctx)
|
return await self.can_run(ctx)
|
||||||
|
|
||||||
|
|
||||||
class Cog(CogMixin, commands.Cog):
|
class Cog(CogMixin, DPYCog, metaclass=DPYCogMeta):
|
||||||
"""
|
"""
|
||||||
Red's Cog base class
|
Red's Cog base class
|
||||||
|
|
||||||
This includes a metaclass from discord.py
|
This includes a metaclass from discord.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NB: Do not move the inheritcance of this. Keeping the mix of that metaclass
|
__cog_commands__: Tuple[Command]
|
||||||
# seperate gives us more freedoms in several places.
|
|
||||||
pass
|
@property
|
||||||
|
def all_commands(self) -> Dict[str, Command]:
|
||||||
|
"""
|
||||||
|
This does not have identical behavior to
|
||||||
|
Group.all_commands but should return what you expect
|
||||||
|
"""
|
||||||
|
return {cmd.name: cmd for cmd in self.__cog_commands__}
|
||||||
|
|
||||||
|
|
||||||
def command(name=None, cls=Command, **attrs):
|
def command(name=None, cls=Command, **attrs):
|
||||||
@@ -717,7 +878,8 @@ def command(name=None, cls=Command, **attrs):
|
|||||||
Same interface as `discord.ext.commands.command`.
|
Same interface as `discord.ext.commands.command`.
|
||||||
"""
|
"""
|
||||||
attrs["help_override"] = attrs.pop("help", None)
|
attrs["help_override"] = attrs.pop("help", None)
|
||||||
return commands.command(name, cls, **attrs)
|
|
||||||
|
return dpy_command_deco(name, cls, **attrs)
|
||||||
|
|
||||||
|
|
||||||
def group(name=None, cls=Group, **attrs):
|
def group(name=None, cls=Group, **attrs):
|
||||||
@@ -725,10 +887,10 @@ def group(name=None, cls=Group, **attrs):
|
|||||||
|
|
||||||
Same interface as `discord.ext.commands.group`.
|
Same interface as `discord.ext.commands.group`.
|
||||||
"""
|
"""
|
||||||
return command(name, cls, **attrs)
|
return dpy_command_deco(name, cls, **attrs)
|
||||||
|
|
||||||
|
|
||||||
__command_disablers = weakref.WeakValueDictionary()
|
__command_disablers: DisablerDictType = weakref.WeakValueDictionary()
|
||||||
|
|
||||||
|
|
||||||
def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitable[bool]]:
|
def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitable[bool]]:
|
||||||
@@ -743,7 +905,7 @@ def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitabl
|
|||||||
|
|
||||||
async def disabler(ctx: "Context") -> bool:
|
async def disabler(ctx: "Context") -> bool:
|
||||||
if ctx.guild == guild:
|
if ctx.guild == guild:
|
||||||
raise commands.DisabledCommand()
|
raise DisabledCommand()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
__command_disablers[guild] = disabler
|
__command_disablers[guild] = disabler
|
||||||
@@ -771,6 +933,3 @@ class _AlwaysAvailableCommand(Command):
|
|||||||
|
|
||||||
async def can_run(self, ctx, *args, **kwargs) -> bool:
|
async def can_run(self, ctx, *args, **kwargs) -> bool:
|
||||||
return not ctx.author.bot
|
return not ctx.author.bot
|
||||||
|
|
||||||
async def _verify_checks(self, ctx) -> bool:
|
|
||||||
return not ctx.author.bot
|
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Iterable, List, Union
|
from typing import Iterable, List, Union, Optional, TYPE_CHECKING
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext.commands import Context as DPYContext
|
||||||
|
|
||||||
from .requires import PermState
|
from .requires import PermState
|
||||||
from ..utils.chat_formatting import box
|
from ..utils.chat_formatting import box
|
||||||
from ..utils.predicates import MessagePredicate
|
from ..utils.predicates import MessagePredicate
|
||||||
from ..utils import common_filters
|
from ..utils import common_filters
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .commands import Command
|
||||||
|
from ..bot import Red
|
||||||
|
|
||||||
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
|
||||||
__all__ = ["Context"]
|
__all__ = ["Context", "GuildContext", "DMContext"]
|
||||||
|
|
||||||
|
|
||||||
class Context(commands.Context):
|
class Context(DPYContext):
|
||||||
"""Command invocation context for Red.
|
"""Command invocation context for Red.
|
||||||
|
|
||||||
All context passed into commands will be of this type.
|
All context passed into commands will be of this type.
|
||||||
@@ -40,6 +47,10 @@ class Context(commands.Context):
|
|||||||
The permission state the current context is in.
|
The permission state the current context is in.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
command: "Command"
|
||||||
|
invoked_subcommand: "Optional[Command]"
|
||||||
|
bot: "Red"
|
||||||
|
|
||||||
def __init__(self, **attrs):
|
def __init__(self, **attrs):
|
||||||
self.assume_yes = attrs.pop("assume_yes", False)
|
self.assume_yes = attrs.pop("assume_yes", False)
|
||||||
super().__init__(**attrs)
|
super().__init__(**attrs)
|
||||||
@@ -254,7 +265,7 @@ class Context(commands.Context):
|
|||||||
return pattern.sub(f"@{me.display_name}", self.prefix)
|
return pattern.sub(f"@{me.display_name}", self.prefix)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def me(self) -> discord.abc.User:
|
def me(self) -> Union[discord.ClientUser, discord.Member]:
|
||||||
"""discord.abc.User: The bot member or user object.
|
"""discord.abc.User: The bot member or user object.
|
||||||
|
|
||||||
If the context is DM, this will be a `discord.User` object.
|
If the context is DM, this will be a `discord.User` object.
|
||||||
@@ -263,3 +274,63 @@ class Context(commands.Context):
|
|||||||
return self.guild.me
|
return self.guild.me
|
||||||
else:
|
else:
|
||||||
return self.bot.user
|
return self.bot.user
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING or os.getenv("BUILDING_DOCS", False):
|
||||||
|
|
||||||
|
class DMContext(Context):
|
||||||
|
"""
|
||||||
|
At runtime, this will still be a normal context object.
|
||||||
|
|
||||||
|
This lies about some type narrowing for type analysis in commands
|
||||||
|
using a dm_only decorator.
|
||||||
|
|
||||||
|
It is only correct to use when those types are already narrowed
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def author(self) -> discord.User:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel(self) -> discord.DMChannel:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guild(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def me(self) -> discord.ClientUser:
|
||||||
|
...
|
||||||
|
|
||||||
|
class GuildContext(Context):
|
||||||
|
"""
|
||||||
|
At runtime, this will still be a normal context object.
|
||||||
|
|
||||||
|
This lies about some type narrowing for type analysis in commands
|
||||||
|
using a guild_only decorator.
|
||||||
|
|
||||||
|
It is only correct to use when those types are already narrowed
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def author(self) -> discord.Member:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel(self) -> discord.TextChannel:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guild(self) -> discord.Guild:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def me(self) -> discord.Member:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
GuildContext = Context
|
||||||
|
DMContext = Context
|
||||||
|
|||||||
@@ -1,14 +1,33 @@
|
|||||||
|
"""
|
||||||
|
commands.converter
|
||||||
|
==================
|
||||||
|
This module contains useful functions and classes for command argument conversion.
|
||||||
|
|
||||||
|
Some of the converters within are included provisionaly and are marked as such.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import functools
|
import functools
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING, Optional, List, Dict
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Generic,
|
||||||
|
Optional,
|
||||||
|
Optional as NoParseOptional,
|
||||||
|
Tuple,
|
||||||
|
List,
|
||||||
|
Dict,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Literal as Literal,
|
||||||
|
)
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as dpy_commands
|
from discord.ext import commands as dpy_commands
|
||||||
|
from discord.ext.commands import BadArgument
|
||||||
|
|
||||||
from . import BadArgument
|
|
||||||
from ..i18n import Translator
|
from ..i18n import Translator
|
||||||
from ..utils.chat_formatting import humanize_timedelta
|
from ..utils.chat_formatting import humanize_timedelta, humanize_list
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .context import Context
|
from .context import Context
|
||||||
@@ -17,10 +36,13 @@ __all__ = [
|
|||||||
"APIToken",
|
"APIToken",
|
||||||
"DictConverter",
|
"DictConverter",
|
||||||
"GuildConverter",
|
"GuildConverter",
|
||||||
|
"UserInputOptional",
|
||||||
|
"NoParseOptional",
|
||||||
"TimedeltaConverter",
|
"TimedeltaConverter",
|
||||||
"get_dict_converter",
|
"get_dict_converter",
|
||||||
"get_timedelta_converter",
|
"get_timedelta_converter",
|
||||||
"parse_timedelta",
|
"parse_timedelta",
|
||||||
|
"Literal",
|
||||||
]
|
]
|
||||||
|
|
||||||
_ = Translator("commands.converter", __file__)
|
_ = Translator("commands.converter", __file__)
|
||||||
@@ -67,7 +89,7 @@ def parse_timedelta(
|
|||||||
allowed_units : Optional[List[str]]
|
allowed_units : Optional[List[str]]
|
||||||
If provided, you can constrain a user to expressing the amount of time
|
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
|
in specific units. The units you can chose to provide are the same as the
|
||||||
parser understands. `weeks` `days` `hours` `minutes` `seconds`
|
parser understands. (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -138,17 +160,18 @@ 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 a space, comma, or semicolon 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.
|
Note: Core usage of this has been replaced with `DictConverter` use instead.
|
||||||
|
|
||||||
This may be removed at a later date (with warning)
|
.. warning::
|
||||||
|
This will be removed in version 3.4.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx, argument) -> dict:
|
async def convert(self, ctx: "Context", argument) -> dict:
|
||||||
bot = ctx.bot
|
bot = ctx.bot
|
||||||
result = {}
|
result = {}
|
||||||
match = re.split(r";|,| ", argument)
|
match = re.split(r";|,| ", argument)
|
||||||
@@ -162,7 +185,16 @@ class APIToken(discord.ext.commands.Converter):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class DictConverter(dpy_commands.Converter):
|
# Below this line are a lot of lies for mypy about things that *end up* correct when
|
||||||
|
# These are used for command conversion purposes. Please refer to the portion
|
||||||
|
# which is *not* for type checking for the actual implementation
|
||||||
|
# and ensure the lies stay correct for how the object should look as a typehint
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
DictConverter = Dict[str, str]
|
||||||
|
else:
|
||||||
|
|
||||||
|
class DictConverter(dpy_commands.Converter):
|
||||||
"""
|
"""
|
||||||
Converts pairs of space seperated values to a dict
|
Converts pairs of space seperated values to a dict
|
||||||
"""
|
"""
|
||||||
@@ -173,7 +205,6 @@ class DictConverter(dpy_commands.Converter):
|
|||||||
self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
|
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]:
|
async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
|
||||||
|
|
||||||
ret: Dict[str, str] = {}
|
ret: Dict[str, str] = {}
|
||||||
args = self.pattern.split(argument)
|
args = self.pattern.split(argument)
|
||||||
|
|
||||||
@@ -191,12 +222,20 @@ class DictConverter(dpy_commands.Converter):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> type:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> Type[dict]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> Type[dict]:
|
||||||
"""
|
"""
|
||||||
Returns a typechecking safe `DictConverter` suitable for use with discord.py
|
Returns a typechecking safe `DictConverter` suitable for use with discord.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class PartialMeta(type(DictConverter)):
|
class PartialMeta(type):
|
||||||
__call__ = functools.partialmethod(
|
__call__ = functools.partialmethod(
|
||||||
type(DictConverter).__call__, *expected_keys, delims=delims
|
type(DictConverter).__call__, *expected_keys, delims=delims
|
||||||
)
|
)
|
||||||
@@ -207,7 +246,11 @@ def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None)
|
|||||||
return ValidatedConverter
|
return ValidatedConverter
|
||||||
|
|
||||||
|
|
||||||
class TimedeltaConverter(dpy_commands.Converter):
|
if TYPE_CHECKING:
|
||||||
|
TimedeltaConverter = timedelta
|
||||||
|
else:
|
||||||
|
|
||||||
|
class TimedeltaConverter(dpy_commands.Converter):
|
||||||
"""
|
"""
|
||||||
This is a converter for timedeltas.
|
This is a converter for timedeltas.
|
||||||
The units should be in order from largest to smallest.
|
The units should be in order from largest to smallest.
|
||||||
@@ -223,11 +266,11 @@ class TimedeltaConverter(dpy_commands.Converter):
|
|||||||
If provided, any parsed value lower than this will raise an exception
|
If provided, any parsed value lower than this will raise an exception
|
||||||
allowed_units : Optional[List[str]]
|
allowed_units : Optional[List[str]]
|
||||||
If provided, you can constrain a user to expressing the amount of time
|
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
|
in specific units. The units you can choose to provide are the same as the
|
||||||
parser understands: `weeks` `days` `hours` `minutes` `seconds`
|
parser understands: (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
||||||
default_unit : Optional[str]
|
default_unit : Optional[str]
|
||||||
If provided, it will additionally try to match integer-only input into
|
If provided, it will additionally try to match integer-only input into
|
||||||
a timedelta, using the unit specified. Same units as in `allowed_units`
|
a timedelta, using the unit specified. Same units as in ``allowed_units``
|
||||||
apply.
|
apply.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -239,26 +282,41 @@ class TimedeltaConverter(dpy_commands.Converter):
|
|||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str) -> timedelta:
|
async def convert(self, ctx: "Context", argument: str) -> timedelta:
|
||||||
if self.default_unit and argument.isdecimal():
|
if self.default_unit and argument.isdecimal():
|
||||||
delta = timedelta(**{self.default_unit: int(argument)})
|
argument = argument + self.default_unit
|
||||||
else:
|
|
||||||
delta = parse_timedelta(
|
delta = parse_timedelta(
|
||||||
argument,
|
argument,
|
||||||
minimum=self.minimum,
|
minimum=self.minimum,
|
||||||
maximum=self.maximum,
|
maximum=self.maximum,
|
||||||
allowed_units=self.allowed_units,
|
allowed_units=self.allowed_units,
|
||||||
)
|
)
|
||||||
|
|
||||||
if delta is not None:
|
if delta is not None:
|
||||||
return delta
|
return delta
|
||||||
raise BadArgument() # This allows this to be a required argument.
|
raise BadArgument() # This allows this to be a required argument.
|
||||||
|
|
||||||
|
|
||||||
def get_timedelta_converter(
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
def get_timedelta_converter(
|
||||||
*,
|
*,
|
||||||
default_unit: Optional[str] = None,
|
default_unit: Optional[str] = None,
|
||||||
maximum: Optional[timedelta] = None,
|
maximum: Optional[timedelta] = None,
|
||||||
minimum: Optional[timedelta] = None,
|
minimum: Optional[timedelta] = None,
|
||||||
allowed_units: Optional[List[str]] = None,
|
allowed_units: Optional[List[str]] = None,
|
||||||
) -> type:
|
) -> Type[timedelta]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def get_timedelta_converter(
|
||||||
|
*,
|
||||||
|
default_unit: Optional[str] = None,
|
||||||
|
maximum: Optional[timedelta] = None,
|
||||||
|
minimum: Optional[timedelta] = None,
|
||||||
|
allowed_units: Optional[List[str]] = None,
|
||||||
|
) -> Type[timedelta]:
|
||||||
"""
|
"""
|
||||||
This creates a type suitable for typechecking which works with discord.py's
|
This creates a type suitable for typechecking which works with discord.py's
|
||||||
commands.
|
commands.
|
||||||
@@ -273,11 +331,11 @@ def get_timedelta_converter(
|
|||||||
If provided, any parsed value lower than this will raise an exception
|
If provided, any parsed value lower than this will raise an exception
|
||||||
allowed_units : Optional[List[str]]
|
allowed_units : Optional[List[str]]
|
||||||
If provided, you can constrain a user to expressing the amount of time
|
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
|
in specific units. The units you can choose to provide are the same as the
|
||||||
parser understands: `weeks` `days` `hours` `minutes` `seconds`
|
parser understands: (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
||||||
default_unit : Optional[str]
|
default_unit : Optional[str]
|
||||||
If provided, it will additionally try to match integer-only input into
|
If provided, it will additionally try to match integer-only input into
|
||||||
a timedelta, using the unit specified. Same units as in `allowed_units`
|
a timedelta, using the unit specified. Same units as in ``allowed_units``
|
||||||
apply.
|
apply.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -286,7 +344,7 @@ def get_timedelta_converter(
|
|||||||
The converter class, which will be a subclass of `TimedeltaConverter`
|
The converter class, which will be a subclass of `TimedeltaConverter`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class PartialMeta(type(TimedeltaConverter)):
|
class PartialMeta(type):
|
||||||
__call__ = functools.partialmethod(
|
__call__ = functools.partialmethod(
|
||||||
type(DictConverter).__call__,
|
type(DictConverter).__call__,
|
||||||
allowed_units=allowed_units,
|
allowed_units=allowed_units,
|
||||||
@@ -299,3 +357,91 @@ def get_timedelta_converter(
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return ValidatedConverter
|
return ValidatedConverter
|
||||||
|
|
||||||
|
|
||||||
|
if not TYPE_CHECKING:
|
||||||
|
|
||||||
|
class NoParseOptional:
|
||||||
|
"""
|
||||||
|
This can be used instead of `typing.Optional`
|
||||||
|
to avoid discord.py special casing the conversion behavior.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This converter class is still provisional.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
The `ignore_optional_for_conversion` option of commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __class_getitem__(cls, key):
|
||||||
|
if isinstance(key, tuple):
|
||||||
|
raise TypeError("Must only provide a single type to Optional")
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
_T_OPT = TypeVar("_T_OPT", bound=Type)
|
||||||
|
|
||||||
|
if TYPE_CHECKING or os.getenv("BUILDING_DOCS", False):
|
||||||
|
|
||||||
|
class UserInputOptional(Generic[_T_OPT]):
|
||||||
|
"""
|
||||||
|
This can be used when user input should be converted as discord.py
|
||||||
|
treats `typing.Optional`, but the type should not be equivalent to
|
||||||
|
``typing.Union[DesiredType, None]`` for type checking.
|
||||||
|
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This converter class is still provisional.
|
||||||
|
|
||||||
|
This class may not play well with mypy yet
|
||||||
|
and may still require you guard this in a
|
||||||
|
type checking conditional import vs the desired types
|
||||||
|
|
||||||
|
We're aware and looking into improving this.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __class_getitem__(cls, key: _T_OPT) -> _T_OPT:
|
||||||
|
if isinstance(key, tuple):
|
||||||
|
raise TypeError("Must only provide a single type to Optional")
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
UserInputOptional = Optional
|
||||||
|
|
||||||
|
|
||||||
|
if not TYPE_CHECKING:
|
||||||
|
|
||||||
|
class Literal(dpy_commands.Converter):
|
||||||
|
"""
|
||||||
|
This can be used as a converter for `typing.Literal`.
|
||||||
|
|
||||||
|
In a type checking context it is `typing.Literal`.
|
||||||
|
In a runtime context, it's a converter which only matches the literals it was given.
|
||||||
|
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This converter class is still provisional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, valid_names: Tuple[str]):
|
||||||
|
self.valid_names = valid_names
|
||||||
|
|
||||||
|
def __call__(self, ctx, arg):
|
||||||
|
# Callable's are treated as valid types:
|
||||||
|
# https://github.com/python/cpython/blob/3.8/Lib/typing.py#L148
|
||||||
|
# Without this, ``typing.Union[Literal["clear"], bool]`` would fail
|
||||||
|
return self.convert(ctx, arg)
|
||||||
|
|
||||||
|
async def convert(self, ctx, arg):
|
||||||
|
if arg in self.valid_names:
|
||||||
|
return arg
|
||||||
|
raise BadArgument(_("Expected one of: {}").format(humanize_list(self.valid_names)))
|
||||||
|
|
||||||
|
def __class_getitem__(cls, k):
|
||||||
|
if not k:
|
||||||
|
raise ValueError("Need at least one value for Literal")
|
||||||
|
if isinstance(k, tuple):
|
||||||
|
return cls(k)
|
||||||
|
else:
|
||||||
|
return cls((k,))
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from . import commands
|
|||||||
from .context import Context
|
from .context import Context
|
||||||
from ..i18n import Translator
|
from ..i18n import Translator
|
||||||
from ..utils import menus
|
from ..utils import menus
|
||||||
|
from ..utils.mod import mass_purge
|
||||||
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
|
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
|
||||||
from ..utils.chat_formatting import box, pagify
|
from ..utils.chat_formatting import box, pagify
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ class RedHelpFormatter:
|
|||||||
return a_line[:67] + "..."
|
return a_line[:67] + "..."
|
||||||
|
|
||||||
subtext = "\n".join(
|
subtext = "\n".join(
|
||||||
shorten_line(f"**{name}** {command.short_doc}")
|
shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}")
|
||||||
for name, command in sorted(subcommands.items())
|
for name, command in sorted(subcommands.items())
|
||||||
)
|
)
|
||||||
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
|
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
|
||||||
@@ -248,7 +249,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 = com.short_doc
|
doc = com.format_shortdoc_for_context(ctx)
|
||||||
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
|
||||||
@@ -398,7 +399,7 @@ class RedHelpFormatter:
|
|||||||
return a_line[:67] + "..."
|
return a_line[:67] + "..."
|
||||||
|
|
||||||
command_text = "\n".join(
|
command_text = "\n".join(
|
||||||
shorten_line(f"**{name}** {command.short_doc}")
|
shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}")
|
||||||
for name, command in sorted(coms.items())
|
for name, command in sorted(coms.items())
|
||||||
)
|
)
|
||||||
for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)):
|
for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)):
|
||||||
@@ -422,7 +423,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 = com.short_doc
|
doc = com.format_shortdoc_for_context(ctx)
|
||||||
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
|
||||||
@@ -465,7 +466,7 @@ class RedHelpFormatter:
|
|||||||
return a_line[:67] + "..."
|
return a_line[:67] + "..."
|
||||||
|
|
||||||
cog_text = "\n".join(
|
cog_text = "\n".join(
|
||||||
shorten_line(f"**{name}** {command.short_doc}")
|
shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}")
|
||||||
for name, command in sorted(data.items())
|
for name, command in sorted(data.items())
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -493,7 +494,7 @@ class RedHelpFormatter:
|
|||||||
doc_max_width = 80 - max_width
|
doc_max_width = 80 - max_width
|
||||||
for nm, com in cmds:
|
for nm, com in cmds:
|
||||||
width_gap = discord.utils._string_width(nm) - len(nm)
|
width_gap = discord.utils._string_width(nm) - len(nm)
|
||||||
doc = com.short_doc
|
doc = com.format_shortdoc_for_context(ctx)
|
||||||
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
|
||||||
@@ -627,18 +628,24 @@ class RedHelpFormatter:
|
|||||||
Sends pages based on settings.
|
Sends pages based on settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not (
|
# save on config calls
|
||||||
ctx.channel.permissions_for(ctx.me).add_reactions
|
config_help = await ctx.bot._config.help()
|
||||||
and await ctx.bot._config.help.use_menus()
|
channel_permissions = ctx.channel.permissions_for(ctx.me)
|
||||||
):
|
|
||||||
|
|
||||||
max_pages_in_guild = await ctx.bot._config.help.max_pages_in_guild()
|
if not (channel_permissions.add_reactions and config_help["use_menus"]):
|
||||||
destination = ctx.author if len(pages) > max_pages_in_guild else ctx
|
|
||||||
|
|
||||||
if embed:
|
max_pages_in_guild = config_help["max_pages_in_guild"]
|
||||||
|
use_DMs = len(pages) > max_pages_in_guild
|
||||||
|
destination = ctx.author if use_DMs else ctx.channel
|
||||||
|
delete_delay = config_help["delete_delay"]
|
||||||
|
|
||||||
|
messages: List[discord.Message] = []
|
||||||
for page in pages:
|
for page in pages:
|
||||||
try:
|
try:
|
||||||
await destination.send(embed=page)
|
if embed:
|
||||||
|
msg = await destination.send(embed=page)
|
||||||
|
else:
|
||||||
|
msg = await destination.send(page)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
T_(
|
T_(
|
||||||
@@ -647,16 +654,26 @@ class RedHelpFormatter:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for page in pages:
|
messages.append(msg)
|
||||||
try:
|
|
||||||
await destination.send(page)
|
# The if statement takes into account that 'destination' will be
|
||||||
except discord.Forbidden:
|
# the context channel in non-DM context, reusing 'channel_permissions' to avoid
|
||||||
return await ctx.send(
|
# computing the permissions twice.
|
||||||
T_(
|
if (
|
||||||
"I couldn't send the help message to you in DM. "
|
not use_DMs # we're not in DMs
|
||||||
"Either you blocked me or you disabled DMs in this server."
|
and delete_delay > 0 # delete delay is enabled
|
||||||
)
|
and channel_permissions.manage_messages # we can manage messages here
|
||||||
)
|
):
|
||||||
|
|
||||||
|
# We need to wrap this in a task to not block after-sending-help interactions.
|
||||||
|
# The channel has to be TextChannel as we can't bulk-delete from DMs
|
||||||
|
async def _delete_delay_help(
|
||||||
|
channel: discord.TextChannel, messages: List[discord.Message], delay: int
|
||||||
|
):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
await mass_purge(messages, channel)
|
||||||
|
|
||||||
|
asyncio.create_task(_delete_delay_help(destination, messages, delete_delay))
|
||||||
else:
|
else:
|
||||||
# Specifically ensuring the menu's message is sent prior to returning
|
# 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]))
|
m = await (ctx.send(embed=pages[0]) if embed else ctx.send(pages[0]))
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ checks like bot permissions checks.
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import enum
|
import enum
|
||||||
|
import inspect
|
||||||
|
from collections import ChainMap
|
||||||
from typing import (
|
from typing import (
|
||||||
Union,
|
Union,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -20,6 +22,7 @@ from typing import (
|
|||||||
TypeVar,
|
TypeVar,
|
||||||
Tuple,
|
Tuple,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
|
Mapping,
|
||||||
)
|
)
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -45,6 +48,7 @@ __all__ = [
|
|||||||
"permissions_check",
|
"permissions_check",
|
||||||
"bot_has_permissions",
|
"bot_has_permissions",
|
||||||
"has_permissions",
|
"has_permissions",
|
||||||
|
"has_guild_permissions",
|
||||||
"is_owner",
|
"is_owner",
|
||||||
"guildowner",
|
"guildowner",
|
||||||
"guildowner_or_permissions",
|
"guildowner_or_permissions",
|
||||||
@@ -52,6 +56,9 @@ __all__ = [
|
|||||||
"admin_or_permissions",
|
"admin_or_permissions",
|
||||||
"mod",
|
"mod",
|
||||||
"mod_or_permissions",
|
"mod_or_permissions",
|
||||||
|
"transition_permstate_to",
|
||||||
|
"PermStateTransitions",
|
||||||
|
"PermStateAllowedStates",
|
||||||
]
|
]
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T")
|
||||||
@@ -182,11 +189,6 @@ class PermState(enum.Enum):
|
|||||||
"""This command has been actively denied by a permission hook
|
"""This command has been actively denied by a permission hook
|
||||||
check validation doesn't need this, but is useful to developers"""
|
check validation doesn't need this, but is useful to developers"""
|
||||||
|
|
||||||
def transition_to(
|
|
||||||
self, next_state: "PermState"
|
|
||||||
) -> Tuple[Optional[bool], Union["PermState", Dict[bool, "PermState"]]]:
|
|
||||||
return self.TRANSITIONS[self][next_state]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bool(cls, value: Optional[bool]) -> "PermState":
|
def from_bool(cls, value: Optional[bool]) -> "PermState":
|
||||||
"""Get a PermState from a bool or ``NoneType``."""
|
"""Get a PermState from a bool or ``NoneType``."""
|
||||||
@@ -211,7 +213,11 @@ class PermState(enum.Enum):
|
|||||||
# result of the default permission checks - the transition from NORMAL
|
# result of the default permission checks - the transition from NORMAL
|
||||||
# to PASSIVE_ALLOW. In this case "next state" is a dict mapping the
|
# to PASSIVE_ALLOW. In this case "next state" is a dict mapping the
|
||||||
# permission check results to the actual next state.
|
# permission check results to the actual next state.
|
||||||
PermState.TRANSITIONS = {
|
|
||||||
|
TransitionResult = Tuple[Optional[bool], Union[PermState, Dict[bool, PermState]]]
|
||||||
|
TransitionDict = Dict[PermState, Dict[PermState, TransitionResult]]
|
||||||
|
|
||||||
|
PermStateTransitions: TransitionDict = {
|
||||||
PermState.ACTIVE_ALLOW: {
|
PermState.ACTIVE_ALLOW: {
|
||||||
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||||
PermState.NORMAL: (True, PermState.ACTIVE_ALLOW),
|
PermState.NORMAL: (True, PermState.ACTIVE_ALLOW),
|
||||||
@@ -248,13 +254,18 @@ PermState.TRANSITIONS = {
|
|||||||
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
PermState.ALLOWED_STATES = (
|
|
||||||
|
PermStateAllowedStates = (
|
||||||
PermState.ACTIVE_ALLOW,
|
PermState.ACTIVE_ALLOW,
|
||||||
PermState.PASSIVE_ALLOW,
|
PermState.PASSIVE_ALLOW,
|
||||||
PermState.CAUTIOUS_ALLOW,
|
PermState.CAUTIOUS_ALLOW,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transition_permstate_to(prev: PermState, next_state: PermState) -> TransitionResult:
|
||||||
|
return PermStateTransitions[prev][next_state]
|
||||||
|
|
||||||
|
|
||||||
class Requires:
|
class Requires:
|
||||||
"""This class describes the requirements for executing a specific command.
|
"""This class describes the requirements for executing a specific command.
|
||||||
|
|
||||||
@@ -326,13 +337,13 @@ class Requires:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_decorator(
|
def get_decorator(
|
||||||
privilege_level: Optional[PrivilegeLevel], user_perms: Dict[str, bool]
|
privilege_level: Optional[PrivilegeLevel], user_perms: Optional[Dict[str, bool]]
|
||||||
) -> Callable[["_CommandOrCoro"], "_CommandOrCoro"]:
|
) -> Callable[["_CommandOrCoro"], "_CommandOrCoro"]:
|
||||||
if not user_perms:
|
if not user_perms:
|
||||||
user_perms = None
|
user_perms = None
|
||||||
|
|
||||||
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||||
if asyncio.iscoroutinefunction(func):
|
if inspect.iscoroutinefunction(func):
|
||||||
func.__requires_privilege_level__ = privilege_level
|
func.__requires_privilege_level__ = privilege_level
|
||||||
func.__requires_user_perms__ = user_perms
|
func.__requires_user_perms__ = user_perms
|
||||||
else:
|
else:
|
||||||
@@ -341,6 +352,7 @@ class Requires:
|
|||||||
func.requires.user_perms = None
|
func.requires.user_perms = None
|
||||||
else:
|
else:
|
||||||
_validate_perms_dict(user_perms)
|
_validate_perms_dict(user_perms)
|
||||||
|
assert func.requires.user_perms is not None
|
||||||
func.requires.user_perms.update(**user_perms)
|
func.requires.user_perms.update(**user_perms)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@@ -357,6 +369,8 @@ class Requires:
|
|||||||
guild_id : int
|
guild_id : int
|
||||||
The ID of the guild for the rule's scope. Set to
|
The ID of the guild for the rule's scope. Set to
|
||||||
`Requires.GLOBAL` for a global rule.
|
`Requires.GLOBAL` for a global rule.
|
||||||
|
If a global rule is set for a model,
|
||||||
|
it will be prefered over the guild rule.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -367,8 +381,9 @@ class Requires:
|
|||||||
"""
|
"""
|
||||||
if not isinstance(model, (str, int)):
|
if not isinstance(model, (str, int)):
|
||||||
model = model.id
|
model = model.id
|
||||||
|
rules: Mapping[Union[int, str], PermState]
|
||||||
if guild_id:
|
if guild_id:
|
||||||
rules = self._guild_rules.get(guild_id, _RulesDict())
|
rules = ChainMap(self._global_rules, self._guild_rules.get(guild_id, _RulesDict()))
|
||||||
else:
|
else:
|
||||||
rules = self._global_rules
|
rules = self._global_rules
|
||||||
return rules.get(model, PermState.NORMAL)
|
return rules.get(model, PermState.NORMAL)
|
||||||
@@ -488,7 +503,7 @@ class Requires:
|
|||||||
async def _transition_state(self, ctx: "Context") -> bool:
|
async def _transition_state(self, ctx: "Context") -> bool:
|
||||||
prev_state = ctx.permission_state
|
prev_state = ctx.permission_state
|
||||||
cur_state = self._get_rule_from_ctx(ctx)
|
cur_state = self._get_rule_from_ctx(ctx)
|
||||||
should_invoke, next_state = prev_state.transition_to(cur_state)
|
should_invoke, next_state = transition_permstate_to(prev_state, cur_state)
|
||||||
if should_invoke is None:
|
if should_invoke is None:
|
||||||
# NORMAL invokation, we simply follow standard procedure
|
# NORMAL invokation, we simply follow standard procedure
|
||||||
should_invoke = await self._verify_user(ctx)
|
should_invoke = await self._verify_user(ctx)
|
||||||
@@ -509,6 +524,7 @@ class Requires:
|
|||||||
would_invoke = await self._verify_user(ctx)
|
would_invoke = await self._verify_user(ctx)
|
||||||
next_state = next_state[would_invoke]
|
next_state = next_state[would_invoke]
|
||||||
|
|
||||||
|
assert isinstance(next_state, PermState)
|
||||||
ctx.permission_state = next_state
|
ctx.permission_state = next_state
|
||||||
return should_invoke
|
return should_invoke
|
||||||
|
|
||||||
@@ -635,6 +651,20 @@ def permissions_check(predicate: CheckPredicate):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def has_guild_permissions(**perms):
|
||||||
|
"""Restrict the command to users with these guild permissions.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_validate_perms_dict(perms)
|
||||||
|
|
||||||
|
def predicate(ctx):
|
||||||
|
return ctx.guild and ctx.author.guild_permissions >= discord.Permissions(**perms)
|
||||||
|
|
||||||
|
return permissions_check(predicate)
|
||||||
|
|
||||||
|
|
||||||
def bot_has_permissions(**perms: bool):
|
def bot_has_permissions(**perms: bool):
|
||||||
"""Complain if the bot is missing permissions.
|
"""Complain if the bot is missing permissions.
|
||||||
|
|
||||||
@@ -757,16 +787,10 @@ class _RulesDict(Dict[Union[int, str], PermState]):
|
|||||||
|
|
||||||
|
|
||||||
def _validate_perms_dict(perms: Dict[str, bool]) -> None:
|
def _validate_perms_dict(perms: Dict[str, bool]) -> None:
|
||||||
|
invalid_keys = set(perms.keys()) - set(discord.Permissions.VALID_FLAGS)
|
||||||
|
if invalid_keys:
|
||||||
|
raise TypeError(f"Invalid perm name(s): {', '.join(invalid_keys)}")
|
||||||
for perm, value in perms.items():
|
for perm, value in perms.items():
|
||||||
try:
|
|
||||||
attr = getattr(discord.Permissions, perm)
|
|
||||||
except AttributeError:
|
|
||||||
attr = None
|
|
||||||
|
|
||||||
if attr is None or not isinstance(attr, property):
|
|
||||||
# We reject invalid permissions
|
|
||||||
raise TypeError(f"Unknown permission name '{perm}'")
|
|
||||||
|
|
||||||
if value is not True:
|
if value is not True:
|
||||||
# We reject any permission not specified as 'True', since this is the only value which
|
# We reject any permission not specified as 'True', since this is the only value which
|
||||||
# makes practical sense.
|
# makes practical sense.
|
||||||
|
|||||||
@@ -979,7 +979,7 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
return self._get_base_group(self.CHANNEL, str(channel_id))
|
return self._get_base_group(self.CHANNEL, str(channel_id))
|
||||||
|
|
||||||
def channel(self, channel: discord.TextChannel) -> Group:
|
def channel(self, channel: discord.abc.GuildChannel) -> Group:
|
||||||
"""Returns a `Group` for the given channel.
|
"""Returns a `Group` for the given channel.
|
||||||
|
|
||||||
This does not discriminate between text and voice channels.
|
This does not discriminate between text and voice channels.
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class CoreLogic:
|
|||||||
else:
|
else:
|
||||||
await bot.add_loaded_package(name)
|
await bot.add_loaded_package(name)
|
||||||
loaded_packages.append(name)
|
loaded_packages.append(name)
|
||||||
# remove in Red 3.3
|
# remove in Red 3.4
|
||||||
downloader = bot.get_cog("Downloader")
|
downloader = bot.get_cog("Downloader")
|
||||||
if downloader is None:
|
if downloader is None:
|
||||||
continue
|
continue
|
||||||
@@ -319,6 +319,9 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
python_version = "[{}.{}.{}]({})".format(*sys.version_info[:3], python_url)
|
python_version = "[{}.{}.{}]({})".format(*sys.version_info[:3], python_url)
|
||||||
red_version = "[{}]({})".format(__version__, red_pypi)
|
red_version = "[{}]({})".format(__version__, red_pypi)
|
||||||
app_info = await self.bot.application_info()
|
app_info = await self.bot.application_info()
|
||||||
|
if app_info.team:
|
||||||
|
owner = app_info.team.name
|
||||||
|
else:
|
||||||
owner = app_info.owner
|
owner = app_info.owner
|
||||||
custom_info = await self.bot._config.custom_info()
|
custom_info = await self.bot._config.custom_info()
|
||||||
|
|
||||||
@@ -358,7 +361,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def uptime(self, ctx: commands.Context):
|
async def uptime(self, ctx: commands.Context):
|
||||||
"""Shows Red's uptime"""
|
"""Shows [botname]'s uptime"""
|
||||||
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
delta = datetime.datetime.utcnow() - self.bot.uptime
|
delta = datetime.datetime.utcnow() - self.bot.uptime
|
||||||
uptime_str = humanize_timedelta(timedelta=delta) or _("Less than one second")
|
uptime_str = humanize_timedelta(timedelta=delta) or _("Less than one second")
|
||||||
@@ -385,6 +388,9 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
guild_setting = await self.bot._config.guild(ctx.guild).embeds()
|
guild_setting = await self.bot._config.guild(ctx.guild).embeds()
|
||||||
text += _("Guild setting: {}\n").format(guild_setting)
|
text += _("Guild setting: {}\n").format(guild_setting)
|
||||||
|
if ctx.channel:
|
||||||
|
channel_setting = await self.bot._config.channel(ctx.channel).embeds()
|
||||||
|
text += _("Channel setting: {}\n").format(channel_setting)
|
||||||
user_setting = await self.bot._config.user(ctx.author).embeds()
|
user_setting = await self.bot._config.user(ctx.author).embeds()
|
||||||
text += _("User setting: {}").format(user_setting)
|
text += _("User setting: {}").format(user_setting)
|
||||||
await ctx.send(box(text))
|
await ctx.send(box(text))
|
||||||
@@ -430,6 +436,31 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@embedset.command(name="channel")
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
async def embedset_channel(self, ctx: commands.Context, enabled: bool = None):
|
||||||
|
"""
|
||||||
|
Toggle the channel's embed setting.
|
||||||
|
|
||||||
|
If enabled is None, the setting will be unset and
|
||||||
|
the guild default will be used instead.
|
||||||
|
|
||||||
|
If set, this is used instead of the guild default
|
||||||
|
to determine whether or not to use embeds. This is
|
||||||
|
used for all commands done in a channel except
|
||||||
|
for help commands.
|
||||||
|
"""
|
||||||
|
await self.bot._config.channel(ctx.channel).embeds.set(enabled)
|
||||||
|
if enabled is None:
|
||||||
|
await ctx.send(_("Embeds will now fall back to the global setting."))
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_("Embeds are now {} for this channel.").format(
|
||||||
|
_("enabled") if enabled else _("disabled")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@embedset.command(name="user")
|
@embedset.command(name="user")
|
||||||
async def embedset_user(self, ctx: commands.Context, enabled: bool = None):
|
async def embedset_user(self, ctx: commands.Context, enabled: bool = None):
|
||||||
"""
|
"""
|
||||||
@@ -471,7 +502,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.check(CoreLogic._can_get_invite_url)
|
@commands.check(CoreLogic._can_get_invite_url)
|
||||||
async def invite(self, ctx):
|
async def invite(self, ctx):
|
||||||
"""Show's Red's invite url"""
|
"""Show's [botname]'s invite url"""
|
||||||
try:
|
try:
|
||||||
await ctx.author.send(await self._invite_url())
|
await ctx.author.send(await self._invite_url())
|
||||||
except discord.errors.Forbidden:
|
except discord.errors.Forbidden:
|
||||||
@@ -674,13 +705,13 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
if len(repos_with_shared_libs) == 1:
|
if len(repos_with_shared_libs) == 1:
|
||||||
formed = _(
|
formed = _(
|
||||||
"**WARNING**: The following repo is using shared libs"
|
"**WARNING**: The following repo is using shared libs"
|
||||||
" which are marked for removal in Red 3.3: {repo}.\n"
|
" which are marked for removal in Red 3.4: {repo}.\n"
|
||||||
"You should inform maintainer of the repo about this message."
|
"You should inform maintainer of the repo about this message."
|
||||||
).format(repo=inline(repos_with_shared_libs.pop()))
|
).format(repo=inline(repos_with_shared_libs.pop()))
|
||||||
else:
|
else:
|
||||||
formed = _(
|
formed = _(
|
||||||
"**WARNING**: The following repos are using shared libs"
|
"**WARNING**: The following repos are using shared libs"
|
||||||
" which are marked for removal in Red 3.3: {repos}.\n"
|
" which are marked for removal in Red 3.4: {repos}.\n"
|
||||||
"You should inform maintainers of these repos about this message."
|
"You should inform maintainers of these repos about this message."
|
||||||
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
|
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
|
||||||
output.append(formed)
|
output.append(formed)
|
||||||
@@ -792,13 +823,13 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
if len(repos_with_shared_libs) == 1:
|
if len(repos_with_shared_libs) == 1:
|
||||||
formed = _(
|
formed = _(
|
||||||
"**WARNING**: The following repo is using shared libs"
|
"**WARNING**: The following repo is using shared libs"
|
||||||
" which are marked for removal in Red 3.3: {repo}.\n"
|
" which are marked for removal in Red 3.4: {repo}.\n"
|
||||||
"You should inform maintainers of these repos about this message."
|
"You should inform maintainers of these repos about this message."
|
||||||
).format(repo=inline(repos_with_shared_libs.pop()))
|
).format(repo=inline(repos_with_shared_libs.pop()))
|
||||||
else:
|
else:
|
||||||
formed = _(
|
formed = _(
|
||||||
"**WARNING**: The following repos are using shared libs"
|
"**WARNING**: The following repos are using shared libs"
|
||||||
" which are marked for removal in Red 3.3: {repos}.\n"
|
" which are marked for removal in Red 3.4: {repos}.\n"
|
||||||
"You should inform maintainers of these repos about this message."
|
"You should inform maintainers of these repos about this message."
|
||||||
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
|
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
|
||||||
output.append(formed)
|
output.append(formed)
|
||||||
@@ -834,7 +865,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
@commands.group(name="set")
|
@commands.group(name="set")
|
||||||
async def _set(self, ctx: commands.Context):
|
async def _set(self, ctx: commands.Context):
|
||||||
"""Changes Red's settings"""
|
"""Changes [botname]'s settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -1020,7 +1051,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@_set.command()
|
@_set.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def avatar(self, ctx: commands.Context, url: str):
|
async def avatar(self, ctx: commands.Context, url: str):
|
||||||
"""Sets Red's avatar"""
|
"""Sets [botname]'s avatar"""
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as r:
|
async with session.get(url) as r:
|
||||||
data = await r.read()
|
data = await r.read()
|
||||||
@@ -1044,7 +1075,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _game(self, ctx: commands.Context, *, game: str = None):
|
async def _game(self, ctx: commands.Context, *, game: str = None):
|
||||||
"""Sets Red's playing status"""
|
"""Sets [botname]'s playing status"""
|
||||||
|
|
||||||
if game:
|
if game:
|
||||||
game = discord.Game(name=game)
|
game = discord.Game(name=game)
|
||||||
@@ -1058,7 +1089,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _listening(self, ctx: commands.Context, *, listening: str = None):
|
async def _listening(self, ctx: commands.Context, *, listening: str = None):
|
||||||
"""Sets Red's listening status"""
|
"""Sets [botname]'s listening status"""
|
||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
if listening:
|
if listening:
|
||||||
@@ -1072,7 +1103,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _watching(self, ctx: commands.Context, *, watching: str = None):
|
async def _watching(self, ctx: commands.Context, *, watching: str = None):
|
||||||
"""Sets Red's watching status"""
|
"""Sets [botname]'s watching status"""
|
||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
if watching:
|
if watching:
|
||||||
@@ -1086,7 +1117,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def status(self, ctx: commands.Context, *, status: str):
|
async def status(self, ctx: commands.Context, *, status: str):
|
||||||
"""Sets Red's status
|
"""Sets [botname]'s status
|
||||||
|
|
||||||
Available statuses:
|
Available statuses:
|
||||||
online
|
online
|
||||||
@@ -1115,7 +1146,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
|
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
|
||||||
"""Sets Red's streaming status
|
"""Sets [botname]'s streaming status
|
||||||
Leaving both streamer and stream_title empty will clear it."""
|
Leaving both streamer and stream_title empty will clear it."""
|
||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None
|
||||||
@@ -1136,7 +1167,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@_set.command(name="username", aliases=["name"])
|
@_set.command(name="username", aliases=["name"])
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _username(self, ctx: commands.Context, *, username: str):
|
async def _username(self, ctx: commands.Context, *, username: str):
|
||||||
"""Sets Red's username"""
|
"""Sets [botname]'s username"""
|
||||||
try:
|
try:
|
||||||
await self._name(name=username)
|
await self._name(name=username)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
@@ -1155,7 +1186,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.admin()
|
@checks.admin()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _nickname(self, ctx: commands.Context, *, nickname: str = None):
|
async def _nickname(self, ctx: commands.Context, *, nickname: str = None):
|
||||||
"""Sets Red's nickname"""
|
"""Sets [botname]'s nickname"""
|
||||||
try:
|
try:
|
||||||
await ctx.guild.me.edit(nick=nickname)
|
await ctx.guild.me.edit(nick=nickname)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
@@ -1166,7 +1197,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@_set.command(aliases=["prefixes"])
|
@_set.command(aliases=["prefixes"])
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def prefix(self, ctx: commands.Context, *prefixes: str):
|
async def prefix(self, ctx: commands.Context, *prefixes: str):
|
||||||
"""Sets Red's global prefix(es)"""
|
"""Sets [botname]'s global prefix(es)"""
|
||||||
if not prefixes:
|
if not prefixes:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
@@ -1177,7 +1208,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.admin()
|
@checks.admin()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
|
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
|
||||||
"""Sets Red's server prefix(es)"""
|
"""Sets [botname]'s server prefix(es)"""
|
||||||
if not prefixes:
|
if not prefixes:
|
||||||
await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=[])
|
await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=[])
|
||||||
await ctx.send(_("Guild prefixes have been reset."))
|
await ctx.send(_("Guild prefixes have been reset."))
|
||||||
@@ -1368,6 +1399,30 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
await ctx.bot._config.help.max_pages_in_guild.set(pages)
|
await ctx.bot._config.help.max_pages_in_guild.set(pages)
|
||||||
await ctx.send(_("Done. The page limit has been set to {}.").format(pages))
|
await ctx.send(_("Done. The page limit has been set to {}.").format(pages))
|
||||||
|
|
||||||
|
@helpset.command(name="deletedelay")
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def helpset_deletedelay(self, ctx: commands.Context, seconds: int):
|
||||||
|
"""Set the delay after which help pages will be deleted.
|
||||||
|
|
||||||
|
The setting is disabled by default, and only applies to non-menu help,
|
||||||
|
sent in server text channels.
|
||||||
|
Setting the delay to 0 disables this feature.
|
||||||
|
|
||||||
|
The bot has to have MANAGE_MESSAGES permission for this to work.
|
||||||
|
"""
|
||||||
|
if seconds < 0:
|
||||||
|
await ctx.send(_("You must give a value of zero or greater!"))
|
||||||
|
return
|
||||||
|
if seconds > 60 * 60 * 24 * 14: # 14 days
|
||||||
|
await ctx.send(_("The delay cannot be longer than 14 days!"))
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.bot._config.help.delete_delay.set(seconds)
|
||||||
|
if seconds == 0:
|
||||||
|
await ctx.send(_("Done. Help messages will not be deleted now."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Done. The delete delay has been set to {} seconds.").format(seconds))
|
||||||
|
|
||||||
@helpset.command(name="tagline")
|
@helpset.command(name="tagline")
|
||||||
async def helpset_tagline(self, ctx: commands.Context, *, tagline: str = None):
|
async def helpset_tagline(self, ctx: commands.Context, *, tagline: str = None):
|
||||||
"""
|
"""
|
||||||
@@ -1457,6 +1512,8 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
if not destination.permissions_for(destination.guild.me).send_messages:
|
if not destination.permissions_for(destination.guild.me).send_messages:
|
||||||
continue
|
continue
|
||||||
if destination.permissions_for(destination.guild.me).embed_links:
|
if destination.permissions_for(destination.guild.me).embed_links:
|
||||||
|
send_embed = await ctx.bot._config.channel(destination).embeds()
|
||||||
|
if send_embed is None:
|
||||||
send_embed = await ctx.bot._config.guild(destination.guild).embeds()
|
send_embed = await ctx.bot._config.guild(destination.guild).embeds()
|
||||||
else:
|
else:
|
||||||
send_embed = False
|
send_embed = False
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class BaseDriver(abc.ABC):
|
|||||||
|
|
||||||
The driver must be initialized before this operation.
|
The driver must be initialized before this operation.
|
||||||
|
|
||||||
The BaseDriver provides a generic method which may be overriden
|
The BaseDriver provides a generic method which may be overridden
|
||||||
by subclasses.
|
by subclasses.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class JsonDriver(BaseDriver):
|
|||||||
|
|
||||||
def _save_json(path: Path, data: Dict[str, Any]) -> None:
|
def _save_json(path: Path, data: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
This fsync stuff here is entirely neccessary.
|
This fsync stuff here is entirely necessary.
|
||||||
|
|
||||||
On windows, it is not available in entirety.
|
On windows, it is not available in entirety.
|
||||||
If a windows user ends up with tons of temp files, they should consider hosting on
|
If a windows user ends up with tons of temp files, they should consider hosting on
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ def init_events(bot, cli_flags):
|
|||||||
users = len(set([m for m in bot.get_all_members()]))
|
users = len(set([m for m in bot.get_all_members()]))
|
||||||
|
|
||||||
app_info = await bot.application_info()
|
app_info = await bot.application_info()
|
||||||
|
|
||||||
|
if app_info.team:
|
||||||
|
if bot._use_team_features:
|
||||||
|
bot.owner_ids = {m.id for m in app_info.team.members}
|
||||||
|
else:
|
||||||
if bot.owner_id is None:
|
if bot.owner_id is None:
|
||||||
bot.owner_id = app_info.owner.id
|
bot.owner_id = app_info.owner.id
|
||||||
|
|
||||||
@@ -213,6 +218,12 @@ def init_events(bot, cli_flags):
|
|||||||
),
|
),
|
||||||
delete_after=error.retry_after,
|
delete_after=error.retry_after,
|
||||||
)
|
)
|
||||||
|
elif isinstance(error, commands.MaxConcurrencyReached):
|
||||||
|
await ctx.send(
|
||||||
|
"Too many people using this command. It can only be used {} time(s) per {} concurrently.".format(
|
||||||
|
error.number, error.per.name
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
log.exception(type(error).__name__, exc_info=error)
|
log.exception(type(error).__name__, exc_info=error)
|
||||||
|
|
||||||
|
|||||||
@@ -324,9 +324,7 @@ class Case:
|
|||||||
|
|
||||||
if embed:
|
if embed:
|
||||||
emb = discord.Embed(title=title, description=reason)
|
emb = discord.Embed(title=title, description=reason)
|
||||||
|
emb.set_author(name=user)
|
||||||
if avatar_url is not None:
|
|
||||||
emb.set_author(name=user, icon_url=avatar_url)
|
|
||||||
emb.add_field(name=_("Moderator"), value=moderator, inline=False)
|
emb.add_field(name=_("Moderator"), value=moderator, inline=False)
|
||||||
if until and duration:
|
if until and duration:
|
||||||
emb.add_field(name=_("Until"), value=until)
|
emb.add_field(name=_("Until"), value=until)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class AntiSpam:
|
|||||||
|
|
||||||
# TODO : Decorator interface for command check using `spammy`
|
# TODO : Decorator interface for command check using `spammy`
|
||||||
# with insertion of the antispam element into context
|
# with insertion of the antispam element into context
|
||||||
# for manual stamping on succesful command completion
|
# for manual stamping on successful command completion
|
||||||
|
|
||||||
default_intervals = [
|
default_intervals = [
|
||||||
(timedelta(seconds=5), 3),
|
(timedelta(seconds=5), 3),
|
||||||
|
|||||||
@@ -38,12 +38,13 @@ async def mass_purge(messages: List[discord.Message], channel: discord.TextChann
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
while messages:
|
while messages:
|
||||||
if len(messages) > 1:
|
# discord.NotFound can be raised when `len(messages) == 1` and the message does not exist.
|
||||||
|
# As a result of this obscure behavior, this error needs to be caught just in case.
|
||||||
|
try:
|
||||||
await channel.delete_messages(messages[:100])
|
await channel.delete_messages(messages[:100])
|
||||||
|
except discord.errors.HTTPException:
|
||||||
|
pass
|
||||||
messages = messages[100:]
|
messages = messages[100:]
|
||||||
else:
|
|
||||||
await messages[0].delete()
|
|
||||||
messages = []
|
|
||||||
await asyncio.sleep(1.5)
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Callable, ClassVar, List, Optional, Pattern, Sequence, Tuple, Union, cast
|
from typing import Callable, ClassVar, List, Optional, Pattern, Sequence, Tuple, Union, cast
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ packages = find_namespace:
|
|||||||
python_requires = >=3.8.1
|
python_requires = >=3.8.1
|
||||||
install_requires =
|
install_requires =
|
||||||
aiohttp==3.6.2
|
aiohttp==3.6.2
|
||||||
aiohttp-json-rpc==0.12.1
|
aiohttp-json-rpc==0.12.2
|
||||||
aiosqlite==0.11.0
|
aiosqlite==0.11.0
|
||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
apsw-wheels==3.30.1.post3
|
apsw-wheels==3.30.1.post3
|
||||||
@@ -38,7 +38,7 @@ install_requires =
|
|||||||
Click==7.0
|
Click==7.0
|
||||||
colorama==0.4.3
|
colorama==0.4.3
|
||||||
contextlib2==0.5.5
|
contextlib2==0.5.5
|
||||||
discord.py==1.2.5
|
discord.py==1.3.1
|
||||||
distro==1.4.0; sys_platform == "linux"
|
distro==1.4.0; sys_platform == "linux"
|
||||||
fuzzywuzzy==0.17.0
|
fuzzywuzzy==0.17.0
|
||||||
idna==2.8
|
idna==2.8
|
||||||
@@ -46,7 +46,7 @@ install_requires =
|
|||||||
python-Levenshtein-wheels==0.13.1
|
python-Levenshtein-wheels==0.13.1
|
||||||
pytz==2019.3
|
pytz==2019.3
|
||||||
PyYAML==5.3
|
PyYAML==5.3
|
||||||
Red-Lavalink==0.4.1
|
Red-Lavalink==0.4.2
|
||||||
schema==0.7.1
|
schema==0.7.1
|
||||||
tqdm==4.41.1
|
tqdm==4.41.1
|
||||||
uvloop==0.14.0; sys_platform != "win32" and platform_python_implementation == "CPython"
|
uvloop==0.14.0; sys_platform != "win32" and platform_python_implementation == "CPython"
|
||||||
|
|||||||
Reference in New Issue
Block a user