mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-02-05 15:03:01 -05:00
Merge branch 'V3/develop' into V3/feature/mutes
This commit is contained in:
@@ -838,9 +838,9 @@ async def set_default_balance(amount: int, guild: discord.Guild = None) -> int:
|
||||
amount = int(amount)
|
||||
max_bal = await get_max_balance(guild)
|
||||
|
||||
if not (0 < amount <= max_bal):
|
||||
if not (0 <= amount <= max_bal):
|
||||
raise ValueError(
|
||||
"Amount must be greater than zero and less than {max}.".format(
|
||||
"Amount must be greater than or equal zero and less than or equal {max}.".format(
|
||||
max=humanize_number(max_bal, override_locale="en_US")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -10,7 +10,19 @@ from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from importlib.machinery import ModuleSpec
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, List, Dict, NoReturn
|
||||
from typing import (
|
||||
Optional,
|
||||
Union,
|
||||
List,
|
||||
Dict,
|
||||
NoReturn,
|
||||
Set,
|
||||
Coroutine,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Awaitable,
|
||||
Any,
|
||||
)
|
||||
from types import MappingProxyType
|
||||
|
||||
import discord
|
||||
@@ -24,6 +36,8 @@ from .dev_commands import Dev
|
||||
from .events import init_events
|
||||
from .global_checks import init_global_checks
|
||||
|
||||
from .settings_caches import PrefixManager
|
||||
|
||||
from .rpc import RPCMixin
|
||||
from .utils import common_filters
|
||||
|
||||
@@ -36,6 +50,9 @@ __all__ = ["RedBase", "Red", "ExitCodes"]
|
||||
|
||||
NotMessage = namedtuple("NotMessage", "guild")
|
||||
|
||||
PreInvokeCoroutine = Callable[[commands.Context], Awaitable[Any]]
|
||||
T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine)
|
||||
|
||||
|
||||
def _is_submodule(parent, child):
|
||||
return parent == child or child.startswith(parent + ".")
|
||||
@@ -76,6 +93,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
help__verify_checks=True,
|
||||
help__verify_exists=False,
|
||||
help__tagline="",
|
||||
description="Red V3",
|
||||
invite_public=False,
|
||||
invite_perm=0,
|
||||
disabled_commands=[],
|
||||
@@ -108,23 +126,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
|
||||
self._config.init_custom(SHARED_API_TOKENS, 2)
|
||||
self._config.register_custom(SHARED_API_TOKENS)
|
||||
self._prefix_cache = PrefixManager(self._config, cli_flags)
|
||||
|
||||
async def prefix_manager(bot, message):
|
||||
if not cli_flags.prefix:
|
||||
global_prefix = await bot._config.prefix()
|
||||
else:
|
||||
global_prefix = cli_flags.prefix
|
||||
if message.guild is None:
|
||||
return global_prefix
|
||||
server_prefix = await bot._config.guild(message.guild).prefix()
|
||||
async def prefix_manager(bot, message) -> List[str]:
|
||||
prefixes = await self._prefix_cache.get_prefixes(message.guild)
|
||||
if cli_flags.mentionable:
|
||||
return (
|
||||
when_mentioned_or(*server_prefix)(bot, message)
|
||||
if server_prefix
|
||||
else when_mentioned_or(*global_prefix)(bot, message)
|
||||
)
|
||||
else:
|
||||
return server_prefix if server_prefix else global_prefix
|
||||
return when_mentioned_or(*prefixes)(bot, message)
|
||||
return prefixes
|
||||
|
||||
if "command_prefix" not in kwargs:
|
||||
kwargs["command_prefix"] = prefix_manager
|
||||
@@ -149,6 +157,64 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
|
||||
self._permissions_hooks: List[commands.CheckPredicate] = []
|
||||
self._red_ready = asyncio.Event()
|
||||
self._red_before_invoke_objs: Set[PreInvokeCoroutine] = set()
|
||||
|
||||
@property
|
||||
def _before_invoke(self): # DEP-WARN
|
||||
return self._red_before_invoke_method
|
||||
|
||||
@_before_invoke.setter
|
||||
def _before_invoke(self, val): # DEP-WARN
|
||||
"""Prevent this from being overwritten in super().__init__"""
|
||||
pass
|
||||
|
||||
async def _red_before_invoke_method(self, ctx):
|
||||
await self.wait_until_red_ready()
|
||||
return_exceptions = isinstance(ctx.command, commands.commands._AlwaysAvailableCommand)
|
||||
if self._red_before_invoke_objs:
|
||||
await asyncio.gather(
|
||||
*(coro(ctx) for coro in self._red_before_invoke_objs),
|
||||
return_exceptions=return_exceptions,
|
||||
)
|
||||
|
||||
def remove_before_invoke_hook(self, coro: PreInvokeCoroutine) -> None:
|
||||
"""
|
||||
Functional method to remove a `before_invoke` hook.
|
||||
"""
|
||||
self._red_before_invoke_objs.discard(coro)
|
||||
|
||||
def before_invoke(self, coro: T_BIC) -> T_BIC:
|
||||
"""
|
||||
Overridden decorator method for Red's ``before_invoke`` behavior.
|
||||
|
||||
This can safely be used purely functionally as well.
|
||||
|
||||
3rd party cogs should remove any hooks which they register at unload
|
||||
using `remove_before_invoke_hook`
|
||||
|
||||
Below behavior shared with discord.py:
|
||||
|
||||
.. note::
|
||||
The ``before_invoke`` hooks are
|
||||
only called if all checks and argument parsing procedures pass
|
||||
without error. If any check or argument parsing procedures fail
|
||||
then the hooks are not called.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coro: Callable[[commands.Context], Awaitable[Any]]
|
||||
The coroutine to register as the pre-invoke hook.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
The coroutine passed is not actually a coroutine.
|
||||
"""
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise TypeError("The pre-invoke hook must be a coroutine.")
|
||||
|
||||
self._red_before_invoke_objs.add(coro)
|
||||
return coro
|
||||
|
||||
@property
|
||||
def cog_mgr(self) -> NoReturn:
|
||||
@@ -199,15 +265,15 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
"""
|
||||
This checks if a user or member is allowed to run things,
|
||||
as considered by Red's whitelist and blacklist.
|
||||
|
||||
|
||||
If given a user object, this function will check the global lists
|
||||
|
||||
|
||||
If given a member, this will additionally check guild lists
|
||||
|
||||
|
||||
If omiting a user or member, you must provide a value for ``who_id``
|
||||
|
||||
|
||||
You may also provide a value for ``guild_id`` in this case
|
||||
|
||||
|
||||
If providing a member by guild and member ids,
|
||||
you should supply ``role_ids`` as well
|
||||
|
||||
@@ -215,7 +281,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
----------
|
||||
who : Optional[Union[discord.Member, discord.User]]
|
||||
The user or member object to check
|
||||
|
||||
|
||||
Other Parameters
|
||||
----------------
|
||||
who_id : Optional[int]
|
||||
@@ -400,6 +466,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
This should only be run once, prior to connecting to discord.
|
||||
"""
|
||||
await self._maybe_update_config()
|
||||
self.description = await self._config.description()
|
||||
|
||||
init_global_checks(self)
|
||||
init_events(self, cli_flags)
|
||||
@@ -547,9 +614,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
bool
|
||||
:code:`True` if an embed is requested
|
||||
"""
|
||||
if isinstance(channel, discord.abc.PrivateChannel) or (
|
||||
command and command == self.get_command("help")
|
||||
):
|
||||
if isinstance(channel, discord.abc.PrivateChannel):
|
||||
user_setting = await self._config.user(user).embeds()
|
||||
if user_setting is not None:
|
||||
return user_setting
|
||||
@@ -557,6 +622,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
guild_setting = await self._config.guild(channel.guild).embeds()
|
||||
if guild_setting is not None:
|
||||
return guild_setting
|
||||
|
||||
global_setting = await self._config.embeds()
|
||||
return global_setting
|
||||
|
||||
@@ -831,7 +897,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
||||
This should realistically only be used for responding using user provided
|
||||
input. (unfortunately, including usernames)
|
||||
Manually crafted messages which dont take any user input have no need of this
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
|
||||
@@ -135,7 +135,9 @@ def parse_cli_flags(args):
|
||||
"security implications if misused. Can be "
|
||||
"multiple.",
|
||||
)
|
||||
parser.add_argument("--prefix", "-p", action="append", help="Global prefix. Can be multiple")
|
||||
parser.add_argument(
|
||||
"--prefix", "-p", action="append", help="Global prefix. Can be multiple", default=[]
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-prompt",
|
||||
action="store_true",
|
||||
|
||||
@@ -4,6 +4,7 @@ This module contains extended classes and functions which are intended to
|
||||
replace those from the `discord.ext.commands` module.
|
||||
"""
|
||||
import inspect
|
||||
import re
|
||||
import weakref
|
||||
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
||||
|
||||
@@ -57,6 +58,49 @@ class CogCommandMixin:
|
||||
checks=getattr(decorated, "__requires_checks__", []),
|
||||
)
|
||||
|
||||
def format_help_for_context(self, ctx: "Context") -> str:
|
||||
"""
|
||||
This formats the help string based on values in context
|
||||
|
||||
The steps are (currently, roughly) the following:
|
||||
|
||||
- get the localized help
|
||||
- substitute ``[p]`` with ``ctx.clean_prefix``
|
||||
- substitute ``[botname]`` with ``ctx.me.display_name``
|
||||
|
||||
More steps may be added at a later time.
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
help_str = self.help
|
||||
if not help_str:
|
||||
# Short circuit out on an empty help string
|
||||
return help_str
|
||||
|
||||
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, help_str)
|
||||
|
||||
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
|
||||
"""Actively allow this command for the given model.
|
||||
|
||||
|
||||
@@ -162,10 +162,10 @@ class RedHelpFormatter:
|
||||
|
||||
@staticmethod
|
||||
def get_default_tagline(ctx: Context):
|
||||
return (
|
||||
f"Type {ctx.clean_prefix}help <command> for more info on a command. "
|
||||
f"You can also type {ctx.clean_prefix}help <category> for more info on a category."
|
||||
)
|
||||
return T_(
|
||||
"Type {ctx.clean_prefix}help <command> for more info on a command. "
|
||||
"You can also type {ctx.clean_prefix}help <category> for more info on a category."
|
||||
).format(ctx=ctx)
|
||||
|
||||
async def format_command_help(self, ctx: Context, obj: commands.Command):
|
||||
|
||||
@@ -187,7 +187,9 @@ class RedHelpFormatter:
|
||||
|
||||
description = command.description or ""
|
||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||
signature = f"`Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}`"
|
||||
signature = (
|
||||
f"`{T_('Syntax')}: {ctx.clean_prefix}{command.qualified_name} {command.signature}`"
|
||||
)
|
||||
subcommands = None
|
||||
|
||||
if hasattr(command, "all_commands"):
|
||||
@@ -198,18 +200,19 @@ class RedHelpFormatter:
|
||||
emb = {"embed": {"title": "", "description": ""}, "footer": {"text": ""}, "fields": []}
|
||||
|
||||
if description:
|
||||
emb["embed"]["title"] = f"*{description[:2044]}*"
|
||||
emb["embed"]["title"] = f"*{description[:250]}*"
|
||||
|
||||
emb["footer"]["text"] = tagline
|
||||
emb["embed"]["description"] = signature
|
||||
|
||||
if command.help:
|
||||
splitted = command.help.split("\n\n")
|
||||
command_help = command.format_help_for_context(ctx)
|
||||
if command_help:
|
||||
splitted = command_help.split("\n\n")
|
||||
name = splitted[0]
|
||||
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
||||
value = "\n\n".join(splitted[1:])
|
||||
if not value:
|
||||
value = EMPTY_STRING
|
||||
field = EmbedField(name[:252], value[:1024], False)
|
||||
field = EmbedField(name[:250], value[:1024], False)
|
||||
emb["fields"].append(field)
|
||||
|
||||
if subcommands:
|
||||
@@ -225,9 +228,9 @@ class RedHelpFormatter:
|
||||
)
|
||||
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
|
||||
if i == 0:
|
||||
title = "**__Subcommands:__**"
|
||||
title = T_("**__Subcommands:__**")
|
||||
else:
|
||||
title = "**__Subcommands:__** (continued)"
|
||||
title = T_("**__Subcommands:__** (continued)")
|
||||
field = EmbedField(title, page, False)
|
||||
emb["fields"].append(field)
|
||||
|
||||
@@ -238,7 +241,7 @@ class RedHelpFormatter:
|
||||
subtext = None
|
||||
subtext_header = None
|
||||
if subcommands:
|
||||
subtext_header = "Subcommands:"
|
||||
subtext_header = T_("Subcommands:")
|
||||
max_width = max(discord.utils._string_width(name) for name in subcommands.keys())
|
||||
|
||||
def width_maker(cmds):
|
||||
@@ -261,7 +264,7 @@ class RedHelpFormatter:
|
||||
(
|
||||
description,
|
||||
signature[1:-1],
|
||||
command.help.replace("[p]", ctx.clean_prefix),
|
||||
command.format_help_for_context(ctx),
|
||||
subtext_header,
|
||||
subtext,
|
||||
),
|
||||
@@ -301,7 +304,10 @@ class RedHelpFormatter:
|
||||
page_char_limit = await ctx.bot._config.help.page_char_limit()
|
||||
page_char_limit = min(page_char_limit, 5500) # Just in case someone was manually...
|
||||
|
||||
author_info = {"name": f"{ctx.me.display_name} Help Menu", "icon_url": ctx.me.avatar_url}
|
||||
author_info = {
|
||||
"name": f"{ctx.me.display_name} {T_('Help Menu')}",
|
||||
"icon_url": ctx.me.avatar_url,
|
||||
}
|
||||
|
||||
# Offset calculation here is for total embed size limit
|
||||
# 20 accounts for# *Page {i} of {page_count}*
|
||||
@@ -346,7 +352,9 @@ class RedHelpFormatter:
|
||||
embed = discord.Embed(color=color, **embed_dict["embed"])
|
||||
|
||||
if page_count > 1:
|
||||
description = f"*Page {i} of {page_count}*\n{embed.description}"
|
||||
description = T_(
|
||||
"*Page {page_num} of {page_count}*\n{content_description}"
|
||||
).format(content_description=embed.description, page_num=i, page_count=page_count)
|
||||
embed.description = description
|
||||
|
||||
embed.set_author(**author_info)
|
||||
@@ -366,7 +374,7 @@ class RedHelpFormatter:
|
||||
if not (coms or await ctx.bot._config.help.verify_exists()):
|
||||
return
|
||||
|
||||
description = obj.help
|
||||
description = obj.format_help_for_context(ctx)
|
||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||
|
||||
if await ctx.embed_requested():
|
||||
@@ -376,7 +384,7 @@ class RedHelpFormatter:
|
||||
if description:
|
||||
splitted = description.split("\n\n")
|
||||
name = splitted[0]
|
||||
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
||||
value = "\n\n".join(splitted[1:])
|
||||
if not value:
|
||||
value = EMPTY_STRING
|
||||
field = EmbedField(name[:252], value[:1024], False)
|
||||
@@ -395,9 +403,9 @@ class RedHelpFormatter:
|
||||
)
|
||||
for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)):
|
||||
if i == 0:
|
||||
title = "**__Commands:__**"
|
||||
title = T_("**__Commands:__**")
|
||||
else:
|
||||
title = "**__Commands:__** (continued)"
|
||||
title = T_("**__Commands:__** (continued)")
|
||||
field = EmbedField(title, page, False)
|
||||
emb["fields"].append(field)
|
||||
|
||||
@@ -407,7 +415,7 @@ class RedHelpFormatter:
|
||||
subtext = None
|
||||
subtext_header = None
|
||||
if coms:
|
||||
subtext_header = "Commands:"
|
||||
subtext_header = T_("Commands:")
|
||||
max_width = max(discord.utils._string_width(name) for name in coms.keys())
|
||||
|
||||
def width_maker(cmds):
|
||||
@@ -442,14 +450,14 @@ class RedHelpFormatter:
|
||||
|
||||
emb["footer"]["text"] = tagline
|
||||
if description:
|
||||
emb["embed"]["title"] = f"*{description[:2044]}*"
|
||||
emb["embed"]["title"] = f"*{description[:250]}*"
|
||||
|
||||
for cog_name, data in coms:
|
||||
|
||||
if cog_name:
|
||||
title = f"**__{cog_name}:__**"
|
||||
else:
|
||||
title = f"**__No Category:__**"
|
||||
title = f"**__{T_('No Category')}:__**"
|
||||
|
||||
def shorten_line(a_line: str) -> str:
|
||||
if len(a_line) < 70: # embed max width needs to be lower
|
||||
@@ -462,7 +470,7 @@ class RedHelpFormatter:
|
||||
)
|
||||
|
||||
for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)):
|
||||
title = title if i < 1 else f"{title} (continued)"
|
||||
title = title if i < 1 else f"{title} {T_('(continued)')}"
|
||||
field = EmbedField(title, page, False)
|
||||
emb["fields"].append(field)
|
||||
|
||||
@@ -478,7 +486,7 @@ class RedHelpFormatter:
|
||||
names.extend(list(v.name for v in v.values()))
|
||||
|
||||
max_width = max(
|
||||
discord.utils._string_width((name or "No Category:")) for name in names
|
||||
discord.utils._string_width((name or T_("No Category:"))) for name in names
|
||||
)
|
||||
|
||||
def width_maker(cmds):
|
||||
@@ -492,7 +500,7 @@ class RedHelpFormatter:
|
||||
|
||||
for cog_name, data in coms:
|
||||
|
||||
title = f"{cog_name}:" if cog_name else "No Category:"
|
||||
title = f"{cog_name}:" if cog_name else T_("No Category:")
|
||||
to_join.append(title)
|
||||
|
||||
for name, doc, width in width_maker(sorted(data.items())):
|
||||
@@ -543,7 +551,9 @@ class RedHelpFormatter:
|
||||
if fuzzy_commands:
|
||||
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
||||
if use_embeds:
|
||||
ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
||||
ret.set_author(
|
||||
name=f"{ctx.me.display_name} {T_('Help Menu')}", icon_url=ctx.me.avatar_url
|
||||
)
|
||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||
ret.set_footer(text=tagline)
|
||||
await ctx.send(embed=ret)
|
||||
@@ -553,7 +563,9 @@ class RedHelpFormatter:
|
||||
ret = T_("Help topic for *{command_name}* not found.").format(command_name=help_for)
|
||||
if use_embeds:
|
||||
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
||||
ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
||||
ret.set_author(
|
||||
name=f"{ctx.me.display_name} {T_('Help Menu')}", icon_url=ctx.me.avatar_url
|
||||
)
|
||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||
ret.set_footer(text=tagline)
|
||||
await ctx.send(embed=ret)
|
||||
@@ -569,7 +581,9 @@ class RedHelpFormatter:
|
||||
)
|
||||
if await ctx.embed_requested():
|
||||
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
||||
ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
|
||||
ret.set_author(
|
||||
name=f"{ctx.me.display_name} {T_('Help Menu')}", icon_url=ctx.me.avatar_url
|
||||
)
|
||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||
ret.set_footer(text=tagline)
|
||||
await ctx.send(embed=ret)
|
||||
|
||||
@@ -95,8 +95,8 @@ class PrivilegeLevel(enum.IntEnum):
|
||||
"""Enumeration for special privileges."""
|
||||
|
||||
# Maintainer Note: do NOT re-order these.
|
||||
# Each privelege level also implies access to the ones before it.
|
||||
# Inserting new privelege levels at a later point is fine if that is considered.
|
||||
# Each privilege level also implies access to the ones before it.
|
||||
# Inserting new privilege levels at a later point is fine if that is considered.
|
||||
|
||||
NONE = enum.auto()
|
||||
"""No special privilege level."""
|
||||
|
||||
@@ -257,10 +257,9 @@ class CoreLogic:
|
||||
The current (or new) list of prefixes.
|
||||
"""
|
||||
if prefixes:
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await self.bot._config.prefix.set(prefixes)
|
||||
await self.bot._prefix_cache.set_prefixes(guild=None, prefixes=prefixes)
|
||||
return prefixes
|
||||
return await self.bot._config.prefix()
|
||||
return await self.bot._prefix_cache.get_prefixes(guild=None)
|
||||
|
||||
@classmethod
|
||||
async def _version_info(cls) -> Dict[str, str]:
|
||||
@@ -563,7 +562,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
msg = ""
|
||||
responses = []
|
||||
for i, server in enumerate(guilds, 1):
|
||||
msg += "{}: {}\n".format(i, server.name)
|
||||
msg += "{}: {} (`{}`)\n".format(i, server.name, server.id)
|
||||
responses.append(str(i))
|
||||
|
||||
for page in pagify(msg, ["\n"]):
|
||||
@@ -847,15 +846,13 @@ class Core(commands.Cog, CoreLogic):
|
||||
mod_role_ids = await ctx.bot._config.guild(ctx.guild).mod_role()
|
||||
mod_role_names = [r.name for r in guild.roles if r.id in mod_role_ids]
|
||||
mod_roles_str = humanize_list(mod_role_names) if mod_role_names else "Not Set."
|
||||
prefixes = await ctx.bot._config.guild(ctx.guild).prefix()
|
||||
guild_settings = _("Admin roles: {admin}\nMod roles: {mod}\n").format(
|
||||
admin=admin_roles_str, mod=mod_roles_str
|
||||
)
|
||||
else:
|
||||
guild_settings = ""
|
||||
prefixes = None # This is correct. The below can happen in a guild.
|
||||
if not prefixes:
|
||||
prefixes = await ctx.bot._config.prefix()
|
||||
|
||||
prefixes = await ctx.bot._prefix_cache.get_prefixes(ctx.guild)
|
||||
locale = await ctx.bot._config.locale()
|
||||
|
||||
prefix_string = " ".join(prefixes)
|
||||
@@ -873,6 +870,32 @@ class Core(commands.Cog, CoreLogic):
|
||||
for page in pagify(settings):
|
||||
await ctx.send(box(page))
|
||||
|
||||
@checks.is_owner()
|
||||
@_set.command(name="description")
|
||||
async def setdescription(self, ctx: commands.Context, *, description: str = ""):
|
||||
"""
|
||||
Sets the bot's description.
|
||||
Use without a description to reset.
|
||||
This is shown in a few locations, including the help menu.
|
||||
|
||||
The default is "Red V3"
|
||||
"""
|
||||
if not description:
|
||||
await ctx.bot._config.description.clear()
|
||||
ctx.bot.description = "Red V3"
|
||||
await ctx.send(_("Description reset."))
|
||||
elif len(description) > 250: # While the limit is 256, we bold it adding characters.
|
||||
await ctx.send(
|
||||
_(
|
||||
"This description is too long to properly display. "
|
||||
"Please try again with below 250 characters"
|
||||
)
|
||||
)
|
||||
else:
|
||||
await ctx.bot._config.description.set(description)
|
||||
ctx.bot.description = description
|
||||
await ctx.tick()
|
||||
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
@@ -1156,11 +1179,11 @@ class Core(commands.Cog, CoreLogic):
|
||||
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
|
||||
"""Sets Red's server prefix(es)"""
|
||||
if not prefixes:
|
||||
await ctx.bot._config.guild(ctx.guild).prefix.set([])
|
||||
await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=[])
|
||||
await ctx.send(_("Guild prefixes have been reset."))
|
||||
return
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await ctx.bot._config.guild(ctx.guild).prefix.set(prefixes)
|
||||
await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=prefixes)
|
||||
await ctx.send(_("Prefix set."))
|
||||
|
||||
@_set.command()
|
||||
|
||||
@@ -4,18 +4,20 @@ from . import commands
|
||||
|
||||
def init_global_checks(bot):
|
||||
@bot.check_once
|
||||
def actually_up(ctx):
|
||||
"""
|
||||
Uptime is set during the initial startup process.
|
||||
If this hasn't been set, we should assume the bot isn't ready yet.
|
||||
def minimum_bot_perms(ctx) -> bool:
|
||||
"""
|
||||
return ctx.bot.uptime is not None
|
||||
Too many 403, 401, and 429 Errors can cause bots to get global'd
|
||||
|
||||
It's reasonable to assume the below as a minimum amount of perms for
|
||||
commands.
|
||||
"""
|
||||
return ctx.channel.permissions_for(ctx.me).send_messages
|
||||
|
||||
@bot.check_once
|
||||
async def whiteblacklist_checks(ctx):
|
||||
async def whiteblacklist_checks(ctx) -> bool:
|
||||
return await ctx.bot.allowed_by_whitelist_blacklist(ctx.author)
|
||||
|
||||
@bot.check_once
|
||||
def bots(ctx):
|
||||
def bots(ctx) -> bool:
|
||||
"""Check the user is not another bot."""
|
||||
return not ctx.author.bot
|
||||
|
||||
@@ -142,6 +142,18 @@ async def _init(bot: Red):
|
||||
bot.add_listener(on_member_unban)
|
||||
|
||||
|
||||
async def handle_auditype_key():
|
||||
all_casetypes = {
|
||||
casetype_name: {
|
||||
inner_key: inner_value
|
||||
for inner_key, inner_value in casetype_data.items()
|
||||
if inner_key != "audit_type"
|
||||
}
|
||||
for casetype_name, casetype_data in (await _conf.custom(_CASETYPES).all()).items()
|
||||
}
|
||||
await _conf.custom(_CASETYPES).set(all_casetypes)
|
||||
|
||||
|
||||
async def _migrate_config(from_version: int, to_version: int):
|
||||
if from_version == to_version:
|
||||
return
|
||||
@@ -170,16 +182,7 @@ async def _migrate_config(from_version: int, to_version: int):
|
||||
await _conf.guild(cast(discord.Guild, discord.Object(id=guild_id))).clear_raw("cases")
|
||||
|
||||
if from_version < 3 <= to_version:
|
||||
all_casetypes = {
|
||||
casetype_name: {
|
||||
inner_key: inner_value
|
||||
for inner_key, inner_value in casetype_data.items()
|
||||
if inner_key != "audit_type"
|
||||
}
|
||||
for casetype_name, casetype_data in (await _conf.custom(_CASETYPES).all()).items()
|
||||
}
|
||||
|
||||
await _conf.custom(_CASETYPES).set(all_casetypes)
|
||||
await handle_auditype_key()
|
||||
await _conf.schema_version.set(3)
|
||||
|
||||
if from_version < 4 <= to_version:
|
||||
@@ -507,8 +510,15 @@ class CaseType:
|
||||
self.image = image
|
||||
self.case_str = case_str
|
||||
self.guild = guild
|
||||
|
||||
if "audit_type" in kwargs:
|
||||
kwargs.pop("audit_type", None)
|
||||
log.warning(
|
||||
"Fix this using the hidden command: `modlogset fixcasetypes` in Discord: "
|
||||
"Got outdated key in casetype: audit_type"
|
||||
)
|
||||
if kwargs:
|
||||
log.warning("Got unexpected keys in case %s", ",".join(kwargs.keys()))
|
||||
log.warning("Got unexpected key(s) in casetype: %s", ",".join(kwargs.keys()))
|
||||
|
||||
async def to_json(self):
|
||||
"""Transforms the case type into a dict and saves it"""
|
||||
|
||||
53
redbot/core/settings_caches.py
Normal file
53
redbot/core/settings_caches.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from argparse import Namespace
|
||||
|
||||
import discord
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
class PrefixManager:
|
||||
def __init__(self, config: Config, cli_flags: Namespace):
|
||||
self._config: Config = config
|
||||
self._global_prefix_overide: Optional[List[str]] = sorted(
|
||||
cli_flags.prefix, reverse=True
|
||||
) or None
|
||||
self._cached: Dict[Optional[int], List[str]] = {}
|
||||
|
||||
async def get_prefixes(self, guild: Optional[discord.Guild] = None) -> List[str]:
|
||||
ret: List[str]
|
||||
|
||||
gid: Optional[int] = guild.id if guild else None
|
||||
|
||||
if gid in self._cached:
|
||||
ret = self._cached[gid].copy()
|
||||
else:
|
||||
if gid is not None:
|
||||
ret = await self._config.guild_from_id(gid).prefix()
|
||||
if not ret:
|
||||
ret = await self.get_prefixes(None)
|
||||
else:
|
||||
ret = self._global_prefix_overide or (await self._config.prefix())
|
||||
|
||||
self._cached[gid] = ret.copy()
|
||||
|
||||
return ret
|
||||
|
||||
async def set_prefixes(
|
||||
self, guild: Optional[discord.Guild] = None, prefixes: Optional[List[str]] = None
|
||||
):
|
||||
gid: Optional[int] = guild.id if guild else None
|
||||
prefixes = prefixes or []
|
||||
if not isinstance(prefixes, list) and not all(isinstance(pfx, str) for pfx in prefixes):
|
||||
raise TypeError("Prefixes must be a list of strings")
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
if gid is None:
|
||||
if not prefixes:
|
||||
raise ValueError("You must have at least one prefix.")
|
||||
self._cached.clear()
|
||||
await self._config.prefix.set(prefixes)
|
||||
else:
|
||||
del self._cached[gid]
|
||||
await self._config.guild_from_id(gid).prefix.set(prefixes)
|
||||
Reference in New Issue
Block a user