Merge branch 'V3/develop' into V3/feature/mutes

This commit is contained in:
Michael H
2020-01-17 20:25:45 -05:00
39 changed files with 1090 additions and 551 deletions

View File

@@ -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")
)
)

View File

@@ -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

View File

@@ -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",

View File

@@ -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.

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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()

View File

@@ -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

View File

@@ -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"""

View 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)