mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-08 18:32:32 -05:00
Merge branch 'V3/release/3.0.0' into V3/develop
# Conflicts: # redbot/cogs/mod/mod.py
This commit is contained in:
@@ -148,5 +148,5 @@ class VersionInfo:
|
||||
)
|
||||
|
||||
|
||||
__version__ = "3.0.0rc2"
|
||||
__version__ = "3.0.0rc3.post1"
|
||||
version_info = VersionInfo.from_str(__version__)
|
||||
|
||||
@@ -111,7 +111,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
|
||||
self.main_dir = bot_dir
|
||||
|
||||
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
||||
self.cog_mgr = CogManager()
|
||||
|
||||
super().__init__(*args, formatter=Help(), **kwargs)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import pkgutil
|
||||
from importlib import import_module, invalidate_caches
|
||||
from importlib.machinery import ModuleSpec
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Union, List, Optional
|
||||
from typing import Union, List, Optional
|
||||
|
||||
import redbot.cogs
|
||||
from redbot.core.utils import deduplicate_iterables
|
||||
@@ -25,8 +25,6 @@ class NoSuchCog(ImportError):
|
||||
Different from ImportError because some ImportErrors can happen inside cogs.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CogManager:
|
||||
"""Directory manager for Red's cogs.
|
||||
@@ -39,30 +37,27 @@ class CogManager:
|
||||
|
||||
CORE_PATH = Path(redbot.cogs.__path__[0])
|
||||
|
||||
def __init__(self, paths: Tuple[str] = ()):
|
||||
def __init__(self):
|
||||
self.conf = Config.get_conf(self, 2938473984732, True)
|
||||
tmp_cog_install_path = cog_data_path(self) / "cogs"
|
||||
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
|
||||
self.conf.register_global(paths=[], install_path=str(tmp_cog_install_path))
|
||||
self._paths = [Path(p) for p in paths]
|
||||
|
||||
async def paths(self) -> Tuple[Path, ...]:
|
||||
"""Get all currently valid path directories.
|
||||
async def paths(self) -> List[Path]:
|
||||
"""Get all currently valid path directories, in order of priority
|
||||
|
||||
Returns
|
||||
-------
|
||||
`tuple` of `pathlib.Path`
|
||||
All valid cog paths.
|
||||
List[pathlib.Path]
|
||||
A list of paths where cog packages can be found. The
|
||||
install path is highest priority, followed by the
|
||||
user-defined paths, and the core path has the lowest
|
||||
priority.
|
||||
|
||||
"""
|
||||
conf_paths = [Path(p) for p in await self.conf.paths()]
|
||||
other_paths = self._paths
|
||||
|
||||
all_paths = deduplicate_iterables(conf_paths, other_paths, [self.CORE_PATH])
|
||||
|
||||
if self.install_path not in all_paths:
|
||||
all_paths.insert(0, await self.install_path())
|
||||
return tuple(p.resolve() for p in all_paths if p.is_dir())
|
||||
return deduplicate_iterables(
|
||||
[await self.install_path()], await self.user_defined_paths(), [self.CORE_PATH]
|
||||
)
|
||||
|
||||
async def install_path(self) -> Path:
|
||||
"""Get the install path for 3rd party cogs.
|
||||
@@ -73,8 +68,20 @@ class CogManager:
|
||||
The path to the directory where 3rd party cogs are stored.
|
||||
|
||||
"""
|
||||
p = Path(await self.conf.install_path())
|
||||
return p.resolve()
|
||||
return Path(await self.conf.install_path()).resolve()
|
||||
|
||||
async def user_defined_paths(self) -> List[Path]:
|
||||
"""Get a list of user-defined cog paths.
|
||||
|
||||
All paths will be absolute and unique, in order of priority.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[pathlib.Path]
|
||||
A list of user-defined paths.
|
||||
|
||||
"""
|
||||
return list(map(Path, deduplicate_iterables(await self.conf.paths())))
|
||||
|
||||
async def set_install_path(self, path: Path) -> Path:
|
||||
"""Set the install path for 3rd party cogs.
|
||||
@@ -125,11 +132,10 @@ class CogManager:
|
||||
path = Path(path)
|
||||
return path
|
||||
|
||||
async def add_path(self, path: Union[Path, str]):
|
||||
async def add_path(self, path: Union[Path, str]) -> None:
|
||||
"""Add a cog path to current list.
|
||||
|
||||
This will ignore duplicates. Does have a side effect of removing all
|
||||
invalid paths from the saved path list.
|
||||
This will ignore duplicates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -156,11 +162,12 @@ class CogManager:
|
||||
if path == self.CORE_PATH:
|
||||
raise ValueError("Cannot add the core path as an additional path.")
|
||||
|
||||
async with self.conf.paths() as paths:
|
||||
if not any(Path(p) == path for p in paths):
|
||||
paths.append(str(path))
|
||||
current_paths = await self.user_defined_paths()
|
||||
if path not in current_paths:
|
||||
current_paths.append(path)
|
||||
await self.set_paths(current_paths)
|
||||
|
||||
async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
|
||||
async def remove_path(self, path: Union[Path, str]) -> None:
|
||||
"""Remove a path from the current paths list.
|
||||
|
||||
Parameters
|
||||
@@ -168,20 +175,12 @@ class CogManager:
|
||||
path : `pathlib.Path` or `str`
|
||||
Path to remove.
|
||||
|
||||
Returns
|
||||
-------
|
||||
`tuple` of `pathlib.Path`
|
||||
Tuple of new valid paths.
|
||||
|
||||
"""
|
||||
path = self._ensure_path_obj(path).resolve()
|
||||
paths = await self.user_defined_paths()
|
||||
|
||||
paths = [Path(p) for p in await self.conf.paths()]
|
||||
if path in paths:
|
||||
paths.remove(path)
|
||||
await self.set_paths(paths)
|
||||
|
||||
return tuple(paths)
|
||||
paths.remove(path)
|
||||
await self.set_paths(paths)
|
||||
|
||||
async def set_paths(self, paths_: List[Path]):
|
||||
"""Set the current paths list.
|
||||
@@ -192,7 +191,7 @@ class CogManager:
|
||||
List of paths to set.
|
||||
|
||||
"""
|
||||
str_paths = [str(p) for p in paths_]
|
||||
str_paths = list(map(str, paths_))
|
||||
await self.conf.paths.set(str_paths)
|
||||
|
||||
async def _find_ext_cog(self, name: str) -> ModuleSpec:
|
||||
@@ -213,9 +212,9 @@ class CogManager:
|
||||
------
|
||||
NoSuchCog
|
||||
When no cog with the requested name was found.
|
||||
|
||||
"""
|
||||
resolved_paths = await self.paths()
|
||||
real_paths = [str(p) for p in resolved_paths if p != self.CORE_PATH]
|
||||
real_paths = list(map(str, [await self.install_path()] + await self.user_defined_paths()))
|
||||
|
||||
for finder, module_name, _ in pkgutil.iter_modules(real_paths):
|
||||
if name == module_name:
|
||||
@@ -287,10 +286,8 @@ class CogManager:
|
||||
return await self._find_core_cog(name)
|
||||
|
||||
async def available_modules(self) -> List[str]:
|
||||
"""Finds the names of all available modules to load.
|
||||
"""
|
||||
paths = (await self.install_path(),) + await self.paths()
|
||||
paths = [str(p) for p in paths]
|
||||
"""Finds the names of all available modules to load."""
|
||||
paths = list(map(str, await self.paths()))
|
||||
|
||||
ret = []
|
||||
for finder, module_name, _ in pkgutil.iter_modules(paths):
|
||||
@@ -314,13 +311,6 @@ _ = Translator("CogManagerUI", __file__)
|
||||
class CogManagerUI(commands.Cog):
|
||||
"""Commands to interface with Red's cog manager."""
|
||||
|
||||
@staticmethod
|
||||
async def visible_paths(ctx):
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
cog_paths = await ctx.bot.cog_mgr.paths()
|
||||
cog_paths = [p for p in cog_paths if p != install_path]
|
||||
return cog_paths
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def paths(self, ctx: commands.Context):
|
||||
@@ -330,8 +320,7 @@ class CogManagerUI(commands.Cog):
|
||||
cog_mgr = ctx.bot.cog_mgr
|
||||
install_path = await cog_mgr.install_path()
|
||||
core_path = cog_mgr.CORE_PATH
|
||||
cog_paths = await cog_mgr.paths()
|
||||
cog_paths = [p for p in cog_paths if p not in (install_path, core_path)]
|
||||
cog_paths = await cog_mgr.user_defined_paths()
|
||||
|
||||
msg = _("Install Path: {install_path}\nCore Path: {core_path}\n\n").format(
|
||||
install_path=install_path, core_path=core_path
|
||||
@@ -369,7 +358,11 @@ class CogManagerUI(commands.Cog):
|
||||
from !paths
|
||||
"""
|
||||
path_number -= 1
|
||||
cog_paths = await self.visible_paths(ctx)
|
||||
if path_number < 0:
|
||||
await ctx.send(_("Path numbers must be positive."))
|
||||
return
|
||||
|
||||
cog_paths = await ctx.bot.cog_mgr.user_defined_paths()
|
||||
try:
|
||||
to_remove = cog_paths.pop(path_number)
|
||||
except IndexError:
|
||||
@@ -388,8 +381,11 @@ class CogManagerUI(commands.Cog):
|
||||
# Doing this because in the paths command they're 1 indexed
|
||||
from_ -= 1
|
||||
to -= 1
|
||||
if from_ < 0 or to < 0:
|
||||
await ctx.send(_("Path numbers must be positive."))
|
||||
return
|
||||
|
||||
all_paths = await self.visible_paths(ctx)
|
||||
all_paths = await ctx.bot.cog_mgr.user_defined_paths()
|
||||
try:
|
||||
to_move = all_paths.pop(from_)
|
||||
except IndexError:
|
||||
|
||||
@@ -145,7 +145,7 @@ class Command(CogCommandMixin, commands.Command):
|
||||
|
||||
@property
|
||||
def parents(self) -> List["Group"]:
|
||||
"""List[Group] : Returns all parent commands of this command.
|
||||
"""List[commands.Group] : Returns all parent commands of this command.
|
||||
|
||||
This is sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
||||
If the command has no parents, this will be an empty list.
|
||||
|
||||
@@ -1173,6 +1173,9 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(
|
||||
_("A backup has been made of this instance. It is at {}.").format(backup_file)
|
||||
)
|
||||
if backup_file.stat().st_size > 8_000_000:
|
||||
await ctx.send(_("This backup is to large to send via DM."))
|
||||
return
|
||||
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
||||
|
||||
pred = MessagePredicate.yes_or_no(ctx)
|
||||
@@ -1183,10 +1186,18 @@ class Core(commands.Cog, CoreLogic):
|
||||
else:
|
||||
if pred.result is True:
|
||||
await ctx.send(_("OK, it's on its way!"))
|
||||
async with ctx.author.typing():
|
||||
await ctx.author.send(
|
||||
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
|
||||
try:
|
||||
async with ctx.author.typing():
|
||||
await ctx.author.send(
|
||||
_("Here's a copy of the backup"),
|
||||
file=discord.File(str(backup_file)),
|
||||
)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(
|
||||
_("I don't seem to be able to DM you. Do you have closed DMs?")
|
||||
)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I could not send the backup file."))
|
||||
else:
|
||||
await ctx.send(_("OK then."))
|
||||
else:
|
||||
|
||||
@@ -15,7 +15,7 @@ from pkg_resources import DistributionNotFound
|
||||
|
||||
from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
|
||||
from .data_manager import storage_type
|
||||
from .utils.chat_formatting import inline, bordered, humanize_list
|
||||
from .utils.chat_formatting import inline, bordered, format_perms_list
|
||||
from .utils import fuzzy_command_search, format_fuzzy_results
|
||||
|
||||
log = logging.getLogger("red")
|
||||
@@ -234,18 +234,13 @@ def init_events(bot, cli_flags):
|
||||
else:
|
||||
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
|
||||
elif isinstance(error, commands.BotMissingPermissions):
|
||||
missing_perms: List[str] = []
|
||||
for perm, value in error.missing:
|
||||
if value is True:
|
||||
perm_name = '"' + perm.replace("_", " ").title() + '"'
|
||||
missing_perms.append(perm_name)
|
||||
if len(missing_perms) == 1:
|
||||
if bin(error.missing.value).count("1") == 1: # Only one perm missing
|
||||
plural = ""
|
||||
else:
|
||||
plural = "s"
|
||||
await ctx.send(
|
||||
"I require the {perms} permission{plural} to execute that command.".format(
|
||||
perms=humanize_list(missing_perms), plural=plural
|
||||
perms=format_perms_list(error.missing), plural=plural
|
||||
)
|
||||
)
|
||||
elif isinstance(error, commands.CheckFailure):
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import itertools
|
||||
from typing import Sequence, Iterator, List
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = Translator("UtilsChatFormatting", __file__)
|
||||
@@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
|
||||
return text
|
||||
|
||||
|
||||
def humanize_list(items: Sequence[str]):
|
||||
def humanize_list(items: Sequence[str]) -> str:
|
||||
"""Get comma-separted list, with the last element joined with *and*.
|
||||
|
||||
This uses an Oxford comma, because without one, items containing
|
||||
@@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]):
|
||||
if len(items) == 1:
|
||||
return items[0]
|
||||
return ", ".join(items[:-1]) + _(", and ") + items[-1]
|
||||
|
||||
|
||||
def format_perms_list(perms: discord.Permissions) -> str:
|
||||
"""Format a list of permission names.
|
||||
|
||||
This will return a humanized list of the names of all enabled
|
||||
permissions in the provided `discord.Permissions` object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
perms : discord.Permissions
|
||||
The permissions object with the requested permissions to list
|
||||
enabled.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The humanized list.
|
||||
|
||||
"""
|
||||
perm_names: List[str] = []
|
||||
for perm, value in perms:
|
||||
if value is True:
|
||||
perm_name = '"' + perm.replace("_", " ").title() + '"'
|
||||
perm_names.append(perm_name)
|
||||
return humanize_list(perm_names).replace("Guild", "Server")
|
||||
|
||||
@@ -73,10 +73,13 @@ async def menu(
|
||||
# noinspection PyAsyncCall
|
||||
start_adding_reactions(message, controls.keys(), ctx.bot.loop)
|
||||
else:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
await message.edit(embed=current_page)
|
||||
else:
|
||||
await message.edit(content=current_page)
|
||||
try:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
await message.edit(embed=current_page)
|
||||
else:
|
||||
await message.edit(content=current_page)
|
||||
except discord.NotFound:
|
||||
return
|
||||
|
||||
try:
|
||||
react, user = await ctx.bot.wait_for(
|
||||
@@ -90,9 +93,12 @@ async def menu(
|
||||
except discord.Forbidden: # cannot remove all reactions
|
||||
for key in controls.keys():
|
||||
await message.remove_reaction(key, ctx.bot.user)
|
||||
return None
|
||||
|
||||
return await controls[react.emoji](ctx, pages, controls, message, page, timeout, react.emoji)
|
||||
except discord.NotFound:
|
||||
return
|
||||
else:
|
||||
return await controls[react.emoji](
|
||||
ctx, pages, controls, message, page, timeout, react.emoji
|
||||
)
|
||||
|
||||
|
||||
async def next_page(
|
||||
@@ -106,10 +112,8 @@ async def next_page(
|
||||
):
|
||||
perms = message.channel.permissions_for(ctx.me)
|
||||
if perms.manage_messages: # Can manage messages, so remove react
|
||||
try:
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await message.remove_reaction(emoji, ctx.author)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
if page == len(pages) - 1:
|
||||
page = 0 # Loop around to the first item
|
||||
else:
|
||||
@@ -128,10 +132,8 @@ async def prev_page(
|
||||
):
|
||||
perms = message.channel.permissions_for(ctx.me)
|
||||
if perms.manage_messages: # Can manage messages, so remove react
|
||||
try:
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await message.remove_reaction(emoji, ctx.author)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
if page == 0:
|
||||
page = len(pages) - 1 # Loop around to the last item
|
||||
else:
|
||||
@@ -148,9 +150,8 @@ async def close_menu(
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
|
||||
def start_adding_reactions(
|
||||
@@ -161,7 +162,7 @@ def start_adding_reactions(
|
||||
"""Start adding reactions to a message.
|
||||
|
||||
This is a non-blocking operation - calling this will schedule the
|
||||
reactions being added, but will the calling code will continue to
|
||||
reactions being added, but the calling code will continue to
|
||||
execute asynchronously. There is no need to await this function.
|
||||
|
||||
This is particularly useful if you wish to start waiting for a
|
||||
@@ -169,7 +170,7 @@ def start_adding_reactions(
|
||||
this is exactly what `menu` uses to do that.
|
||||
|
||||
This spawns a `asyncio.Task` object and schedules it on ``loop``.
|
||||
If ``loop`` omitted, the loop will be retreived with
|
||||
If ``loop`` omitted, the loop will be retrieved with
|
||||
`asyncio.get_event_loop`.
|
||||
|
||||
Parameters
|
||||
|
||||
@@ -2,7 +2,6 @@ import discord
|
||||
from datetime import datetime
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
import io
|
||||
import sys
|
||||
import weakref
|
||||
from typing import List, Optional
|
||||
from .common_filters import filter_mass_mentions
|
||||
@@ -151,15 +150,12 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
|
||||
"""
|
||||
files = []
|
||||
size = 0
|
||||
max_size = 8 * 1024 * 1024
|
||||
for a in m.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
size += sys.getsizeof(_fp)
|
||||
if size > max_size:
|
||||
return []
|
||||
files.append(discord.File(_fp, filename=a.filename))
|
||||
max_size = 8 * 1000 * 1000
|
||||
if m.attachments and sum(a.size for a in m.attachments) <= max_size:
|
||||
for a in m.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
files.append(discord.File(_fp, filename=a.filename))
|
||||
return files
|
||||
|
||||
async def communicate(
|
||||
|
||||
Reference in New Issue
Block a user