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.cache.manager import create_cache_client
from libbot.pycord.classes import PycordBot as LibPycordBot from libbot.pycord.classes import PycordBot as LibPycordBot
from classes import PycordEvent, PycordGuild, PycordUser, PycordEventStage from classes import PycordEvent, PycordEventStage, PycordGuild, PycordUser
from modules.logging_utils import get_logger from modules.utils import get_logger
logger: Logger = get_logger(__name__) logger: Logger = get_logger(__name__)

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ from pymongo.results import InsertOneResult
from classes.errors.pycord_user import UserNotFoundError from classes.errors.pycord_user import UserNotFoundError
from modules.database import col_users 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__) logger: Logger = get_logger(__name__)
@@ -41,11 +41,10 @@ class PycordUser:
Raises: Raises:
UserNotFoundError: User was not found and creation was not allowed UserNotFoundError: User was not found and creation was not allowed
""" """
if cache is not None: cached_entry: Dict[str, Any] | None = restore_from_cache(cls.__short_name__, user_id, cache=cache)
cached_entry: Dict[str, Any] | None = cache.get_json(f"{cls.__short_name__}_{user_id}")
if cached_entry is not None: if cached_entry is not None:
return cls(**cached_entry) return cls(**cached_entry)
db_entry = await cls.__collection__.find_one({"id": user_id}) db_entry = await cls.__collection__.find_one({"id": user_id})
@@ -78,44 +77,49 @@ class PycordUser:
"id": self.id, "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. """Set attribute data and save it into the database.
Args: Args:
key (str): Attribute to change
value (Any): Value to set
cache (:obj:`Cache`, optional): Cache engine to write the update into 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): for key, value in kwargs.items():
raise AttributeError() 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) 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. """Remove attribute data and save it into the database.
Args: Args:
key (str): Attribute to remove
cache (:obj:`Cache`, optional): Cache engine to write the update into cache (:obj:`Cache`, optional): Cache engine to write the update into
*args (str): List of attributes to remove
""" """
if not hasattr(self, key): attributes: Dict[str, Any] = {}
raise AttributeError()
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) 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: def _get_cache_key(self) -> str:
return f"{self.__short_name__}_{self.id}" 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 import PycordGuild
from classes.pycord_bot import PycordBot 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): class Config(Cog):

View File

@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import Dict, Any from typing import Any, Dict
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from bson import ObjectId 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 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.ext.commands import Cog
from discord.utils import basic_autocomplete 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 classes.pycord_bot import PycordBot
from modules.utils import autocomplete_active_events, autocomplete_event_stages 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): class Stage(Cog):
"""Cog with event stage management commands.""" """Cog with event stage management commands."""
@@ -39,13 +57,16 @@ class Stage(Cog):
media: Attachment = None, media: Attachment = None,
) -> None: ) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
pycord_event: PycordEvent = await self.bot.find_event(event)
if not guild.is_configured(): if not guild.is_configured():
# TODO Make a nice message # TODO Make a nice message
await ctx.respond("Guild is not configured.") await ctx.respond("Guild is not configured.")
return 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_stage: PycordEventStage = await self.bot.create_event_stage(
event=pycord_event, event=pycord_event,
event_id=pycord_event._id, event_id=pycord_event._id,
@@ -96,14 +117,18 @@ class Stage(Cog):
remove_media: bool = False, remove_media: bool = False,
) -> None: ) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) 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(): if not guild.is_configured():
# TODO Make a nice message # TODO Make a nice message
await ctx.respond("Guild is not configured.") await ctx.respond("Guild is not configured.")
return 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): if order is not None and order > len(pycord_event.stage_ids):
# TODO Make a nice message # TODO Make a nice message
await ctx.respond("Stage sequence out of range.") await ctx.respond("Stage sequence out of range.")
@@ -149,14 +174,18 @@ class Stage(Cog):
return return
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) 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(): if not guild.is_configured():
# TODO Make a nice message # TODO Make a nice message
await ctx.respond("Guild is not configured.") await ctx.respond("Guild is not configured.")
return 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 pycord_event.remove_stage(self.bot, event_stage._id, cache=self.bot.cache)
await event_stage.purge(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 libbot.utils import config_get
from classes.pycord_bot import PycordBot from classes.pycord_bot import PycordBot
from modules.logging_utils import get_logger, get_logging_config
from modules.migrator import migrate_database from modules.migrator import migrate_database
from modules.scheduler import scheduler from modules.scheduler import scheduler
from modules.utils import get_logger, get_logging_config
makedirs(Path("logs/"), exist_ok=True) 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 datetime import datetime
from typing import List, Dict, Any from typing import Any, Dict, List
from zoneinfo import ZoneInfo, available_timezones from zoneinfo import ZoneInfo, available_timezones
from bson import ObjectId from bson import ObjectId
@@ -9,28 +9,23 @@ from pymongo import ASCENDING
from modules.database import col_events, col_stages 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]: async def autocomplete_timezones(ctx: AutocompleteContext) -> List[str]:
"""Return available timezones"""
return sorted(list(available_timezones())) return sorted(list(available_timezones()))
# TODO Maybe move to a separate module
async def autocomplete_languages(ctx: AutocompleteContext) -> List[str]: async def autocomplete_languages(ctx: AutocompleteContext) -> List[str]:
"""Return locales supported by the bot"""
# TODO Discord normally uses a different set of locales. # TODO Discord normally uses a different set of locales.
# For example, "en" being "en-US", etc. This will require changes to locale handling later. # For example, "en" being "en-US", etc. This will require changes to locale handling later.
return ctx.bot.locales.keys() return ctx.bot.locales.keys()
# TODO Maybe move to a separate module
async def autocomplete_active_events(ctx: AutocompleteContext) -> List[OptionChoice]: async def autocomplete_active_events(ctx: AutocompleteContext) -> List[OptionChoice]:
"""Return list of active events"""
query: Dict[str, Any] = { query: Dict[str, Any] = {
"ended": None, "ended": None,
"ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))}, "ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
@@ -45,8 +40,9 @@ async def autocomplete_active_events(ctx: AutocompleteContext) -> List[OptionCho
return event_names return event_names
# TODO Maybe move to a separate module
async def autocomplete_event_stages(ctx: AutocompleteContext) -> List[OptionChoice]: async def autocomplete_event_stages(ctx: AutocompleteContext) -> List[OptionChoice]:
"""Return list of stages of the event"""
event_id: str | None = ctx.options["event"] event_id: str | None = ctx.options["event"]
if event_id is None: 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")