Implemented #7

This commit is contained in:
2025-04-25 11:37:44 +02:00
parent d9f563285c
commit fb16fa2bc8
9 changed files with 175 additions and 58 deletions

View File

@@ -1,10 +1,11 @@
from datetime import datetime
from logging import Logger
from pathlib import Path
from typing import Any, override, List, Dict
from zoneinfo import ZoneInfo
from bson import ObjectId
from discord import Guild, User, TextChannel
from discord import Guild, User, TextChannel, Attachment, File
from libbot.cache.classes import CacheMemcached, CacheRedis
from libbot.cache.manager import create_cache_client
from libbot.pycord.classes import PycordBot as LibPycordBot
@@ -59,7 +60,6 @@ class PycordBot(LibPycordBot):
async def _execute_event_controller(self) -> None:
await self._process_events_start()
await self._process_events_end()
# await self._process_events_post_end()
async def _process_events_start(self) -> None:
# Get events to start
@@ -69,22 +69,45 @@ class PycordBot(LibPycordBot):
# Process each event
for event in events:
if len(event.stage_ids) == 0:
# TODO Make a nice message for management
logger.error("Could not start the event %s: no event stages are defined.", event._id)
continue
guild: Guild = self.get_guild(event.guild_id)
pycord_guild: PycordGuild = await self.find_guild(guild)
first_stage: PycordEventStage = await self.find_event_stage(event.stage_ids[0])
# Get list of participants
users: List[PycordUser] = await self._get_event_participants(event._id)
for user in users:
await user._set(self.cache, current_event_id=event._id, current_stage_id=first_stage._id)
# Create a channel for each participant
await user.setup_event_channel(self, guild, pycord_guild, event, cache=self.cache)
# Send a notification about event start
user_channel: TextChannel = guild.get_channel(user.event_channels[str(event._id)])
thumbnail: File | None = (
None
if event.thumbnail is None
else File(Path(f"data/{event.thumbnail['id']}"), event.thumbnail["filename"])
)
# TODO Make a nice message
# TODO Also send a thumbnail, event info and short explanation on how to play
await user_channel.send(f"Event **{event.name}** is starting!")
await user_channel.send(
f"Event **{event.name}** is starting!\n\nUse slash command `/guess` to suggest your answers to each event stage.",
file=thumbnail,
)
first_stage_files: List[File] | None = first_stage.get_media_files()
await user_channel.send(
f"First stage...\n\n{first_stage.question}", files=first_stage_files
)
# TODO Make a nice message
await self._notify_admins(
@@ -103,6 +126,13 @@ class PycordBot(LibPycordBot):
for event in events:
guild: Guild = self.get_guild(event.guild_id)
pycord_guild: PycordGuild = await self.find_guild(guild)
stages: List[PycordEventStage] = await self._get_event_stages(event)
# TODO Make a nice message
stages_string: str = "\n\n".join(
f"**Stage {stage.sequence+1}**\nQuestion: {stage.question}\nAnswer: ||{stage.answer}||"
for stage in stages
)
# Get list of participants
users: List[PycordUser] = await self._get_event_participants(event._id)
@@ -112,8 +142,9 @@ class PycordBot(LibPycordBot):
user_channel: TextChannel = guild.get_channel(user.event_channels[str(event._id)])
# TODO Make a nice message
# TODO Reveal answers to stages
await user_channel.send(f"Event **{event.name}** has ended!")
await user_channel.send(
f"Event **{event.name}** has ended! Stages and respective answers are listed below.\n\n{stages_string}"
)
# Lock each participant out
await user.lock_event_channel(guild, event._id, channel=user_channel)
@@ -125,37 +156,6 @@ class PycordBot(LibPycordBot):
f"Event **{event.name}** has ended! Users can no longer submit their answers.",
)
# async def _process_events_post_end(self) -> None:
# # Get events that ended an hour ago
# # TODO Replace with 1 hour after testing!
# events: List[PycordEvent] = await self._get_events(
# {
# "ends": datetime.now(tz=ZoneInfo("UTC")).replace(second=0, microsecond=0)
# - timedelta(minutes=1)
# }
# )
#
# # Process each event
# for event in events:
# guild: Guild = self.get_guild(event.guild_id)
# pycord_guild: PycordGuild = await self.find_guild(guild)
#
# # Get list of participants
# users: List[PycordUser] = await self._get_event_participants(event._id)
#
# for user in users:
# # Send a notification about event start
# user_channel: TextChannel = guild.get_channel(user.event_channels[str(event._id)])
#
# # Remove their view permissions
# await user.lock_event_channel(guild, event._id, completely=True, channel=user_channel)
#
# await self._notify_admins(
# guild,
# pycord_guild,
# f"Access has been updated, users can no longer access their channels for the event **{event.name}**.",
# )
@staticmethod
async def _get_events(query: Dict[str, Any]) -> List[PycordEvent]:
events: List[PycordEvent] = []
@@ -174,6 +174,9 @@ class PycordBot(LibPycordBot):
return users
async def _get_event_stages(self, event: PycordEvent) -> List[PycordEventStage]:
return [(await self.find_event_stage(stage_id)) for stage_id in event.stage_ids]
@staticmethod
async def _notify_admins(guild: Guild, pycord_guild: PycordGuild, message: str) -> None:
management_channel: TextChannel | None = guild.get_channel(pycord_guild.channel_id)
@@ -264,3 +267,13 @@ class PycordBot(LibPycordBot):
# TODO Document this method
async def find_event_stage(self, stage_id: str | ObjectId) -> PycordEventStage:
return await PycordEventStage.from_id(stage_id, cache=self.cache)
@staticmethod
async def process_attachments(attachments: List[Attachment]) -> List[Dict[str, Any]]:
processed_attachments: List[Dict[str, Any]] = []
for attachment in attachments:
await attachment.save(Path(f"data/{attachment.id}"))
processed_attachments.append({"id": attachment.id, "filename": attachment.filename})
return processed_attachments

View File

@@ -27,7 +27,7 @@ class PycordEvent:
"creator_id",
"starts",
"ends",
"thumbnail_id",
"thumbnail",
"stage_ids",
)
__short_name__ = "event"
@@ -42,7 +42,7 @@ class PycordEvent:
creator_id: int
starts: datetime
ends: datetime
thumbnail_id: str | None
thumbnail: Dict[str, Any] | None
stage_ids: List[ObjectId]
@classmethod
@@ -105,7 +105,7 @@ class PycordEvent:
creator_id: int,
starts: datetime,
ends: datetime,
thumbnail_id: str | None,
thumbnail: Dict[str, Any] | None,
cache: Optional[Cache] = None,
) -> "PycordEvent":
db_entry: Dict[str, Any] = {
@@ -117,7 +117,7 @@ class PycordEvent:
"creator_id": creator_id,
"starts": starts,
"ends": ends,
"thumbnail_id": thumbnail_id,
"thumbnail": thumbnail,
"stage_ids": [],
}
@@ -213,7 +213,7 @@ class PycordEvent:
"creator_id": self.creator_id,
"starts": self.starts,
"ends": self.ends,
"thumbnail_id": self.thumbnail_id,
"thumbnail": self.thumbnail,
"stage_ids": self.stage_ids,
}
@@ -228,7 +228,7 @@ class PycordEvent:
"creator_id": None,
"starts": None,
"ends": None,
"thumbnail_id": None,
"thumbnail": None,
"stage_ids": [],
}

View File

@@ -1,10 +1,12 @@
from dataclasses import dataclass
from datetime import datetime
from logging import Logger
from pathlib import Path
from typing import Any, Dict, List, Optional
from zoneinfo import ZoneInfo
from bson import ObjectId
from discord import File
from libbot.cache.classes import Cache
from pymongo.results import InsertOneResult
@@ -38,7 +40,7 @@ class PycordEventStage:
creator_id: int
question: str
answer: str
media: List[int]
media: List[Dict[str, Any]]
@classmethod
async def from_id(cls, stage_id: str | ObjectId, cache: Optional[Cache] = None) -> "PycordEventStage":
@@ -235,3 +237,11 @@ class PycordEventStage:
"""
await self.__collection__.delete_one({"_id": self._id})
self._delete_cache(cache)
# TODO Add documentation
def get_media_files(self) -> List[File] | None:
return (
None
if len(self.media) == 0
else [File(Path(f"data/{media['id']}"), media["filename"]) for media in self.media]
)

View File

@@ -361,3 +361,9 @@ class PycordUser:
self.event_channels[event_id if isinstance(event_id, str) else str(event_id)] = channel_id
await self._set(cache, event_channels=self.event_channels)
# TODO Add documentation
async def set_event_stage(self, stage_id: str | ObjectId | None, cache: Optional[Cache] = None) -> None:
await self._set(
cache, current_stage_id=stage_id if isinstance(stage_id, str) else ObjectId(stage_id)
)

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Dict, List, Any
from zoneinfo import ZoneInfo
from bson.errors import InvalidId
@@ -64,13 +65,17 @@ class Event(Cog):
await validate_event_validity(ctx, name, start_date, end_date, guild_timezone)
processed_media: List[Dict[str, Any]] = (
[] if thumbnail is None else await self.bot.process_attachments([thumbnail])
)
event: PycordEvent = await self.bot.create_event(
name=name,
guild_id=guild.id,
creator_id=ctx.author.id,
starts=start_date.astimezone(ZoneInfo("UTC")),
ends=end_date.astimezone(ZoneInfo("UTC")),
thumbnail_id=thumbnail.id if thumbnail else None,
thumbnail=processed_media[0] if thumbnail else None,
)
# TODO Make a nice message
@@ -138,12 +143,16 @@ class Event(Cog):
await validate_event_validity(ctx, name, start_date, end_date, guild_timezone)
processed_media: List[Dict[str, Any]] = (
[] if thumbnail is None else await self.bot.process_attachments([thumbnail])
)
await pycord_event.update(
self.bot.cache,
starts=start_date,
ends=end_date,
name=pycord_event.name if name is None else name,
thumbnail_id=pycord_event.thumbnail_id if thumbnail is None else thumbnail.id,
thumbnail=pycord_event.thumbnail if thumbnail is None else processed_media[0],
)
# TODO Make a nice message

View File

@@ -1,5 +1,10 @@
from discord import ApplicationContext, Cog, option, slash_command
from typing import List
from bson import ObjectId
from bson.errors import InvalidId
from discord import ApplicationContext, Cog, option, slash_command, File
from classes import PycordEvent, PycordUser, PycordGuild, PycordEventStage
from classes.pycord_bot import PycordBot
@@ -16,7 +21,64 @@ class Guess(Cog):
)
@option("answer", description="An answer to the current stage")
async def command_guess(self, ctx: ApplicationContext, answer: str) -> None:
await ctx.respond("Not implemented.")
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
if not guild.is_configured():
# TODO Make a nice message
await ctx.respond("Guild is not configured.")
return
user: PycordUser = await self.bot.find_user(ctx.author)
if user.current_event_id is None or user.current_stage_id is None:
# TODO Make a nice message
await ctx.respond("You have no ongoing events.")
return
try:
event: PycordEvent = await self.bot.find_event(event_id=user.current_event_id)
stage: PycordEventStage = await self.bot.find_event_stage(user.current_stage_id)
except (InvalidId, RuntimeError):
# TODO Make a nice message
await ctx.respond(
"Your event could not be found. Please, report this issue to the event's management."
)
return
if answer.lower() != stage.answer.lower():
# TODO Make a nice message
await ctx.respond("Provided answer is wrong.")
return
next_stage_index = stage.sequence + 1
next_stage_id: ObjectId | None = (
None if len(event.stage_ids) < next_stage_index + 1 else event.stage_ids[next_stage_index]
)
if next_stage_id is None:
# TODO Make a nice message
user.completed_event_ids.append(event._id)
await user._set(
self.bot.cache,
current_event_id=None,
current_stage_id=None,
completed_event_ids=user.completed_event_ids,
)
await ctx.respond("Event is completed.")
return
next_stage: PycordEventStage = await self.bot.find_event_stage(next_stage_id)
files: List[File] | None = next_stage.get_media_files()
await ctx.respond(
f"Provided answer is correct! Next stage...\n\n{next_stage.question}",
files=files,
)
await user.set_event_stage(next_stage._id, cache=self.bot.cache)
def setup(bot: PycordBot) -> None:

View File

@@ -1,3 +1,5 @@
from zoneinfo import ZoneInfo
from bson.errors import InvalidId
from discord import ApplicationContext, Cog, option, slash_command
from discord.utils import basic_autocomplete
@@ -47,9 +49,10 @@ class Register(Cog):
await user.event_register(pycord_event._id, cache=self.bot.cache)
# TODO Text channel must be created and updated
await ctx.respond("Ok.")
# TODO Make a nice message
await ctx.respond(
f"You are now registered for the event **{pycord_event.name}**.\n\nNew channel will be created for you and further instructions will be provided as soon as the event starts <t:{int((pycord_event.starts.replace(tzinfo=ZoneInfo('UTC'))).timestamp())}:R>. Good luck!"
)
def setup(bot: PycordBot) -> None:

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from typing import List, Dict, Any
from zoneinfo import ZoneInfo
from bson.errors import InvalidId
@@ -14,11 +15,11 @@ from modules.utils import autocomplete_active_events, autocomplete_event_stages
async def validate_event_status(
ctx: ApplicationContext,
event: PycordEvent,
) -> None:
) -> bool:
if event.is_cancelled:
# TODO Make a nice message
await ctx.respond("This event was cancelled.")
return
return False
if (
event.starts.replace(tzinfo=ZoneInfo("UTC"))
@@ -27,7 +28,9 @@ async def validate_event_status(
):
# TODO Make a nice message
await ctx.respond("Ongoing events cannot be modified.")
return
return False
return True
class Stage(Cog):
@@ -75,7 +78,12 @@ class Stage(Cog):
await ctx.respond("Event was not found.")
return
await validate_event_status(ctx, pycord_event)
if not (await validate_event_status(ctx, pycord_event)):
return
processed_media: List[Dict[str, Any]] = (
[] if media is None else await self.bot.process_attachments([media])
)
event_stage: PycordEventStage = await self.bot.create_event_stage(
event=pycord_event,
@@ -85,7 +93,7 @@ class Stage(Cog):
creator_id=ctx.author.id,
question=question,
answer=answer,
media=[] if media is None else [media.id],
media=[] if media is None else processed_media,
)
# TODO Make a nice message
@@ -140,7 +148,8 @@ class Stage(Cog):
await ctx.respond("Event was not found.")
return
await validate_event_status(ctx, pycord_event)
if not (await validate_event_status(ctx, pycord_event)):
return
try:
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
@@ -154,11 +163,15 @@ class Stage(Cog):
await ctx.respond("Stage sequence out of range.")
return
processed_media: List[Dict[str, Any]] = (
[] if media is None else await self.bot.process_attachments([media])
)
if not (question is None and answer is None and media is None and remove_media is False):
await event_stage.update(
question=event_stage.question if question is None else question,
answer=event_stage.answer if answer is None else answer,
media=[] if remove_media else (event_stage.media if media is None else [media.id]),
media=([] if remove_media else (event_stage.media if media is None else processed_media)),
)
if order is not None and order - 1 != event_stage.sequence:
@@ -207,7 +220,8 @@ class Stage(Cog):
await ctx.respond("Event was not found.")
return
await validate_event_status(ctx, pycord_event)
if not (await validate_event_status(ctx, pycord_event)):
return
try:
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)

0
data/.gitkeep Normal file
View File