diff options
author | 2022-04-30 19:27:04 +0100 | |
---|---|---|
committer | 2022-04-30 19:27:04 +0100 | |
commit | ce10a11b6f53afd877710d09ee375f5e31864201 (patch) | |
tree | 4f13a53d207d2670956764b12211d6bc22151150 | |
parent | Only suppress 404s from site when checking for bumped threads (diff) | |
parent | Merge pull request #2160 from python-discord/2128 (diff) |
Merge branch 'main' into improve-thread-bump
-rw-r--r-- | .github/CODEOWNERS | 2 | ||||
-rw-r--r-- | bot/converters.py | 14 | ||||
-rw-r--r-- | bot/exts/backend/sync/_cog.py | 16 | ||||
-rw-r--r-- | bot/exts/backend/sync/_syncers.py | 13 | ||||
-rw-r--r-- | bot/exts/info/doc/_cog.py | 2 | ||||
-rw-r--r-- | bot/exts/moderation/clean.py | 5 | ||||
-rw-r--r-- | bot/exts/moderation/defcon.py | 33 | ||||
-rw-r--r-- | bot/exts/moderation/incidents.py | 2 | ||||
-rw-r--r-- | bot/exts/moderation/modpings.py | 2 | ||||
-rw-r--r-- | bot/exts/moderation/slowmode.py | 21 | ||||
-rw-r--r-- | bot/exts/utils/extensions.py | 25 | ||||
-rw-r--r-- | bot/resources/tags/or-gotcha.md | 1 | ||||
-rw-r--r-- | bot/utils/extensions.py | 34 | ||||
-rw-r--r-- | poetry.lock | 10 | ||||
-rw-r--r-- | pyproject.toml | 4 | ||||
-rw-r--r-- | tests/helpers.py | 2 |
16 files changed, 86 insertions, 100 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ea69f7677..0bc2bb793 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,10 +18,8 @@ bot/exts/recruitment/** @wookie184 bot/rules/** @mbaruh # Utils -bot/utils/extensions.py @MarkKoz bot/utils/function.py @MarkKoz bot/utils/lock.py @MarkKoz -bot/utils/scheduling.py @MarkKoz # Tests tests/_autospec.py @MarkKoz diff --git a/bot/converters.py b/bot/converters.py index a3f4630a0..910ed9e39 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -9,19 +9,19 @@ import dateutil.parser import discord from aiohttp import ClientConnectorError from botcore.site_api import ResponseCodeError +from botcore.utils import unqualify from botcore.utils.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter from discord.utils import escape_markdown, snowflake_time -from bot import exts +from bot import exts, instance as bot_instance from bot.constants import URLs from bot.errors import InvalidInfraction from bot.exts.info.doc import _inventory_parser from bot.exts.info.tags import TagIdentifier from bot.log import get_logger from bot.utils import time -from bot.utils.extensions import EXTENSIONS, unqualify if t.TYPE_CHECKING: from bot.exts.info.source import SourceType @@ -150,13 +150,13 @@ class Extension(Converter): argument = argument.lower() - if argument in EXTENSIONS: + if argument in bot_instance.all_extensions: return argument - elif (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: + elif (qualified_arg := f"{exts.__name__}.{argument}") in bot_instance.all_extensions: return qualified_arg matches = [] - for ext in EXTENSIONS: + for ext in bot_instance.all_extensions: if argument == unqualify(ext): matches.append(ext) @@ -382,8 +382,8 @@ class Age(DurationDelta): class OffTopicName(Converter): """A converter that ensures an added off-topic name is valid.""" - ALLOWED_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-<>" - TRANSLATED_CHARACTERS = "๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐นว๏ผโโ-๏ผ๏ผ" + ALLOWED_CHARACTERS = r"ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-<>\/" + TRANSLATED_CHARACTERS = "๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐นว๏ผโโ-๏ผ๏ผโงนโงธ" @classmethod def translate_name(cls, name: str, *, from_unicode: bool = True) -> str: diff --git a/bot/exts/backend/sync/_cog.py b/bot/exts/backend/sync/_cog.py index a5bf82397..85266340b 100644 --- a/bot/exts/backend/sync/_cog.py +++ b/bot/exts/backend/sync/_cog.py @@ -1,9 +1,11 @@ +import asyncio +import datetime from typing import Any, Dict from botcore.site_api import ResponseCodeError from discord import Member, Role, User from discord.ext import commands -from discord.ext.commands import Cog, Context +from discord.ext.commands import Cog, Context, errors from bot import constants from bot.bot import Bot @@ -27,6 +29,18 @@ class Sync(Cog): if guild is None: return + log.info("Waiting for guild to be chunked to start syncers.") + end = datetime.datetime.now() + datetime.timedelta(minutes=30) + while not guild.chunked: + await asyncio.sleep(10) + if datetime.datetime.now() > end: + # More than 30 minutes have passed while trying, abort + raise errors.ExtensionFailed( + self.__class__.__name__, + RuntimeError("The guild was not chunked in time, not loading sync cog.") + ) + + log.info("Starting syncers.") for syncer in (_syncers.RoleSyncer, _syncers.UserSyncer): await syncer.sync(guild) diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index e1c4541ef..799137cb9 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -2,6 +2,7 @@ import abc import typing as t from collections import namedtuple +import discord.errors from botcore.site_api import ResponseCodeError from discord import Guild from discord.ext.commands import Context @@ -9,7 +10,6 @@ from more_itertools import chunked import bot from bot.log import get_logger -from bot.utils.members import get_or_fetch_member log = get_logger(__name__) @@ -157,7 +157,16 @@ class UserSyncer(Syncer): if db_user[db_field] != guild_value: updated_fields[db_field] = guild_value - if guild_user := await get_or_fetch_member(guild, db_user["id"]): + guild_user = guild.get_member(db_user["id"]) + if not guild_user and db_user["in_guild"]: + # The member was in the guild during the last sync. + # We try to fetch them to verify cache integrity. + try: + guild_user = await guild.fetch_member(db_user["id"]) + except discord.errors.NotFound: + guild_user = None + + if guild_user: seen_guild_users.add(guild_user.id) maybe_update("name", guild_user.name) diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py index 079bfc942..dece44063 100644 --- a/bot/exts/info/doc/_cog.py +++ b/bot/exts/info/doc/_cog.py @@ -431,7 +431,7 @@ class DocCog(commands.Cog): async def refresh_command(self, ctx: commands.Context) -> None: """Refresh inventories and show the difference.""" old_inventories = set(self.base_urls) - with ctx.typing(): + async with ctx.typing(): await self.refresh_inventories() new_inventories = set(self.base_urls) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 32e485996..0f14f515e 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -441,7 +441,10 @@ class Clean(Cog): f"A log of the deleted messages can be found here {log_url}." ) if log_url and is_mod_channel(ctx.channel): - await ctx.reply(success_message) + try: + await ctx.reply(success_message) + except errors.NotFound: + await ctx.send(success_message) elif log_url: if mods := self.bot.get_channel(Channels.mods): await mods.send(f"{ctx.author.mention} {success_message}") diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index d092af6e9..1df79149d 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -71,10 +71,11 @@ class Defcon(Cog): scheduling.create_task(self._sync_settings(), event_loop=self.bot.loop) - @property - def mod_log(self) -> ModLog: + async def get_mod_log(self) -> ModLog: """Get currently loaded ModLog cog instance.""" - return self.bot.get_cog("ModLog") + while not (cog := self.bot.get_cog("ModLog")): + await asyncio.sleep(1) + return cog @defcon_settings.atomic_transaction async def _sync_settings(self) -> None: @@ -82,20 +83,6 @@ class Defcon(Cog): log.trace("Waiting for the guild to become available before syncing.") await self.bot.wait_until_guild_available() - # Load order isn't guaranteed, attempt to check mod log load status 3 times before erroring. - for _ in range(3): - if self.mod_log: - break - else: - await asyncio.sleep(5) - else: - log.exception("Modlog cog not loaded, aborting sync.") - await self.channel.send( - f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" - f"\n\nmod log cog could not be found after 3 tries." - ) - return - self.channel = await self.bot.fetch_channel(Channels.defcon) log.trace("Syncing settings.") @@ -118,7 +105,7 @@ class Defcon(Cog): self._update_notifier() log.info(f"DEFCON synchronized: {time.humanize_delta(self.threshold) if self.threshold else '-'}") - self._update_channel_topic() + await self._update_channel_topic() @Cog.listener() async def on_member_join(self, member: Member) -> None: @@ -150,7 +137,7 @@ class Defcon(Cog): if not message_sent: message = f"{message}\n\nUnable to send rejection message via DM; they probably have DMs disabled." - await self.mod_log.send_log_message( + await (await self.get_mod_log()).send_log_message( Icons.defcon_denied, Colours.soft_red, "Entry denied", message, member.display_avatar.url ) @@ -226,12 +213,12 @@ class Defcon(Cog): await role.edit(reason="DEFCON unshutdown", permissions=permissions) await ctx.send(f"{Action.SERVER_OPEN.value.emoji} Server reopened.") - def _update_channel_topic(self) -> None: + async def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" threshold = time.humanize_delta(self.threshold) if self.threshold else '-' new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {threshold})" - self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) + (await self.get_mod_log()).ignore(Event.guild_channel_update, Channels.defcon) scheduling.create_task(self.channel.edit(topic=new_topic)) @defcon_settings.atomic_transaction @@ -290,7 +277,7 @@ class Defcon(Cog): await channel.send(message) await self._send_defcon_log(action, author) - self._update_channel_topic() + await self._update_channel_topic() self._log_threshold_stat(threshold) @@ -318,7 +305,7 @@ class Defcon(Cog): ) status_msg = f"DEFCON {action.name.lower()}" - await self.mod_log.send_log_message(info.icon, info.color, status_msg, log_msg) + await (await self.get_mod_log()).send_log_message(info.icon, info.color, status_msg, log_msg) def _update_notifier(self) -> None: """Start or stop the notifier according to the DEFCON status.""" diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index b65f9262f..155b123ca 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -404,7 +404,7 @@ class Incidents(Cog): def check(payload: discord.RawReactionActionEvent) -> bool: return payload.message_id == incident.id - coroutine = self.bot.wait_for(event="raw_message_delete", check=check, timeout=timeout) + coroutine = self.bot.wait_for("raw_message_delete", check=check, timeout=timeout) return scheduling.create_task(coroutine, event_loop=self.bot.loop) async def process_event(self, reaction: str, incident: discord.Message, member: discord.Member) -> None: diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 4d02c4530..e82099c88 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -42,7 +42,7 @@ class ModPings(Cog): async def cog_load(self) -> None: """Schedule both when to reapply role and all mod ping schedules.""" - await self.reschedule_modpings_schedule() + # await self.reschedule_modpings_schedule() await self.reschedule_roles() async def reschedule_roles(self) -> None: diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 1be568a56..c43ae8b0c 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -1,7 +1,7 @@ -from typing import Optional +from typing import Literal, Optional, Union from dateutil.relativedelta import relativedelta -from discord import TextChannel +from discord import TextChannel, Thread from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot @@ -20,6 +20,8 @@ COMMONLY_SLOWMODED_CHANNELS = { Channels.off_topic_0: "ot0", } +MessageHolder = Optional[Union[TextChannel, Thread]] + class Slowmode(Cog): """Commands for getting and setting slowmode delays of text channels.""" @@ -33,7 +35,7 @@ class Slowmode(Cog): await ctx.send_help(ctx.command) @slowmode_group.command(name='get', aliases=['g']) - async def get_slowmode(self, ctx: Context, channel: Optional[TextChannel]) -> None: + async def get_slowmode(self, ctx: Context, channel: MessageHolder) -> None: """Get the slowmode delay for a text channel.""" # Use the channel this command was invoked in if one was not given if channel is None: @@ -44,7 +46,12 @@ class Slowmode(Cog): await ctx.send(f'The slowmode delay for {channel.mention} is {humanized_delay}.') @slowmode_group.command(name='set', aliases=['s']) - async def set_slowmode(self, ctx: Context, channel: Optional[TextChannel], delay: DurationDelta) -> None: + async def set_slowmode( + self, + ctx: Context, + channel: MessageHolder, + delay: Union[DurationDelta, Literal["0s", "0seconds"]], + ) -> None: """Set the slowmode delay for a text channel.""" # Use the channel this command was invoked in if one was not given if channel is None: @@ -52,8 +59,10 @@ class Slowmode(Cog): # Convert `dateutil.relativedelta.relativedelta` to `datetime.timedelta` # Must do this to get the delta in a particular unit of time - slowmode_delay = time.relativedelta_to_timedelta(delay).total_seconds() + if isinstance(delay, str): + delay = relativedelta(seconds=0) + slowmode_delay = time.relativedelta_to_timedelta(delay).total_seconds() humanized_delay = time.humanize_delta(delay) # Ensure the delay is within discord's limits @@ -80,7 +89,7 @@ class Slowmode(Cog): ) @slowmode_group.command(name='reset', aliases=['r']) - async def reset_slowmode(self, ctx: Context, channel: Optional[TextChannel]) -> None: + async def reset_slowmode(self, ctx: Context, channel: MessageHolder) -> None: """Reset the slowmode delay for a text channel to 0 seconds.""" await self.set_slowmode(ctx, channel, relativedelta(seconds=0)) diff --git a/bot/exts/utils/extensions.py b/bot/exts/utils/extensions.py index 95ce94c2c..0f5fc0de4 100644 --- a/bot/exts/utils/extensions.py +++ b/bot/exts/utils/extensions.py @@ -12,7 +12,6 @@ from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs from bot.converters import Extension from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils.extensions import EXTENSIONS log = get_logger(__name__) @@ -53,9 +52,9 @@ class Extensions(commands.Cog): return if "*" in extensions or "**" in extensions: - extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) + extensions = set(self.bot.all_extensions) - set(self.bot.extensions.keys()) - msg = self.batch_manage(Action.LOAD, *extensions) + msg = await self.batch_manage(Action.LOAD, *extensions) await ctx.send(msg) @extensions_group.command(name="unload", aliases=("ul",)) @@ -77,7 +76,7 @@ class Extensions(commands.Cog): if "*" in extensions or "**" in extensions: extensions = set(self.bot.extensions.keys()) - UNLOAD_BLACKLIST - msg = self.batch_manage(Action.UNLOAD, *extensions) + msg = await self.batch_manage(Action.UNLOAD, *extensions) await ctx.send(msg) @@ -96,12 +95,12 @@ class Extensions(commands.Cog): return if "**" in extensions: - extensions = EXTENSIONS + extensions = self.bot.all_extensions elif "*" in extensions: extensions = set(self.bot.extensions.keys()) | set(extensions) extensions.remove("*") - msg = self.batch_manage(Action.RELOAD, *extensions) + msg = await self.batch_manage(Action.RELOAD, *extensions) await ctx.send(msg) @@ -136,7 +135,7 @@ class Extensions(commands.Cog): """Return a mapping of extension names and statuses to their categories.""" categories = {} - for ext in EXTENSIONS: + for ext in self.bot.all_extensions: if ext in self.bot.extensions: status = Emojis.status_online else: @@ -152,21 +151,21 @@ class Extensions(commands.Cog): return categories - def batch_manage(self, action: Action, *extensions: str) -> str: + async def batch_manage(self, action: Action, *extensions: str) -> str: """ Apply an action to multiple extensions and return a message with the results. If only one extension is given, it is deferred to `manage()`. """ if len(extensions) == 1: - msg, _ = self.manage(action, extensions[0]) + msg, _ = await self.manage(action, extensions[0]) return msg verb = action.name.lower() failures = {} for extension in extensions: - _, error = self.manage(action, extension) + _, error = await self.manage(action, extension) if error: failures[extension] = error @@ -181,17 +180,17 @@ class Extensions(commands.Cog): return msg - def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: + async def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: """Apply an action to an extension and return the status message and any error message.""" verb = action.name.lower() error_msg = None try: - action.value(self.bot, ext) + await action.value(self.bot, ext) except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): if action is Action.RELOAD: # When reloading, just load the extension if it was not loaded. - return self.manage(Action.LOAD, ext) + return await self.manage(Action.LOAD, ext) msg = f":x: Extension `{ext}` is already {verb}ed." log.debug(msg[4:]) diff --git a/bot/resources/tags/or-gotcha.md b/bot/resources/tags/or-gotcha.md index d75a73d78..25ade8620 100644 --- a/bot/resources/tags/or-gotcha.md +++ b/bot/resources/tags/or-gotcha.md @@ -1,5 +1,6 @@ When checking if something is equal to one thing or another, you might think that this is possible: ```py +# Incorrect... if favorite_fruit == 'grapefruit' or 'lemon': print("That's a weird favorite fruit to have.") ``` diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py deleted file mode 100644 index 50350ea8d..000000000 --- a/bot/utils/extensions.py +++ /dev/null @@ -1,34 +0,0 @@ -import importlib -import inspect -import pkgutil -from typing import Iterator, NoReturn - -from bot import exts - - -def unqualify(name: str) -> str: - """Return an unqualified name given a qualified module/package `name`.""" - return name.rsplit(".", maxsplit=1)[-1] - - -def walk_extensions() -> Iterator[str]: - """Yield extension names from the bot.exts subpackage.""" - - def on_error(name: str) -> NoReturn: - raise ImportError(name=name) # pragma: no cover - - for module in pkgutil.walk_packages(exts.__path__, f"{exts.__name__}.", onerror=on_error): - if unqualify(module.name).startswith("_"): - # Ignore module/package names starting with an underscore. - continue - - if module.ispkg: - imported = importlib.import_module(module.name) - if not inspect.isfunction(getattr(imported, "setup", None)): - # If it lacks a setup function, it's not an extension. - continue - - yield module.name - - -EXTENSIONS = frozenset(walk_extensions()) diff --git a/poetry.lock b/poetry.lock index e90274559..8281394e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -125,7 +125,7 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "6.0.0" +version = "6.4.0" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false @@ -133,7 +133,7 @@ python-versions = "3.9.*" [package.dependencies] async-rediscache = {version = "0.2.0", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/987235d5649e7c2b1a927637bab6547244ecb2cf.zip"} +"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/5a06fa5f3e28d2b7191722e1a84c541560008aea.zip"} statsd = "3.3.0" [package.extras] @@ -141,7 +141,7 @@ async-rediscache = ["async-rediscache[fakeredis] (==0.2.0)"] [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v6.0.0.zip" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v6.4.0.zip" [[package]] name = "certifi" version = "2021.10.8" @@ -263,7 +263,7 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"] [package.source] type = "url" -url = "https://github.com/Rapptz/discord.py/archive/987235d5649e7c2b1a927637bab6547244ecb2cf.zip" +url = "https://github.com/Rapptz/discord.py/archive/5a06fa5f3e28d2b7191722e1a84c541560008aea.zip" [[package]] name = "distlib" @@ -1150,7 +1150,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "3a9451cdbafd9880f794fe5ea4ece75663a778708707e7b5fb5e9aaffc2bbbc8" +content-hash = "a07f619c75f8133982984eb506ad350144829f10c704421f09b3dbe72cd037d8" [metadata.files] aiodns = [ diff --git a/pyproject.toml b/pyproject.toml index 2a4415419..402d05f70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ license = "MIT" [tool.poetry.dependencies] python = "3.9.*" -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/987235d5649e7c2b1a927637bab6547244ecb2cf.zip"} +"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/5a06fa5f3e28d2b7191722e1a84c541560008aea.zip"} # See https://bot-core.pythondiscord.com/ for docs. -bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v6.0.0.zip", extras = ["async-rediscache"]} +bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v6.4.0.zip", extras = ["async-rediscache"]} aiodns = "3.0.0" aiohttp = "3.8.1" diff --git a/tests/helpers.py b/tests/helpers.py index a6e4bdd66..5f3111616 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -171,7 +171,7 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin): spec_set = guild_instance def __init__(self, roles: Optional[Iterable[MockRole]] = None, **kwargs) -> None: - default_kwargs = {'id': next(self.discord_id), 'members': []} + default_kwargs = {'id': next(self.discord_id), 'members': [], "chunked": True} super().__init__(**collections.ChainMap(kwargs, default_kwargs)) self.roles = [MockRole(name="@everyone", position=1, id=0)] |