diff options
-rw-r--r-- | bot/constants.py | 10 | ||||
-rw-r--r-- | bot/exts/backend/sync/_cog.py | 21 | ||||
-rw-r--r-- | bot/exts/backend/sync/_syncers.py | 2 | ||||
-rw-r--r-- | bot/exts/help_channels/_cog.py | 14 | ||||
-rw-r--r-- | tests/bot/exts/backend/sync/test_cog.py | 61 | ||||
-rw-r--r-- | tests/bot/exts/backend/sync/test_users.py | 1 |
6 files changed, 67 insertions, 42 deletions
diff --git a/bot/constants.py b/bot/constants.py index 0250d0e31..a9e192c52 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -8,7 +8,7 @@ By default, the values defined in the classes are used, these can be overridden import os from enum import Enum -from pydantic import BaseModel +from pydantic import BaseModel, computed_field from pydantic_settings import BaseSettings @@ -322,7 +322,7 @@ class _DuckPond(EnvConfig, env_prefix="duck_pond_"): threshold: int = 7 - channel_blacklist: tuple[int, ...] = ( + default_channel_blacklist: tuple[int, ...] = ( Channels.announcements, Channels.python_news, Channels.python_events, @@ -336,6 +336,12 @@ class _DuckPond(EnvConfig, env_prefix="duck_pond_"): Channels.staff_info, ) + extra_channel_blacklist: tuple[int, ...] = tuple() + + @computed_field + @property + def channel_blacklist(self) -> tuple[int, ...]: + return self.default_channel_blacklist + self.extra_channel_blacklist DuckPond = _DuckPond() diff --git a/bot/exts/backend/sync/_cog.py b/bot/exts/backend/sync/_cog.py index 83ebb70d2..8ec01828b 100644 --- a/bot/exts/backend/sync/_cog.py +++ b/bot/exts/backend/sync/_cog.py @@ -1,10 +1,11 @@ import asyncio from typing import Any -from discord import Member, Role, User +from discord import Guild, Member, Role, User from discord.ext import commands from discord.ext.commands import Cog, Context from pydis_core.site_api import ResponseCodeError +from pydis_core.utils.scheduling import create_task from bot import constants from bot.bot import Bot @@ -20,33 +21,39 @@ class Sync(Cog): def __init__(self, bot: Bot) -> None: self.bot = bot + self.guild: Guild | None = None + async def cog_load(self) -> None: """Syncs the roles/users of the guild with the database.""" await self.bot.wait_until_guild_available() - guild = self.bot.get_guild(constants.Guild.id) - if guild is None: - return + self.guild = self.bot.get_guild(constants.Guild.id) + if self.guild is None: + raise ValueError("Could not fetch guild from cache, not loading sync cog.") attempts = 0 while True: attempts += 1 - if guild.chunked: + if self.guild.chunked: log.info("Guild was found to be chunked after %d attempt(s).", attempts) break if attempts == MAX_ATTEMPTS: log.info("Guild not chunked after %d attempts, calling chunk manually.", MAX_ATTEMPTS) - await guild.chunk() + await self.guild.chunk() break log.info("Attempt %d/%d: Guild not yet chunked, checking again in 10s.", attempts, MAX_ATTEMPTS) await asyncio.sleep(10) + create_task(self.sync()) + + async def sync(self) -> None: + await asyncio.sleep(10) # Give time to other cogs starting up log.info("Starting syncers.") for syncer in (_syncers.RoleSyncer, _syncers.UserSyncer): - await syncer.sync(guild) + await syncer.sync(self.guild) async def patch_user(self, user_id: int, json: dict[str, Any], ignore_404: bool = False) -> None: """Send a PATCH request to partially update a user in the database.""" diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index cd7f5040d..e1ed6dff4 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -170,6 +170,7 @@ class UserSyncer(Syncer): seen_guild_users.add(guild_user.id) maybe_update("name", guild_user.name) + maybe_update("display_name", guild_user.display_name) maybe_update("discriminator", int(guild_user.discriminator)) maybe_update("in_guild", True) @@ -196,6 +197,7 @@ class UserSyncer(Syncer): new_user = { "id": member.id, "name": member.name, + "display_name": member.display_name, "discriminator": int(member.discriminator), "roles": [role.id for role in member.roles], "in_guild": True diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 59a95ab6a..96c0a89ba 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -1,7 +1,7 @@ """Contains the Cog that receives discord.py events and defers most actions to other files in the module.""" import discord -from discord.ext import commands +from discord.ext import commands, tasks from pydis_core.utils import scheduling from bot import constants @@ -38,18 +38,14 @@ class HelpForum(commands.Cog): self.help_forum_channel = self.bot.get_channel(constants.Channels.python_help) if not isinstance(self.help_forum_channel, discord.ForumChannel): raise TypeError("Channels.python_help is not a forum channel!") - await self.check_all_open_posts_have_close_task() + self.check_all_open_posts_have_close_task.start() - async def check_all_open_posts_have_close_task(self, delay: int = 5*60) -> None: - """ - Check that each open help post has a scheduled task to close, adding one if not. - - Once complete, schedule another check after `delay` seconds. - """ + @tasks.loop(minutes=5) + async def check_all_open_posts_have_close_task(self) -> None: + """Check that each open help post has a scheduled task to close, adding one if not.""" for post in self.help_forum_channel.threads: if post.id not in self.scheduler: await _channel.maybe_archive_idle_post(post, self.scheduler) - self.scheduler.schedule_later(delay, "help_channel_idle_check", self.check_all_open_posts_have_close_task()) async def close_check(self, ctx: commands.Context) -> bool: """Return True if the channel is a help post, and the user is the claimant or has a whitelisted role.""" diff --git a/tests/bot/exts/backend/sync/test_cog.py b/tests/bot/exts/backend/sync/test_cog.py index 2ce950965..bf117b478 100644 --- a/tests/bot/exts/backend/sync/test_cog.py +++ b/tests/bot/exts/backend/sync/test_cog.py @@ -1,4 +1,5 @@ import unittest +import unittest.mock from unittest import mock import discord @@ -60,40 +61,52 @@ class SyncCogTestCase(unittest.IsolatedAsyncioTestCase): class SyncCogTests(SyncCogTestCase): """Tests for the Sync cog.""" - async def test_sync_cog_sync_on_load(self): - """Roles and users should be synced on cog load.""" - guild = helpers.MockGuild() - self.bot.get_guild = mock.MagicMock(return_value=guild) - - self.RoleSyncer.reset_mock() - self.UserSyncer.reset_mock() - - await self.cog.cog_load() - - self.RoleSyncer.sync.assert_called_once_with(guild) - self.UserSyncer.sync.assert_called_once_with(guild) - - async def test_sync_cog_sync_guild(self): - """Roles and users should be synced only if a guild is successfully retrieved.""" + @unittest.mock.patch("bot.exts.backend.sync._cog.create_task", new_callable=unittest.mock.MagicMock) + async def test_sync_cog_sync_on_load(self, mock_create_task: unittest.mock.MagicMock): + """Sync function should be synced on cog load only if guild is found.""" for guild in (helpers.MockGuild(), None): with self.subTest(guild=guild): + mock_create_task.reset_mock() self.bot.reset_mock() self.RoleSyncer.reset_mock() self.UserSyncer.reset_mock() self.bot.get_guild = mock.MagicMock(return_value=guild) - - await self.cog.cog_load() - - self.bot.wait_until_guild_available.assert_called_once() - self.bot.get_guild.assert_called_once_with(constants.Guild.id) + error_raised = False + try: + await self.cog.cog_load() + except ValueError: + if guild is None: + error_raised = True + else: + raise if guild is None: - self.RoleSyncer.sync.assert_not_called() - self.UserSyncer.sync.assert_not_called() + self.assertTrue(error_raised) + mock_create_task.assert_not_called() else: - self.RoleSyncer.sync.assert_called_once_with(guild) - self.UserSyncer.sync.assert_called_once_with(guild) + mock_create_task.assert_called_once() + self.assertIsInstance(mock_create_task.call_args[0][0], type(self.cog.sync())) + + + async def test_sync_cog_sync_guild(self): + """Roles and users should be synced only if a guild is successfully retrieved.""" + guild = helpers.MockGuild() + self.bot.reset_mock() + self.RoleSyncer.reset_mock() + self.UserSyncer.reset_mock() + + self.bot.get_guild = mock.MagicMock(return_value=guild) + await self.cog.cog_load() + + with mock.patch("asyncio.sleep", new_callable=unittest.mock.AsyncMock): + await self.cog.sync() + + self.bot.wait_until_guild_available.assert_called_once() + self.bot.get_guild.assert_called_once_with(constants.Guild.id) + + self.RoleSyncer.sync.assert_called_once() + self.UserSyncer.sync.assert_called_once() async def patch_user_helper(self, side_effect: BaseException) -> None: """Helper to set a side effect for bot.api_client.patch and then assert it is called.""" diff --git a/tests/bot/exts/backend/sync/test_users.py b/tests/bot/exts/backend/sync/test_users.py index 2fc97af2d..2fc000446 100644 --- a/tests/bot/exts/backend/sync/test_users.py +++ b/tests/bot/exts/backend/sync/test_users.py @@ -11,6 +11,7 @@ def fake_user(**kwargs): """Fixture to return a dictionary representing a user with default values set.""" kwargs.setdefault("id", 43) kwargs.setdefault("name", "bob the test man") + kwargs.setdefault("display_name", "bob") kwargs.setdefault("discriminator", 1337) kwargs.setdefault("roles", [helpers.MockRole(id=666)]) kwargs.setdefault("in_guild", True) |