aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/constants.py10
-rw-r--r--bot/exts/backend/sync/_cog.py21
-rw-r--r--bot/exts/backend/sync/_syncers.py2
-rw-r--r--bot/exts/help_channels/_cog.py14
-rw-r--r--tests/bot/exts/backend/sync/test_cog.py61
-rw-r--r--tests/bot/exts/backend/sync/test_users.py1
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)