Implemented basic guild config, improved PycordUser and PycordGuild, fixed PycordBut, added stubs for events

This commit is contained in:
2025-04-18 18:17:35 +02:00
parent 691dd1c958
commit f7fd81f299
5 changed files with 297 additions and 19 deletions

View File

@@ -1,17 +1,20 @@
import logging
from logging import Logger from logging import Logger
from typing import Any from typing import Any
from discord import User from discord import User, Guild
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 PycordUser from classes import PycordUser, PycordGuild
from modules.logging_utils import get_logger
logger: Logger = logging.getLogger(__name__) logger: Logger = get_logger(__name__)
class PycordBot(LibPycordBot): class PycordBot(LibPycordBot):
cache: CacheMemcached | CacheRedis | None = None
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -24,6 +27,10 @@ class PycordBot(LibPycordBot):
# i18n formats than provided by libbot # i18n formats than provided by libbot
self._ = self._modified_string_getter self._ = self._modified_string_getter
def _set_cache_engine(self) -> None:
if "cache" in self.config and self.config["cache"]["type"] is not None:
self.cache = create_cache_client(self.config, self.config["cache"]["type"])
def _modified_string_getter(self, key: str, *args: str, locale: str | None = None) -> Any: def _modified_string_getter(self, key: str, *args: str, locale: str | None = None) -> Any:
"""This method exists because of the different i18n formats than provided by libbot. """This method exists because of the different i18n formats than provided by libbot.
It splits "-" and takes the first part of the provided locale to make complex language codes It splits "-" and takes the first part of the provided locale to make complex language codes
@@ -53,6 +60,24 @@ class PycordBot(LibPycordBot):
else await PycordUser.from_id(user.id, cache=self.cache) else await PycordUser.from_id(user.id, cache=self.cache)
) )
async def find_guild(self, guild: int | Guild) -> PycordGuild:
"""Find Guild by its ID or Guild object.
Args:
guild (int | Guild): ID or User object to extract ID from
Returns:
PycordGuild: Guild object
Raises:
GuildNotFoundException: Guild was not found and creation was not allowed
"""
return (
await PycordGuild.from_id(guild, cache=self.cache)
if isinstance(guild, int)
else await PycordGuild.from_id(guild.id, cache=self.cache)
)
async def start(self, *args: Any, **kwargs: Any) -> None: async def start(self, *args: Any, **kwargs: Any) -> None:
await super().start(*args, **kwargs) await super().start(*args, **kwargs)

View File

@@ -1,17 +1,30 @@
from dataclasses import dataclass from dataclasses import dataclass
from logging import Logger
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from bson import ObjectId from bson import ObjectId
from libbot.cache.classes import Cache from libbot.cache.classes import Cache
from pymongo.results import InsertOneResult
from classes.errors import GuildNotFoundError
from modules.database import col_guilds
from modules.logging_utils import get_logger
logger: Logger = get_logger(__name__)
@dataclass @dataclass
class PycordGuild: class PycordGuild:
"""Dataclass of DB entry of a guild"""
__slots__ = ("_id", "id", "channel_id", "category_id")
__short_name__ = "guild"
__collection__ = col_guilds
_id: ObjectId _id: ObjectId
id: int id: int
channel_id: Optional[int]
def __init__(self) -> None: category_id: Optional[int]
raise NotImplementedError()
@classmethod @classmethod
async def from_id( async def from_id(
@@ -30,7 +43,87 @@ class PycordGuild:
Raises: Raises:
GuildNotFoundError: User was not found and creation was not allowed GuildNotFoundError: User was not found and creation was not allowed
""" """
raise NotImplementedError() if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"{cls.__short_name__}_{guild_id}")
if cached_entry is not None:
return cls(**cached_entry)
db_entry = await cls.__collection__.find_one({"id": guild_id})
if db_entry is None:
if not allow_creation:
raise GuildNotFoundError(guild_id)
db_entry = PycordGuild.get_defaults(guild_id)
insert_result: InsertOneResult = await cls.__collection__.insert_one(db_entry)
db_entry["_id"] = insert_result.inserted_id
if cache is not None:
cache.set_json(f"{cls.__short_name__}_{guild_id}", db_entry)
return cls(**db_entry)
async def _set(self, key: str, value: Any, cache: Optional[Cache] = None) -> 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
"""
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, value)
await self.__collection__.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True)
self._update_cache(cache)
logger.info("Set attribute '%s' of user %s to '%s'", key, self.id, value)
async def _remove(self, key: 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
"""
if not hasattr(self, key):
raise AttributeError()
default_value: Any = PycordGuild.get_default_value(key)
setattr(self, key, default_value)
await self.__collection__.update_one({"_id": self._id}, {"$set": {key: default_value}}, upsert=True)
self._update_cache(cache)
logger.info("Removed attribute '%s' of guild %s", key, self.id)
def _get_cache_key(self) -> str:
return f"{self.__short_name__}_{self.id}"
def _update_cache(self, cache: Optional[Cache] = None) -> None:
if cache is None:
return
user_dict: Dict[str, Any] = self.to_dict()
if user_dict is not None:
cache.set_json(self._get_cache_key(), user_dict)
else:
self._delete_cache(cache)
def _delete_cache(self, cache: Optional[Cache] = None) -> None:
if cache is None:
return
cache.delete(self._get_cache_key())
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]: def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
"""Convert PycordGuild object to a JSON representation. """Convert PycordGuild object to a JSON representation.
@@ -44,4 +137,42 @@ class PycordGuild:
return { return {
"_id": self._id if not json_compatible else str(self._id), "_id": self._id if not json_compatible else str(self._id),
"id": self.id, "id": self.id,
"channel_id": self.channel_id,
"category_id": self.category_id,
} }
@staticmethod
def get_defaults(guild_id: Optional[int] = None) -> Dict[str, Any]:
return {"id": guild_id, "channel_id": None, "category_id": None}
@staticmethod
def get_default_value(key: str) -> Any:
if key not in PycordGuild.get_defaults():
raise KeyError(f"There's no default value for key '{key}' in PycordGuild")
return PycordGuild.get_defaults()[key]
async def purge(self, cache: Optional[Cache] = None) -> None:
"""Completely remove guild data from database. Currently only removes the guild record from guilds collection.
Args:
cache (:obj:`Cache`, optional): Cache engine to write the update into
"""
await self.__collection__.delete_one({"_id": self._id})
self._delete_cache(cache)
# TODO Add documentation
async def set_channel(self, channel_id: Optional[int] = None, cache: Optional[Cache] = None) -> None:
await self._set("channel_id", channel_id, cache)
# TODO Add documentation
async def set_category(self, category_id: Optional[int] = None, cache: Optional[Cache] = None) -> None:
await self._set("category_id", category_id, cache)
# TODO Add documentation
async def reset_channel(self, cache: Optional[Cache] = None) -> None:
await self._remove("channel_id", cache)
# TODO Add documentation
async def reset_category(self, cache: Optional[Cache] = None) -> None:
await self._remove("category_id", cache)

View File

@@ -1,4 +1,3 @@
import logging
from dataclasses import dataclass from dataclasses import dataclass
from logging import Logger from logging import Logger
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -9,8 +8,9 @@ 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
logger: Logger = logging.getLogger(__name__) logger: Logger = get_logger(__name__)
@dataclass @dataclass
@@ -18,6 +18,8 @@ class PycordUser:
"""Dataclass of DB entry of a user""" """Dataclass of DB entry of a user"""
__slots__ = ("_id", "id") __slots__ = ("_id", "id")
__short_name__ = "user"
__collection__ = col_users
_id: ObjectId _id: ObjectId
id: int id: int
@@ -40,12 +42,12 @@ class PycordUser:
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: if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"user_{user_id}") 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 col_users.find_one({"id": user_id}) db_entry = await cls.__collection__.find_one({"id": user_id})
if db_entry is None: if db_entry is None:
if not allow_creation: if not allow_creation:
@@ -53,12 +55,12 @@ class PycordUser:
db_entry = PycordUser.get_defaults(user_id) db_entry = PycordUser.get_defaults(user_id)
insert_result: InsertOneResult = await col_users.insert_one(db_entry) insert_result: InsertOneResult = await cls.__collection__.insert_one(db_entry)
db_entry["_id"] = insert_result.inserted_id db_entry["_id"] = insert_result.inserted_id
if cache is not None: if cache is not None:
cache.set_json(f"user_{user_id}", db_entry) cache.set_json(f"{cls.__short_name__}_{user_id}", db_entry)
return cls(**db_entry) return cls(**db_entry)
@@ -89,7 +91,7 @@ class PycordUser:
setattr(self, key, value) setattr(self, key, value)
await col_users.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True) await self.__collection__.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True)
self._update_cache(cache) self._update_cache(cache)
@@ -109,14 +111,14 @@ class PycordUser:
setattr(self, key, default_value) setattr(self, key, default_value)
await col_users.update_one({"_id": self._id}, {"$set": {key: default_value}}, upsert=True) await self.__collection__.update_one({"_id": self._id}, {"$set": {key: default_value}}, upsert=True)
self._update_cache(cache) self._update_cache(cache)
logger.info("Removed attribute '%s' of user %s", key, self.id) logger.info("Removed attribute '%s' of user %s", key, self.id)
def _get_cache_key(self) -> str: def _get_cache_key(self) -> str:
return f"user_{self.id}" return f"{self.__short_name__}_{self.id}"
def _update_cache(self, cache: Optional[Cache] = None) -> None: def _update_cache(self, cache: Optional[Cache] = None) -> None:
if cache is None: if cache is None:
@@ -154,5 +156,5 @@ class PycordUser:
Args: Args:
cache (:obj:`Cache`, optional): Cache engine to write the update into cache (:obj:`Cache`, optional): Cache engine to write the update into
""" """
await col_users.delete_one({"_id": self._id}) await self.__collection__.delete_one({"_id": self._id})
self._delete_cache(cache) self._delete_cache(cache)

View File

@@ -1,5 +1,7 @@
from discord import SlashCommandGroup, option, CategoryChannel, ApplicationContext, TextChannel
from discord.ext.commands import Cog from discord.ext.commands import Cog
from classes import PycordGuild
from classes.pycord_bot import PycordBot from classes.pycord_bot import PycordBot
@@ -9,6 +11,60 @@ class Config(Cog):
def __init__(self, bot: PycordBot): def __init__(self, bot: PycordBot):
self.bot: PycordBot = bot self.bot: PycordBot = bot
# TODO Introduce i18n
command_group: SlashCommandGroup = SlashCommandGroup("config", "Guild management")
# TODO Introduce i18n
@command_group.command(
name="set",
description="Configure the guild",
)
@option("category", description="Category where channels for each user will be created", required=True)
@option("channel", description="Text channel for admin notifications", required=True)
async def command_config_set(
self, ctx: ApplicationContext, category: CategoryChannel, channel: TextChannel
) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
await guild.set_channel(channel.id, cache=self.bot.cache)
await guild.set_category(category.id, cache=self.bot.cache)
# TODO Make a nice message
await ctx.respond("Okay.")
# TODO Introduce i18n
@command_group.command(
name="reset",
description="Reset the guild's configuration",
)
@option("confirm", description="Confirmation of the operation", required=False)
async def command_config_reset(self, ctx: ApplicationContext, 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)
await guild.reset_channel(cache=self.bot.cache)
await guild.reset_category(cache=self.bot.cache)
# TODO Make a nice message
await ctx.respond("Okay.")
# TODO Introduce i18n
@command_group.command(
name="show",
description="Show the guild's configuration",
)
async def command_config_reset(self, ctx: ApplicationContext) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
# TODO Make a nice message
await ctx.respond(
f"**Guild config**\n\nChannel: <#{guild.channel_id}>\nCategory: <#{guild.category_id}>"
)
def setup(bot: PycordBot) -> None: def setup(bot: PycordBot) -> None:
bot.add_cog(Config(bot)) bot.add_cog(Config(bot))

View File

@@ -1,5 +1,7 @@
from discord import SlashCommandGroup, option, ApplicationContext, Attachment
from discord.ext.commands import Cog from discord.ext.commands import Cog
from classes import PycordGuild
from classes.pycord_bot import PycordBot from classes.pycord_bot import PycordBot
@@ -9,6 +11,68 @@ class Event(Cog):
def __init__(self, bot: PycordBot): def __init__(self, bot: PycordBot):
self.bot: PycordBot = bot self.bot: PycordBot = bot
# TODO Introduce i18n
command_group: SlashCommandGroup = SlashCommandGroup("event", "Event management")
# TODO Implement the command
@command_group.command(
name="create",
description="Create new event",
)
@option("name", description="Name of the event", required=True)
@option("start", description="Date when the event starts (DD.MM.YYYY)", required=True)
@option("finish", description="Date when the event finishes (DD.MM.YYYY)", required=True)
@option("thumbnail", description="Thumbnail of the event", required=False)
async def command_event_create(
self,
ctx: ApplicationContext,
name: str,
start: str,
finish: str,
thumbnail: Attachment = None,
) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
await ctx.respond("Not implemented.")
# TODO Implement the command
@command_group.command(
name="edit",
description="Edit event",
)
@option("event", description="Name of the event", required=True)
@option("name", description="New name of the event", required=False)
@option("start", description="Date when the event starts (DD.MM.YYYY)", required=False)
@option("finish", description="Date when the event finishes (DD.MM.YYYY)", required=False)
@option("thumbnail", description="Thumbnail of the event", required=False)
async def command_event_edit(
self,
ctx: ApplicationContext,
event: str,
name: str,
start: str,
finish: str,
thumbnail: Attachment = None,
) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
await ctx.respond("Not implemented.")
# TODO Implement the command
@command_group.command(
name="cancel",
description="Cancel event",
)
@option("name", description="Name of the event", required=True)
async def command_event_cancel(
self,
ctx: ApplicationContext,
name: str,
) -> None:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
await ctx.respond("Not implemented.")
def setup(bot: PycordBot) -> None: def setup(bot: PycordBot) -> None:
bot.add_cog(Event(bot)) bot.add_cog(Event(bot))