From 10cf0802af4ba491f0b714a81f16637fadfd5810 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 28 Jun 2022 22:35:37 +0100 Subject: Use monkey patches from botcore --- bot/__init__.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) (limited to 'bot/__init__.py') diff --git a/bot/__init__.py b/bot/__init__.py index 3136c863..c1ecb30f 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -8,15 +8,14 @@ except ModuleNotFoundError: import asyncio import logging import os -from functools import partial, partialmethod import arrow import sentry_sdk -from discord.ext import commands +from botcore.utils import apply_monkey_patches from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration -from bot import log, monkey_patches +from bot import log sentry_logging = LoggingIntegration( level=logging.DEBUG, @@ -41,17 +40,4 @@ start_time = arrow.utcnow() if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -monkey_patches.patch_typing() - -# This patches any convertors that use PartialMessage, but not the PartialMessageConverter itself -# as library objects are made by this mapping. -# https://github.com/Rapptz/discord.py/blob/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f/discord/ext/commands/converter.py#L984-L1004 -commands.converter.PartialMessageConverter = monkey_patches.FixedPartialMessageConverter - -# Monkey-patch discord.py decorators to use the both the Command and Group subclasses which supports root aliases. -# Must be patched before any cogs are added. -commands.command = partial(commands.command, cls=monkey_patches.Command) -commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=monkey_patches.Command) - -commands.group = partial(commands.group, cls=monkey_patches.Group) -commands.GroupMixin.group = partialmethod(commands.GroupMixin.group, cls=monkey_patches.Group) +apply_monkey_patches() -- cgit v1.2.3 From 510da5190a0f499397c9419fa3b125233bed566c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 28 Jun 2022 22:36:22 +0100 Subject: Use BotBase from bot core --- bot/__init__.py | 6 ++ bot/__main__.py | 70 ++++++++++++++++++++---- bot/bot.py | 166 +++++--------------------------------------------------- 3 files changed, 79 insertions(+), 163 deletions(-) (limited to 'bot/__init__.py') diff --git a/bot/__init__.py b/bot/__init__.py index c1ecb30f..33fd4e1c 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -8,6 +8,7 @@ except ModuleNotFoundError: import asyncio import logging import os +from typing import TYPE_CHECKING import arrow import sentry_sdk @@ -17,6 +18,9 @@ from sentry_sdk.integrations.redis import RedisIntegration from bot import log +if TYPE_CHECKING: + from bot.bot import Bot + sentry_logging = LoggingIntegration( level=logging.DEBUG, event_level=logging.WARNING @@ -41,3 +45,5 @@ if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) apply_monkey_patches() + +instance: "Bot" = None # Global Bot instance. diff --git a/bot/__main__.py b/bot/__main__.py index bd6c70ee..418fd91b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,19 +1,67 @@ +import asyncio import logging -from bot.bot import bot -from bot.constants import Client, STAFF_ROLES, WHITELISTED_CHANNELS +import aiohttp +import discord +from async_rediscache import RedisSession +from botcore import StartupError +from discord.ext import commands + +import bot +from bot import constants +from bot.bot import Bot from bot.utils.decorators import whitelist_check -from bot.utils.extensions import walk_extensions log = logging.getLogger(__name__) -bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES)) -for ext in walk_extensions(): - bot.load_extension(ext) +async def _create_redis_session() -> RedisSession: + """Create and connect to a redis session.""" + redis_session = RedisSession( + address=(constants.RedisConfig.host, constants.RedisConfig.port), + password=constants.RedisConfig.password, + minsize=1, + maxsize=20, + use_fakeredis=constants.RedisConfig.use_fakeredis, + global_namespace="bot", + ) + try: + await redis_session.connect() + except OSError as e: + raise StartupError(e) + return redis_session + + +async def main() -> None: + """Entry async method for starting the bot.""" + allowed_roles = list({discord.Object(id_) for id_ in constants.MODERATION_ROLES}) + intents = discord.Intents.default() + intents.bans = False + intents.integrations = False + intents.invites = False + intents.message_content = True + intents.typing = False + intents.webhooks = False + + async with aiohttp.ClientSession() as session: + bot.instance = Bot( + guild_id=constants.Client.guild, + http_session=session, + redis_session=await _create_redis_session(), + command_prefix=commands.when_mentioned_or(constants.Client.prefix), + activity=discord.Game(name=f"Commands: {constants.Client.prefix}help"), + case_insensitive=True, + allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles), + intents=intents, + allowed_roles=allowed_roles, + ) + + async with bot.instance as _bot: + _bot.add_check(whitelist_check( + channels=constants.WHITELISTED_CHANNELS, + roles=constants.STAFF_ROLES, + )) + await _bot.start(constants.Client.token) + -if not Client.in_ci: - # Manually enable the message content intent. This is required until the below PR is merged - # https://github.com/python-discord/sir-lancebot/pull/1092 - bot._connection._intents.value += 1 << 15 - bot.run(Client.token) +asyncio.run(main()) diff --git a/bot/bot.py b/bot/bot.py index c7b87a65..221bfd62 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,24 +1,20 @@ -import asyncio import logging -import socket -from contextlib import suppress from typing import Optional import discord -from aiohttp import AsyncResolver, ClientSession, TCPConnector -from async_rediscache import RedisSession -from discord import DiscordException, Embed, Forbidden, Thread +from botcore import BotBase +from botcore.utils import scheduling +from discord import DiscordException, Embed from discord.ext import commands -from discord.ext.commands import Cog, when_mentioned_or -from bot import constants +from bot import constants, exts log = logging.getLogger(__name__) -__all__ = ("Bot", "bot") +__all__ = ("Bot", ) -class Bot(commands.Bot): +class Bot(BotBase): """ Base bot instance. @@ -29,16 +25,6 @@ class Bot(commands.Bot): name = constants.Client.name - def __init__(self, redis_session: RedisSession, **kwargs): - super().__init__(**kwargs) - self.http_session = ClientSession( - connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET) - ) - self._guild_available = asyncio.Event() - self.redis_session = redis_session - self.loop.create_task(self.check_channels()) - self.loop.create_task(self.send_log(self.name, "Connected!")) - @property def member(self) -> Optional[discord.Member]: """Retrieves the guild member object for the bot.""" @@ -47,60 +33,6 @@ class Bot(commands.Bot): return None return guild.me - @Cog.listener() - async def on_thread_join(self, thread: Thread) -> None: - """ - Try to join newly created threads. - - Despite the event name being misleading, this is dispatched when new threads are created. - We want our bots to automatically join threads in order to answer commands using their prefixes. - """ - if thread.me: - # Already in this thread, return early - return - - with suppress(Forbidden): - await thread.join() - - async def close(self) -> None: - """Close Redis session when bot is shutting down.""" - await super().close() - - if self.http_session: - await self.http_session.close() - - if self.redis_session: - await self.redis_session.close() - - def add_cog(self, cog: commands.Cog) -> None: - """ - Delegate to super to register `cog`. - - This only serves to make the info log, so that extensions don't have to. - """ - super().add_cog(cog) - log.info(f"Cog loaded: {cog.qualified_name}") - - def add_command(self, command: commands.Command) -> None: - """Add `command` as normal and then add its root aliases to the bot.""" - super().add_command(command) - self._add_root_aliases(command) - - def remove_command(self, name: str) -> Optional[commands.Command]: - """ - Remove a command/alias as normal and then remove its root aliases from the bot. - - Individual root aliases cannot be removed by this function. - To remove them, either remove the entire command or manually edit `bot.all_commands`. - """ - command = super().remove_command(name) - if command is None: - # Even if it's a root alias, there's no way to get the Bot instance to remove the alias. - return - - self._remove_root_aliases(command) - return command - async def on_command_error(self, context: commands.Context, exception: DiscordException) -> None: """Check command errors for UserInputError and reset the cooldown if thrown.""" if isinstance(exception, commands.UserInputError): @@ -144,84 +76,14 @@ class Bot(commands.Bot): await devlog.send(embed=embed) - async def on_guild_available(self, guild: discord.Guild) -> None: - """ - Set the internal `_guild_available` event when PyDis guild becomes available. + async def setup_hook(self) -> None: + """Default async initialisation method for discord.py.""" + await super().setup_hook() - If the cache appears to still be empty (no members, no channels, or no roles), the event - will not be set. - """ - if guild.id != constants.Client.guild: - return - - if not guild.roles or not guild.members or not guild.channels: - log.warning("Guild available event was dispatched but the cache appears to still be empty!") - return + await self.check_channels() - self._guild_available.set() - - async def on_guild_unavailable(self, guild: discord.Guild) -> None: - """Clear the internal `_guild_available` event when PyDis guild becomes unavailable.""" - if guild.id != constants.Client.guild: - return + # This is not awaited to avoid a deadlock with any cogs that have + # wait_until_guild_available in their cog_load method. + scheduling.create_task(self.load_extensions(exts)) - self._guild_available.clear() - - async def wait_until_guild_available(self) -> None: - """ - Wait until the PyDis guild becomes available (and the cache is ready). - - The on_ready event is inadequate because it only waits 2 seconds for a GUILD_CREATE - gateway event before giving up and thus not populating the cache for unavailable guilds. - """ - await self._guild_available.wait() - - def _add_root_aliases(self, command: commands.Command) -> None: - """Recursively add root aliases for `command` and any of its subcommands.""" - if isinstance(command, commands.Group): - for subcommand in command.commands: - self._add_root_aliases(subcommand) - - for alias in getattr(command, "root_aliases", ()): - if alias in self.all_commands: - raise commands.CommandRegistrationError(alias, alias_conflict=True) - - self.all_commands[alias] = command - - def _remove_root_aliases(self, command: commands.Command) -> None: - """Recursively remove root aliases for `command` and any of its subcommands.""" - if isinstance(command, commands.Group): - for subcommand in command.commands: - self._remove_root_aliases(subcommand) - - for alias in getattr(command, "root_aliases", ()): - self.all_commands.pop(alias, None) - - -_allowed_roles = [discord.Object(id_) for id_ in constants.MODERATION_ROLES] - -_intents = discord.Intents.default() # Default is all intents except for privileged ones (Members, Presences, ...) -_intents.bans = False -_intents.integrations = False -_intents.invites = False -_intents.typing = False -_intents.webhooks = False - -redis_session = RedisSession( - address=(constants.RedisConfig.host, constants.RedisConfig.port), - password=constants.RedisConfig.password, - minsize=1, - maxsize=20, - use_fakeredis=constants.RedisConfig.use_fakeredis, - global_namespace="sir-lancebot" -) -loop = asyncio.get_event_loop() -loop.run_until_complete(redis_session.connect()) - -bot = Bot( - redis_session=redis_session, - command_prefix=when_mentioned_or(constants.Client.prefix), - activity=discord.Game(name=f"Commands: {constants.Client.prefix}help"), - allowed_mentions=discord.AllowedMentions(everyone=False, roles=_allowed_roles), - intents=_intents, -) + await self.send_log(self.name, "Connected!") -- cgit v1.2.3