Implemented /stage edit and /stage delete (#2)

This commit is contained in:
2025-04-22 22:36:43 +02:00
parent f2c81648fa
commit ec094f9a98
7 changed files with 183 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
from logging import Logger
from typing import Any
from typing import Any, override
from bson import ObjectId
from discord import Guild, User
@@ -43,6 +43,14 @@ class PycordBot(LibPycordBot):
if "cache" in self.config and self.config["cache"]["type"] is not None:
self.cache = create_cache_client(self.config, self.config["cache"]["type"])
@override
async def start(self, *args: Any, **kwargs: Any) -> None:
await super().start(*args, **kwargs)
@override
async def close(self, **kwargs) -> None:
await super().close(**kwargs)
async def find_user(self, user: int | User) -> PycordUser:
"""Find User by its ID or User object.
@@ -100,16 +108,11 @@ class PycordBot(LibPycordBot):
event_stage: PycordEventStage = await PycordEventStage.create(**kwargs, cache=self.cache)
await event.insert_stage(event_stage._id, kwargs["sequence"], cache=self.cache)
await event.insert_stage(self, event_stage._id, kwargs["sequence"], cache=self.cache)
return event_stage
async def start(self, *args: Any, **kwargs: Any) -> None:
await super().start(*args, **kwargs)
async def close(self, **kwargs) -> None:
await super().close(**kwargs)
# TODO Document this method
async def find_event(self, event_id: str | ObjectId | None = None, event_name: str | None = None):
if event_id is None and event_name is None:
raise AttributeError("Either event's ID or name must be provided!")
@@ -118,3 +121,7 @@ class PycordBot(LibPycordBot):
return await PycordEvent.from_id(event_id, cache=self.cache)
else:
return await PycordEvent.from_name(event_name, cache=self.cache)
# 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)

View File

@@ -5,10 +5,11 @@ from typing import Any, Dict, List, Optional
from zoneinfo import ZoneInfo
from bson import ObjectId
from discord import Bot
from libbot.cache.classes import Cache
from pymongo.results import InsertOneResult
from modules.database import col_events, col_stages
from modules.database import col_events
from modules.logging_utils import get_logger
logger: Logger = get_logger(__name__)
@@ -266,14 +267,62 @@ class PycordEvent:
await self.__collection__.delete_one({"_id": self._id})
self._delete_cache(cache)
# TODO Add documentation
async def cancel(self, cache: Optional[Cache] = None):
await self._set(cache, cancelled=True)
async def insert_stage(
self, event_stage_id: ObjectId, sequence: int, cache: Optional[Cache] = None
async def _update_event_stage_order(
self,
bot: Any,
old_stage_ids: List[ObjectId],
cache: Optional[Cache] = None,
) -> None:
self.stage_ids.insert(sequence, event_stage_id)
await self._set(cache, stage_ids=self.stage_ids)
logger.info("Updating event stages order for %s...", self._id)
# TODO Check if this works
await col_stages.update_many({"_id": {"$eq": self.stage_ids[sequence:]}}, {"$inc": {"sequence": 1}})
logger.debug("Old stage IDs: %s", old_stage_ids)
logger.debug("New stage IDs: %s", self.stage_ids)
for event_stage_id in self.stage_ids:
if event_stage_id not in old_stage_ids:
continue
stage_index: int = self.stage_ids.index(event_stage_id)
old_stage_index: int = old_stage_ids.index(event_stage_id)
logger.debug(
"Indexes for %s: was %s and is now %s", event_stage_id, old_stage_index, stage_index
)
if stage_index != old_stage_index:
await (await bot.find_event_stage(event_stage_id)).update(cache, sequence=stage_index)
# TODO Add documentation
async def insert_stage(
self, bot: Bot, event_stage_id: ObjectId, index: int, cache: Optional[Cache] = None
) -> None:
old_stage_ids: List[ObjectId] = self.stage_ids.copy()
self.stage_ids.insert(index, event_stage_id)
await self._set(cache, stage_ids=self.stage_ids)
await self._update_event_stage_order(bot, old_stage_ids, cache=cache)
# TODO Add documentation
async def reorder_stage(
self, bot: Any, event_stage_id: ObjectId, index: int, cache: Optional[Cache] = None
) -> None:
old_stage_ids: List[ObjectId] = self.stage_ids.copy()
self.stage_ids.insert(index, self.stage_ids.pop(self.stage_ids.index(event_stage_id)))
await self._set(cache, stage_ids=self.stage_ids)
await self._update_event_stage_order(bot, old_stage_ids, cache=cache)
# TODO Add documentation
async def remove_stage(self, bot: Bot, event_stage_id: ObjectId, cache: Optional[Cache] = None) -> None:
old_stage_ids: List[ObjectId] = self.stage_ids.copy()
self.stage_ids.pop(self.stage_ids.index(event_stage_id))
await self._set(cache, stage_ids=self.stage_ids)
await self._update_event_stage_order(bot, old_stage_ids, cache=cache)

View File

@@ -38,7 +38,7 @@ class PycordEventStage:
creator_id: int
question: str
answer: str
media: List[str]
media: List[int]
@classmethod
async def from_id(cls, stage_id: str | ObjectId, cache: Optional[Cache] = None) -> "PycordEventStage":

View File

@@ -12,7 +12,7 @@ from discord.utils import basic_autocomplete
from classes import PycordGuild
from classes.pycord_bot import PycordBot
from modules.utils import autofill_timezones, autofill_languages
from modules.utils import autocomplete_timezones, autocomplete_languages
class Config(Cog):
@@ -34,13 +34,13 @@ class Config(Cog):
@option(
"timezone",
description="Timezone in which events take place",
autocomplete=basic_autocomplete(autofill_timezones),
autocomplete=basic_autocomplete(autocomplete_timezones),
required=True,
)
@option(
"language",
description="Language for bot's messages",
autocomplete=basic_autocomplete(autofill_languages),
autocomplete=basic_autocomplete(autocomplete_languages),
required=True,
)
async def command_config_set(
@@ -97,6 +97,11 @@ class Config(Cog):
async def command_config_show(self, ctx: ApplicationContext) -> None:
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
# TODO Make a nice message
await ctx.respond(
f"**Guild config**\n\nChannel: <#{guild.channel_id}>\nCategory: <#{guild.category_id}>\nTimezone: {guild.timezone}\nLanguage: {guild.language}"

View File

@@ -15,7 +15,7 @@ from discord.utils import basic_autocomplete
from classes import PycordEvent, PycordGuild
from classes.pycord_bot import PycordBot
from modules.database import col_events
from modules.utils import autofill_active_events
from modules.utils import autocomplete_active_events
# TODO Move to staticmethod or to a separate module
@@ -120,7 +120,7 @@ class Event(Cog):
@option(
"event",
description="Name of the event",
autocomplete=basic_autocomplete(autofill_active_events),
autocomplete=basic_autocomplete(autocomplete_active_events),
required=True,
)
@option("name", description="New name of the event", required=False)
@@ -192,14 +192,21 @@ class Event(Cog):
@option(
"event",
description="Name of the event",
autocomplete=basic_autocomplete(autofill_active_events),
autocomplete=basic_autocomplete(autocomplete_active_events),
required=True,
)
@option("confirm", description="Confirmation of the operation", required=False)
async def command_event_cancel(
self,
ctx: ApplicationContext,
event: str,
confirm: bool = False,
) -> None:
if confirm is None or not confirm:
# TODO Make a nice message
await ctx.respond("Operation not confirmed.")
return
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
pycord_event: PycordEvent = await self.bot.find_event(event_id=event)
@@ -239,7 +246,7 @@ class Event(Cog):
@option(
"event",
description="Name of the event",
autocomplete=basic_autocomplete(autofill_active_events),
autocomplete=basic_autocomplete(autocomplete_active_events),
required=True,
)
async def command_event_show(self, ctx: ApplicationContext, event: str) -> None:

View File

@@ -4,7 +4,7 @@ from discord.utils import basic_autocomplete
from classes import PycordGuild, PycordEventStage, PycordEvent
from classes.pycord_bot import PycordBot
from modules.utils import autofill_active_events
from modules.utils import autocomplete_active_events, autocomplete_event_stages
class Stage(Cog):
@@ -24,7 +24,7 @@ class Stage(Cog):
@option(
"event",
description="Name of the event",
autocomplete=basic_autocomplete(autofill_active_events),
autocomplete=basic_autocomplete(autocomplete_active_events),
required=True,
)
@option("question", description="Question to be answered", required=True)
@@ -54,7 +54,7 @@ class Stage(Cog):
creator_id=ctx.author.id,
question=question,
answer=answer,
media=None if media is None else media.id,
media=[] if media is None else [media.id],
)
# TODO Make a nice message
@@ -69,14 +69,21 @@ class Stage(Cog):
@option(
"event",
description="Name of the event",
autocomplete=basic_autocomplete(autofill_active_events),
autocomplete=basic_autocomplete(autocomplete_active_events),
required=True,
)
@option("stage", description="Stage to edit", required=True)
@option("order", description="Number in the event stages' order", required=False)
# TODO Add autofill
@option(
"stage",
description="Stage to edit",
autocomplete=basic_autocomplete(autocomplete_event_stages),
required=True,
)
@option("order", description="Number in the event stages' order", min_value=1, required=False)
@option("question", description="Question to be answered", required=False)
@option("answer", description="Answer to the stage's question", required=False)
@option("media", description="Media file to be attached", required=False)
@option("remove_media", description="Remove attached media", required=False)
async def command_stage_edit(
self,
ctx: ApplicationContext,
@@ -86,8 +93,33 @@ class Stage(Cog):
question: str,
answer: str,
media: Attachment = None,
remove_media: bool = False,
) -> None:
await ctx.respond("Not implemented.")
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
pycord_event: PycordEvent = await self.bot.find_event(event)
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
if not guild.is_configured():
# TODO Make a nice message
await ctx.respond("Guild is not configured.")
return
if order is not None and order > len(pycord_event.stage_ids):
# TODO Make a nice message
await ctx.respond("Stage sequence out of range.")
return
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]),
)
if order is not None and order - 1 != event_stage.sequence:
await pycord_event.reorder_stage(self.bot, event_stage._id, order - 1, cache=self.bot.cache)
await ctx.respond("Event stage has been updated.")
# TODO Implement the command
# /stage delete <event> <stage> <confirm>
@@ -98,15 +130,38 @@ class Stage(Cog):
@option(
"event",
description="Name of the event",
autocomplete=basic_autocomplete(autofill_active_events),
autocomplete=basic_autocomplete(autocomplete_active_events),
required=True,
)
@option(
"stage",
description="Stage to delete",
autocomplete=basic_autocomplete(autocomplete_event_stages),
required=True,
)
@option("stage", description="Stage to edit", required=True)
@option("confirm", description="Confirmation of the operation", required=False)
async def command_stage_delete(
self, ctx: ApplicationContext, event: str, stage: str, confirm: bool = False
) -> None:
await ctx.respond("Not implemented.")
if confirm is None or not confirm:
# TODO Make a nice message
await ctx.respond("Operation not confirmed.")
return
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
pycord_event: PycordEvent = await self.bot.find_event(event)
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
if not guild.is_configured():
# TODO Make a nice message
await ctx.respond("Guild is not configured.")
return
await pycord_event.remove_stage(self.bot, event_stage._id, cache=self.bot.cache)
await event_stage.purge(cache=self.bot.cache)
# TODO Make a nice message
await ctx.respond("Okay.")
def setup(bot: PycordBot) -> None:

View File

@@ -2,9 +2,11 @@ from datetime import datetime
from typing import List, Dict, Any
from zoneinfo import ZoneInfo, available_timezones
from bson import ObjectId
from discord import AutocompleteContext, OptionChoice
from pymongo import ASCENDING
from modules.database import col_events
from modules.database import col_events, col_stages
def hex_to_int(hex_color: str) -> int:
@@ -16,19 +18,19 @@ def int_to_hex(integer_color: int) -> str:
# TODO Maybe move to a separate module
async def autofill_timezones(ctx: AutocompleteContext) -> List[str]:
async def autocomplete_timezones(ctx: AutocompleteContext) -> List[str]:
return sorted(list(available_timezones()))
# TODO Maybe move to a separate module
async def autofill_languages(ctx: AutocompleteContext) -> List[str]:
async def autocomplete_languages(ctx: AutocompleteContext) -> List[str]:
# TODO Discord normally uses a different set of locales.
# For example, "en" being "en-US", etc. This will require changes to locale handling later.
return ctx.bot.locales.keys()
# TODO Maybe move to a separate module
async def autofill_active_events(ctx: AutocompleteContext) -> List[OptionChoice]:
async def autocomplete_active_events(ctx: AutocompleteContext) -> List[OptionChoice]:
query: Dict[str, Any] = {
"ended": None,
"ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
@@ -41,3 +43,24 @@ async def autofill_active_events(ctx: AutocompleteContext) -> List[OptionChoice]
event_names.append(OptionChoice(result["name"], str(result["_id"])))
return event_names
# TODO Maybe move to a separate module
async def autocomplete_event_stages(ctx: AutocompleteContext) -> List[OptionChoice]:
event_id: str | None = ctx.options["event"]
if event_id is None:
return []
query: Dict[str, Any] = {
"event_id": ObjectId(event_id),
}
event_stages: List[OptionChoice] = []
async for result in col_stages.find(query).sort([("sequence", ASCENDING)]):
event_stages.append(
OptionChoice(f"{result['sequence']+1} ({result['question']})", str(result["_id"]))
)
return event_stages