Compare commits

3 Commits

16 changed files with 131 additions and 81 deletions

View File

@@ -7,8 +7,8 @@ from libbot.cache.classes import CacheMemcached, CacheRedis
from libbot.cache.manager import create_cache_client
from libbot.pycord.classes import PycordBot as LibPycordBot
from classes import PycordEvent, PycordGuild, PycordUser, PycordEventStage
from modules.logging_utils import get_logger
from classes import PycordEvent, PycordEventStage, PycordGuild, PycordUser
from modules.utils import get_logger
logger: Logger = get_logger(__name__)

View File

@@ -10,7 +10,7 @@ from libbot.cache.classes import Cache
from pymongo.results import InsertOneResult
from modules.database import col_events
from modules.logging_utils import get_logger
from modules.utils import get_logger, restore_from_cache
logger: Logger = get_logger(__name__)
@@ -59,11 +59,10 @@ class PycordEvent:
Raises:
EventNotFoundError: Event was not found
"""
if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"{cls.__short_name__}_{event_id}")
cached_entry: Dict[str, Any] | None = restore_from_cache(cls.__short_name__, event_id, cache=cache)
if cached_entry is not None:
return cls(**cached_entry)
if cached_entry is not None:
return cls(**cached_entry)
db_entry = await cls.__collection__.find_one(
{"_id": event_id if isinstance(event_id, ObjectId) else ObjectId(event_id)}
@@ -130,14 +129,12 @@ class PycordEvent:
return cls(**db_entry)
# TODO Update the docstring
async def _set(self, cache: Optional[Cache] = None, **kwargs) -> None:
"""Set attribute data and save it into the database.
Args:
key (str): Attribute to change
value (Any): Value to set
cache (:obj:`Cache`, optional): Cache engine to write the update into
**kwargs (str): Mapping of attribute names and respective values to be set
"""
for key, value in kwargs.items():
if not hasattr(self, key):
@@ -151,13 +148,12 @@ class PycordEvent:
logger.info("Set attributes of event %s to %s", self._id, kwargs)
# TODO Update the docstring
async def _remove(self, cache: Optional[Cache] = None, *args: str) -> None:
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
"""Remove attribute data and save it into the database.
Args:
key (str): Attribute to remove
cache (:obj:`Cache`, optional): Cache engine to write the update into
*args (str): List of attributes to remove
"""
attributes: Dict[str, Any] = {}
@@ -326,3 +322,11 @@ class PycordEvent:
await self._set(cache, stage_ids=self.stage_ids)
await self._update_event_stage_order(bot, old_stage_ids, cache=cache)
# # TODO Add documentation
# def get_localized_start_date(self, tz: str | timezone | ZoneInfo) -> datetime:
# return self.starts.replace(tzinfo=tz)
#
# # TODO Add documentation
# def get_localized_end_date(self, tz: str | timezone | ZoneInfo) -> datetime:
# return self.ends.replace(tzinfo=tz)

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass
from datetime import datetime
from logging import Logger
from typing import List, Dict, Any, Optional
from typing import Any, Dict, List, Optional
from zoneinfo import ZoneInfo
from bson import ObjectId
@@ -9,7 +9,7 @@ from libbot.cache.classes import Cache
from pymongo.results import InsertOneResult
from modules.database import col_stages
from modules.logging_utils import get_logger
from modules.utils import get_logger, restore_from_cache
logger: Logger = get_logger(__name__)
@@ -54,11 +54,10 @@ class PycordEventStage:
Raises:
EventStageNotFoundError: Event stage was not found
"""
if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"{cls.__short_name__}_{stage_id}")
cached_entry: Dict[str, Any] | None = restore_from_cache(cls.__short_name__, stage_id, cache=cache)
if cached_entry is not None:
return cls(**cached_entry)
if cached_entry is not None:
return cls(**cached_entry)
db_entry = await cls.__collection__.find_one(
{"_id": stage_id if isinstance(stage_id, ObjectId) else ObjectId(stage_id)}
@@ -107,14 +106,12 @@ class PycordEventStage:
return cls(**db_entry)
# TODO Update the docstring
async def _set(self, cache: Optional[Cache] = None, **kwargs) -> None:
"""Set attribute data and save it into the database.
Args:
key (str): Attribute to change
value (Any): Value to set
cache (:obj:`Cache`, optional): Cache engine to write the update into
**kwargs (str): Mapping of attribute names and respective values to be set
"""
for key, value in kwargs.items():
if not hasattr(self, key):
@@ -128,13 +125,12 @@ class PycordEventStage:
logger.info("Set attributes of event stage %s to %s", self._id, kwargs)
# TODO Update the docstring
async def _remove(self, cache: Optional[Cache] = None, *args: str) -> None:
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
"""Remove attribute data and save it into the database.
Args:
key (str): Attribute to remove
cache (:obj:`Cache`, optional): Cache engine to write the update into
*args (str): List of attributes to remove
"""
attributes: Dict[str, Any] = {}

View File

@@ -8,7 +8,7 @@ from pymongo.results import InsertOneResult
from classes.errors import GuildNotFoundError
from modules.database import col_guilds
from modules.logging_utils import get_logger
from modules.utils import get_logger, restore_from_cache
logger: Logger = get_logger(__name__)
@@ -45,11 +45,10 @@ class PycordGuild:
Raises:
GuildNotFoundError: User was not found and creation was not allowed
"""
if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"{cls.__short_name__}_{guild_id}")
cached_entry: Dict[str, Any] | None = restore_from_cache(cls.__short_name__, guild_id, cache=cache)
if cached_entry is not None:
return cls(**cached_entry)
if cached_entry is not None:
return cls(**cached_entry)
db_entry = await cls.__collection__.find_one({"id": guild_id})
@@ -68,14 +67,12 @@ class PycordGuild:
return cls(**db_entry)
# TODO Update the docstring
async def _set(self, cache: Optional[Cache] = None, **kwargs) -> None:
"""Set attribute data and save it into the database.
Args:
key (str): Attribute to change
value (Any): Value to set
cache (:obj:`Cache`, optional): Cache engine to write the update into
**kwargs (str): Mapping of attribute names and respective values to be set
"""
for key, value in kwargs.items():
if not hasattr(self, key):
@@ -89,13 +86,12 @@ class PycordGuild:
logger.info("Set attributes of guild %s to %s", self.id, kwargs)
# TODO Update the docstring
async def _remove(self, cache: Optional[Cache] = None, *args: str) -> None:
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
"""Remove attribute data and save it into the database.
Args:
key (str): Attribute to remove
cache (:obj:`Cache`, optional): Cache engine to write the update into
*args (str): List of attributes to remove
"""
attributes: Dict[str, Any] = {}

View File

@@ -8,7 +8,7 @@ from pymongo.results import InsertOneResult
from classes.errors.pycord_user import UserNotFoundError
from modules.database import col_users
from modules.logging_utils import get_logger
from modules.utils import get_logger, restore_from_cache
logger: Logger = get_logger(__name__)
@@ -41,11 +41,10 @@ class PycordUser:
Raises:
UserNotFoundError: User was not found and creation was not allowed
"""
if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"{cls.__short_name__}_{user_id}")
cached_entry: Dict[str, Any] | None = restore_from_cache(cls.__short_name__, user_id, cache=cache)
if cached_entry is not None:
return cls(**cached_entry)
if cached_entry is not None:
return cls(**cached_entry)
db_entry = await cls.__collection__.find_one({"id": user_id})
@@ -78,44 +77,49 @@ class PycordUser:
"id": self.id,
}
async def _set(self, key: str, value: Any, cache: Optional[Cache] = None) -> None:
async def _set(self, cache: Optional[Cache] = None, **kwargs) -> None:
"""Set attribute data and save it into the database.
Args:
key (str): Attribute to change
value (Any): Value to set
cache (:obj:`Cache`, optional): Cache engine to write the update into
**kwargs (str): Mapping of attribute names and respective values to be set
"""
if not hasattr(self, key):
raise AttributeError()
for key, value in kwargs.items():
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, value)
setattr(self, key, value)
await self.__collection__.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True)
await self.__collection__.update_one({"_id": self._id}, {"$set": kwargs}, upsert=True)
self._update_cache(cache)
logger.info("Set attribute '%s' of user %s to '%s'", key, self.id, value)
logger.info("Set attributes of user %s to %s", self.id, kwargs)
async def _remove(self, key: str, cache: Optional[Cache] = None) -> None:
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
"""Remove attribute data and save it into the database.
Args:
key (str): Attribute to remove
cache (:obj:`Cache`, optional): Cache engine to write the update into
*args (str): List of attributes to remove
"""
if not hasattr(self, key):
raise AttributeError()
attributes: Dict[str, Any] = {}
default_value: Any = PycordUser.get_default_value(key)
for key in args:
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, default_value)
default_value: Any = self.get_default_value(key)
await self.__collection__.update_one({"_id": self._id}, {"$set": {key: default_value}}, upsert=True)
setattr(self, key, default_value)
attributes[key] = default_value
await self.__collection__.update_one({"_id": self._id}, {"$set": attributes}, upsert=True)
self._update_cache(cache)
logger.info("Removed attribute '%s' of user %s", key, self.id)
logger.info("Reset attributes %s of user %s to default values", args, self.id)
def _get_cache_key(self) -> str:
return f"{self.__short_name__}_{self.id}"

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 autocomplete_timezones, autocomplete_languages
from modules.utils import autocomplete_languages, autocomplete_timezones
class Config(Cog):

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Dict, Any
from typing import Any, Dict
from zoneinfo import ZoneInfo
from bson import ObjectId

View File

@@ -1,4 +1,4 @@
from discord import Cog, slash_command, option, ApplicationContext
from discord import ApplicationContext, Cog, option, slash_command
from classes.pycord_bot import PycordBot

View File

@@ -1,12 +1,30 @@
from discord import SlashCommandGroup, option, ApplicationContext, Attachment
from datetime import datetime
from zoneinfo import ZoneInfo
from discord import ApplicationContext, Attachment, SlashCommandGroup, option
from discord.ext.commands import Cog
from discord.utils import basic_autocomplete
from classes import PycordGuild, PycordEventStage, PycordEvent
from classes import PycordEvent, PycordEventStage, PycordGuild
from classes.pycord_bot import PycordBot
from modules.utils import autocomplete_active_events, autocomplete_event_stages
async def validate_event_status(
ctx: ApplicationContext,
event: PycordEvent,
) -> None:
if event.cancelled:
# TODO Make a nice message
await ctx.respond("This event was cancelled.")
return
if event.starts <= datetime.now(tz=ZoneInfo("UTC")) <= event.ends:
# TODO Make a nice message
await ctx.respond("Ongoing events cannot be modified.")
return
class Stage(Cog):
"""Cog with event stage management commands."""
@@ -39,13 +57,16 @@ class Stage(Cog):
media: Attachment = None,
) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
pycord_event: PycordEvent = await self.bot.find_event(event)
if not guild.is_configured():
# TODO Make a nice message
await ctx.respond("Guild is not configured.")
return
pycord_event: PycordEvent = await self.bot.find_event(event)
await validate_event_status(ctx, pycord_event)
event_stage: PycordEventStage = await self.bot.create_event_stage(
event=pycord_event,
event_id=pycord_event._id,
@@ -96,14 +117,18 @@ class Stage(Cog):
remove_media: bool = False,
) -> None:
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
pycord_event: PycordEvent = await self.bot.find_event(event)
await validate_event_status(ctx, pycord_event)
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
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.")
@@ -149,14 +174,18 @@ class Stage(Cog):
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
pycord_event: PycordEvent = await self.bot.find_event(event)
await validate_event_status(ctx, pycord_event)
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
await pycord_event.remove_stage(self.bot, event_stage._id, cache=self.bot.cache)
await event_stage.purge(cache=self.bot.cache)

View File

@@ -10,9 +10,9 @@ from discord import LoginFailure
from libbot.utils import config_get
from classes.pycord_bot import PycordBot
from modules.logging_utils import get_logger, get_logging_config
from modules.migrator import migrate_database
from modules.scheduler import scheduler
from modules.utils import get_logger, get_logging_config
makedirs(Path("logs/"), exist_ok=True)

1
modules/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import utils, database, migrator, scheduler

View File

@@ -0,0 +1,8 @@
from .autocomplete_utils import (
autocomplete_active_events,
autocomplete_event_stages,
autocomplete_languages,
autocomplete_timezones,
)
from .cache_utils import restore_from_cache
from .logging_utils import get_logger, get_logging_config

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import List, Dict, Any
from typing import Any, Dict, List
from zoneinfo import ZoneInfo, available_timezones
from bson import ObjectId
@@ -9,28 +9,23 @@ from pymongo import ASCENDING
from modules.database import col_events, col_stages
def hex_to_int(hex_color: str) -> int:
return int(hex_color.lstrip("#"), 16)
def int_to_hex(integer_color: int) -> str:
return "#" + format(integer_color, "06x")
# TODO Maybe move to a separate module
async def autocomplete_timezones(ctx: AutocompleteContext) -> List[str]:
"""Return available timezones"""
return sorted(list(available_timezones()))
# TODO Maybe move to a separate module
async def autocomplete_languages(ctx: AutocompleteContext) -> List[str]:
"""Return locales supported by the bot"""
# 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 autocomplete_active_events(ctx: AutocompleteContext) -> List[OptionChoice]:
"""Return list of active events"""
query: Dict[str, Any] = {
"ended": None,
"ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
@@ -45,8 +40,9 @@ async def autocomplete_active_events(ctx: AutocompleteContext) -> List[OptionCho
return event_names
# TODO Maybe move to a separate module
async def autocomplete_event_stages(ctx: AutocompleteContext) -> List[OptionChoice]:
"""Return list of stages of the event"""
event_id: str | None = ctx.options["event"]
if event_id is None:

View File

@@ -0,0 +1,10 @@
from typing import Any, Dict, Optional
from bson import ObjectId
from libbot.cache.classes import Cache
def restore_from_cache(
cache_prefix: str, cache_key: str | int | ObjectId, cache: Optional[Cache] = None
) -> Dict[str, Any] | None:
return None if cache is None else cache.get_json(f"{cache_prefix}_{cache_key}")

6
modules/utils/color.py Normal file
View File

@@ -0,0 +1,6 @@
def hex_to_int(hex_color: str) -> int:
return int(hex_color.lstrip("#"), 16)
def int_to_hex(integer_color: int) -> str:
return "#" + format(integer_color, "06x")