From 80eae3f1b18d417824494568fc3972ad335293a0 Mon Sep 17 00:00:00 2001 From: profitroll Date: Fri, 2 May 2025 12:01:23 +0200 Subject: [PATCH] Closes #11 --- classes/pycord_bot.py | 45 ++++++++++++++- classes/pycord_user.py | 40 +++++++++++++- cogs/cog_register.py | 13 +---- cogs/cog_utility.py | 85 +++++++++++++++++++++++++++-- modules/utils/autocomplete_utils.py | 1 + 5 files changed, 165 insertions(+), 19 deletions(-) diff --git a/classes/pycord_bot.py b/classes/pycord_bot.py index 3c8e199..abe5e8c 100644 --- a/classes/pycord_bot.py +++ b/classes/pycord_bot.py @@ -18,6 +18,7 @@ from classes.errors import ( EventStageMissingSequenceError, EventStageNotFoundError, GuildNotFoundError, + DiscordGuildMemberNotFoundError, ) from modules.database import col_events, col_users from modules.utils import get_logger @@ -118,9 +119,25 @@ class PycordBot(LibPycordBot): await user._set(self.cache, current_event_id=event._id, current_stage_id=first_stage._id) # Create a channel for each participant - user_channel: TextChannel | None = await user.setup_event_channel( - self, guild, pycord_guild, event, cache=self.cache - ) + try: + user_channel: TextChannel | None = await user.setup_event_channel( + self, guild, pycord_guild, event, cache=self.cache + ) + except DiscordGuildMemberNotFoundError: + logger.error( + "Could not create and configure event channel for user %s in %s (event %s): user not found in the guild", + user.id, + guild.id, + event._id, + ) + + await self.notify_admins( + guild, + pycord_guild, + f"Event channel could not be created for user with ID `{user.id}` (<@{user.id}>) and event **{event.name}**: user was not found on the server.", + ) + + continue if user_channel is None: logger.error( @@ -374,3 +391,25 @@ class PycordBot(LibPycordBot): processed_attachments.append({"id": attachment.id, "filename": attachment.filename}) return processed_attachments + + # TODO Add documentation + async def send_stage_question( + self, + channel: TextChannel, + event: PycordEvent, + stage: Optional[PycordEventStage] = None, + use_first_stage: bool = False, + ) -> None: + stage: PycordEventStage = ( + stage + if not use_first_stage or stage is not None + else await self.find_event_stage(event.stage_ids[0]) + ) + + stage_files: List[File] | None = stage.get_media_files() + + question_chunks: List[str] = stage.get_question_chunked(2000) + question_chunks_length: int = len(question_chunks) + + for index, chunk in enumerate(question_chunks): + await channel.send(chunk, files=None if index != question_chunks_length - 1 else stage_files) diff --git a/classes/pycord_user.py b/classes/pycord_user.py index df7a303..c1a5218 100644 --- a/classes/pycord_user.py +++ b/classes/pycord_user.py @@ -275,9 +275,10 @@ class PycordUser: guild: Guild, pycord_guild: "PycordGuild", pycord_event: "PycordEvent", + ignore_exists: bool = False, cache: Optional[Cache] = None, ) -> TextChannel | None: - if str(pycord_event._id) in self.event_channels.keys(): + if not ignore_exists and str(pycord_event._id) in self.event_channels.keys(): return None discord_member: Member | None = guild.get_member(self.id) @@ -314,6 +315,43 @@ class PycordUser: return channel + # TODO Add documentation + async def fix_event_channel( + self, + bot: Bot, + guild: Guild, + pycord_guild: "PycordGuild", + pycord_event: "PycordEvent", + cache: Optional[Cache] = None, + ) -> TextChannel | None: + # Configure channel if not set + if str(pycord_event._id) not in self.event_channels.keys(): + return await self.setup_event_channel(bot, guild, pycord_guild, pycord_event, cache=cache) + + discord_member: Member | None = guild.get_member(self.id) + + if discord_member is None: + raise DiscordGuildMemberNotFoundError(self.id, guild.id) + + channel: TextChannel = guild.get_channel(self.event_channels[str(pycord_event._id)]) + + if channel is None: + return await self.setup_event_channel( + bot, guild, pycord_guild, pycord_event, ignore_exists=True, cache=cache + ) + + await channel.set_permissions( + discord_member, + overwrite=PermissionOverwrite( + view_channel=True, + send_messages=True, + use_application_commands=True, + ), + reason=f"Updated event channel of {self.id} for event {pycord_event._id}", + ) + + return channel + # TODO Add documentation async def lock_event_channel( self, diff --git a/cogs/cog_register.py b/cogs/cog_register.py index d116785..8fc0afd 100644 --- a/cogs/cog_register.py +++ b/cogs/cog_register.py @@ -1,11 +1,10 @@ from datetime import datetime from logging import Logger from pathlib import Path -from typing import List from zoneinfo import ZoneInfo from bson.errors import InvalidId -from discord import ApplicationContext, Cog, File, TextChannel, option, slash_command +from discord import ApplicationContext, Cog, TextChannel, option, slash_command, File from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale @@ -126,15 +125,7 @@ class CogRegister(Cog): await user.set_event_stage(first_stage._id, cache=self.bot.cache) - first_stage_files: List[File] | None = first_stage.get_media_files() - - question_chunks: List[str] = first_stage.get_question_chunked(2000) - question_chunks_length: int = len(question_chunks) - - for index, chunk in enumerate(question_chunks): - await user_channel.send( - chunk, files=None if index != question_chunks_length - 1 else first_stage_files - ) + await self.bot.send_stage_question(user_channel, pycord_event, first_stage) def setup(bot: PycordBot) -> None: diff --git a/cogs/cog_utility.py b/cogs/cog_utility.py index f31a428..64a5739 100644 --- a/cogs/cog_utility.py +++ b/cogs/cog_utility.py @@ -1,8 +1,17 @@ +from datetime import datetime from logging import Logger +from pathlib import Path +from typing import Any, Dict, List +from zoneinfo import ZoneInfo -from discord import Activity, ActivityType, Cog, Member +from bson import ObjectId +from bson.errors import InvalidId +from discord import Activity, ActivityType, Cog, Member, TextChannel, File +from classes import PycordEvent, PycordGuild, PycordUser +from classes.errors import GuildNotFoundError from classes.pycord_bot import PycordBot +from modules.database import col_users from modules.utils import get_logger logger: Logger = get_logger(__name__) @@ -53,10 +62,78 @@ class CogUtility(Cog): logger.info("Set activity type to %s with message %s", activity_type, activity_message) - # TODO Implement #11 - @Cog.listener() + @Cog.listener("on_member_join") async def on_member_join(self, member: Member) -> None: - pass + try: + guild: PycordGuild = await self.bot.find_guild(member.guild.id) + except (InvalidId, GuildNotFoundError) as exc: + logger.error( + "Could not process member join event for %s in %s due to: %s", + member.id, + member.guild.id, + exc, + ) + return + + user: PycordUser = await self.bot.find_user(member.id, member.guild.id) + events: List[PycordEvent] = [] + + pipeline: List[Dict[str, Any]] = [ + {"$match": {"id": user.id}}, + { + "$lookup": { + "from": "events", + "localField": "registered_event_ids", + "foreignField": "_id", + "as": "registered_events", + } + }, + { + "$match": { + "registered_events.ended": None, + "registered_events.ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))}, + "registered_events.starts": {"$lt": datetime.now(tz=ZoneInfo("UTC"))}, + "registered_events.is_cancelled": False, + } + }, + ] + + async for result in col_users.aggregate(pipeline): + for registered_event in result["registered_events"]: + events.append(PycordEvent(**registered_event)) + + for event in events: + if user.current_event_id is not None and user.current_event_id != event._id: + continue + + if user.current_event_id is None: + await user.set_event(event._id, cache=self.bot.cache) + + channel: TextChannel | None = await user.fix_event_channel( + self.bot, member.guild, guild, event, cache=self.bot.cache + ) + + if channel is None: + continue + + thumbnail: File | None = ( + None + if event.thumbnail is None + else File(Path(f"data/{event.thumbnail['id']}"), event.thumbnail["filename"]) + ) + + await channel.send( + self.bot._("register_already_started", "messages").format(event_name=event.name), + file=thumbnail, + ) + + stage_id: ObjectId = ( + event.stage_ids[0] if user.current_stage_id is None else user.current_stage_id + ) + + await user.set_event_stage(stage_id, cache=self.bot.cache) + + await self.bot.send_stage_question(channel, event, await self.bot.find_event_stage(stage_id)) def setup(bot: PycordBot) -> None: diff --git a/modules/utils/autocomplete_utils.py b/modules/utils/autocomplete_utils.py index 34bc2ed..30056ba 100644 --- a/modules/utils/autocomplete_utils.py +++ b/modules/utils/autocomplete_utils.py @@ -50,6 +50,7 @@ async def autocomplete_user_registered_events(ctx: AutocompleteContext) -> List[ """Return list of active events user is registered in""" pipeline: List[Dict[str, Any]] = [ + {"$match": {"id": ctx.interaction.user.id}}, { "$lookup": { "from": "events",