Compare commits
14 Commits
498d822e09
...
dev
Author | SHA1 | Date | |
---|---|---|---|
17a445a230
|
|||
126dfa8b30
|
|||
294a57338e
|
|||
c0451de27a
|
|||
5f9ef163e1
|
|||
8d60d8aef5
|
|||
3700e4055d
|
|||
e7c719312f
|
|||
96c1314234
|
|||
d1498f38e9
|
|||
86c75d06fa
|
|||
9d562e2e9d
|
|||
6b5a276f00
|
|||
134533e342
|
102
README.md
102
README.md
@@ -15,10 +15,10 @@ Open source Discord bot for quizzes and quest-like events.
|
||||
### Installation from release
|
||||
|
||||
1. Download the release archive from [Releases](https://git.end-play.xyz/profitroll/QuizBot/releases)
|
||||
2. Unpack the archive to a folder of your choice
|
||||
3. Go to the project folder
|
||||
2. Unpack the archive to a directory of your choice
|
||||
3. Go to the project's directory
|
||||
4. Create a virtual environment: `python3 -m venv .venv`
|
||||
5. Activate virtual environment:
|
||||
5. Activate the virtual environment:
|
||||
- Linux: `source .venv/bin/activate`
|
||||
- Windows (cmd): `.venv/bin/activate.bat`
|
||||
- Windows (PowerShell): `.venv/bin/activate.ps1`
|
||||
@@ -26,20 +26,108 @@ Open source Discord bot for quizzes and quest-like events.
|
||||
7. Copy example config to a real file: `cp config_example.json config.json`
|
||||
8. Configure the bot (see [Configuration](#configuration))
|
||||
9. Start the bot: `python main.py`
|
||||
10. The bot can be stopped by a keyboard interrupt (`Ctrl+C`) and a virtual environment can be deactivated using
|
||||
`deactivate`
|
||||
|
||||
### Installation from source
|
||||
|
||||
1. Clone the repository: `git clone https://git.end-play.xyz/profitroll/QuizBot.git`
|
||||
2. Go to the project's folder: `cd QuizBot`
|
||||
2. Go to the project's directory: `cd QuizBot`
|
||||
3. Continue from step 4 of [Installation from release](#installation-from-release)
|
||||
|
||||
## Configuration
|
||||
|
||||
TODO
|
||||
```json
|
||||
{
|
||||
// Bot's default locale. Based on file name from locale/
|
||||
"locale": "en-US",
|
||||
// Debug mode setting
|
||||
"debug": false,
|
||||
// Bot's config
|
||||
"bot": {
|
||||
// Discord ID(s) of bot's owner(s)
|
||||
"owners": [
|
||||
0
|
||||
],
|
||||
// Discord ID(s) of the debug guild(s)
|
||||
"debug_guilds": [
|
||||
0
|
||||
],
|
||||
// Bot's token
|
||||
"bot_token": "",
|
||||
// Bot's timezone
|
||||
"timezone": "UTC",
|
||||
// Bot's status activity
|
||||
"status": {
|
||||
// Whether activity is enabled
|
||||
"enabled": true,
|
||||
// Type of the activity. Can be: "playing", "watching", "listening", "streaming", "competing" or "custom"
|
||||
"activity_type": "playing",
|
||||
// Text of the activity
|
||||
"activity_text": "The Game Of Life"
|
||||
}
|
||||
},
|
||||
// Database connection
|
||||
"database": {
|
||||
// User name for database connection. null if without auth
|
||||
"user": null,
|
||||
// User password for database connection. null if without auth
|
||||
"password": null,
|
||||
// Database host
|
||||
"host": "127.0.0.1",
|
||||
// Database port
|
||||
"port": 27017,
|
||||
// Database name
|
||||
"name": "quiz_bot"
|
||||
},
|
||||
// Cache connection
|
||||
"cache": {
|
||||
// Type of caching engine. Can be: "memcached", "redis" or null
|
||||
"type": null,
|
||||
// Memcached connection. Only used if cache type is "memcached"
|
||||
"memcached": {
|
||||
// Memcached URI
|
||||
"uri": "127.0.0.1:11211"
|
||||
},
|
||||
// Redis connection. Only used if cache type is "redis"
|
||||
"redis": {
|
||||
// Redis URI
|
||||
"uri": "redis://127.0.0.1:6379/0"
|
||||
}
|
||||
},
|
||||
// Emojis used by guilds that prefer emoji messages
|
||||
"emojis": {
|
||||
// Markdown of a Discord emoji to be used for wrong guesses
|
||||
"guess_wrong": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
TODO
|
||||
### Upgrading from release
|
||||
|
||||
Installing over the older version is not supported. Fresh installation is necessary to prevent data corruption.
|
||||
|
||||
1. Make a backup of the project's directory. Some of the old files will be reused
|
||||
2. Follow the [Installation from release](#installation-from-release) from the beginning and stop before 7th step
|
||||
3. Copy file `config.json` and directory `data` from the backup you made into the new installation's directory
|
||||
4. While still in the virtual environment, migrate the database: `python main.py --migrate`
|
||||
|
||||
After these steps are performed, the bot is ready to be started and used.
|
||||
|
||||
### Upgrading from source
|
||||
|
||||
1. Make a backup of the project's directory
|
||||
2. Go to the project's directory
|
||||
3. Update the project: `git pull`
|
||||
4. Activate the virtual environment:
|
||||
- Linux: `source .venv/bin/activate`
|
||||
- Windows (cmd): `.venv/bin/activate.bat`
|
||||
- Windows (PowerShell): `.venv/bin/activate.ps1`
|
||||
5. Migrate the database: `python main.py --migrate`
|
||||
|
||||
After these steps are performed, the bot is ready to be started and used.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -51,4 +139,4 @@ TODO
|
||||
Only admins should have access to following commands: `/config`, `/event`, `/stage` and `/user`.
|
||||
Allowing access to `/status` is not recommended, however won't do any harm if done so.
|
||||
3. Configure bot for usage on your server using `/config set` providing all the necessary arguments.
|
||||
Timezones are compatible with summer time (e.g. `CET` will be interpreted as `CEST` during summer time).
|
||||
Timezones are compatible with daylight saving time (e.g. `CET` will be interpreted as `CEST` during daylight saving).
|
1
classes/abstract/__init__.py
Normal file
1
classes/abstract/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .cacheable import Cacheable
|
81
classes/abstract/cacheable.py
Normal file
81
classes/abstract/cacheable.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, ClassVar, Dict, Optional
|
||||
|
||||
from libbot.cache.classes import Cache
|
||||
from pymongo.asynchronous.collection import AsyncCollection
|
||||
|
||||
|
||||
class Cacheable(ABC):
|
||||
"""Abstract class for cacheable"""
|
||||
|
||||
__short_name__: str
|
||||
__collection__: ClassVar[AsyncCollection]
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def from_id(cls, *args: Any, cache: Optional[Cache] = None, **kwargs: Any) -> Any:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _set(self, cache: Optional[Cache] = None, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_cache_key(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _update_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _delete_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _entry_to_cache(db_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _entry_from_cache(cache_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_defaults(**kwargs: Any) -> Dict[str, Any]:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_default_value(key: str) -> Any:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(
|
||||
self,
|
||||
cache: Optional[Cache] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def reset(
|
||||
self,
|
||||
*args: str,
|
||||
cache: Optional[Cache] = None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
pass
|
0
classes/base/__init__.py
Normal file
0
classes/base/__init__.py
Normal file
110
classes/base/base_cacheable.py
Normal file
110
classes/base/base_cacheable.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from abc import ABC
|
||||
from logging import Logger
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from bson import ObjectId
|
||||
from libbot.cache.classes import Cache
|
||||
|
||||
from classes.abstract import Cacheable
|
||||
from modules.utils import get_logger
|
||||
|
||||
logger: Logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BaseCacheable(Cacheable, ABC):
|
||||
"""Base implementation of Cacheable used by all cachable classes."""
|
||||
|
||||
_id: ObjectId
|
||||
|
||||
async def _set(self, cache: Optional[Cache] = None, **kwargs: Any) -> None:
|
||||
for key, value in kwargs.items():
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
await self.__collection__.update_one({"_id": self._id}, {"$set": kwargs})
|
||||
|
||||
self._update_cache(cache)
|
||||
|
||||
logger.info("Set attributes of %s to %s", self._id, kwargs)
|
||||
|
||||
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
|
||||
attributes: Dict[str, Any] = {}
|
||||
|
||||
for key in args:
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
default_value: Any = self.get_default_value(key)
|
||||
|
||||
setattr(self, key, default_value)
|
||||
|
||||
attributes[key] = default_value
|
||||
|
||||
await self.__collection__.update_one({"_id": self._id}, {"$set": attributes})
|
||||
|
||||
self._update_cache(cache)
|
||||
|
||||
logger.info("Reset attributes %s of %s to default values", args, self._id)
|
||||
|
||||
def _update_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
if cache is None:
|
||||
return
|
||||
|
||||
object_dict: Dict[str, Any] = self.to_dict(json_compatible=True)
|
||||
|
||||
if object_dict is not None:
|
||||
cache.set_json(self._get_cache_key(), object_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())
|
||||
|
||||
async def update(
|
||||
self,
|
||||
cache: Optional[Cache] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Update attribute(s) on the object and save the updated entry into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to update the cache.
|
||||
**kwargs (Any): Mapping of attributes in format `attribute_name=attribute_value` to update.
|
||||
|
||||
Raises:
|
||||
AttributeError: Provided attribute does not exist in the class.
|
||||
"""
|
||||
await self._set(cache=cache, **kwargs)
|
||||
|
||||
async def reset(
|
||||
self,
|
||||
*args: str,
|
||||
cache: Optional[Cache] = None,
|
||||
) -> None:
|
||||
"""Remove attribute(s) on the object, replace them with a default value and save the updated entry into the database.
|
||||
|
||||
Args:
|
||||
*args (str): List of attributes to remove.
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to update the cache.
|
||||
|
||||
Raises:
|
||||
AttributeError: Provided attribute does not exist in the class.
|
||||
"""
|
||||
await self._remove(*args, cache=cache)
|
||||
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
"""Completely remove object data from database. Currently only removes the record from a respective collection.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to update the cache.
|
||||
"""
|
||||
await self.__collection__.delete_one({"_id": self._id})
|
||||
|
||||
self._delete_cache(cache)
|
||||
|
||||
logger.info("Purged %s from the database", self._id)
|
@@ -14,20 +14,20 @@ from typing_extensions import override
|
||||
|
||||
from classes import PycordEvent, PycordEventStage, PycordGuild, PycordUser
|
||||
from classes.errors import (
|
||||
DiscordGuildMemberNotFoundError,
|
||||
EventNotFoundError,
|
||||
EventStageMissingSequenceError,
|
||||
EventStageNotFoundError,
|
||||
GuildNotFoundError,
|
||||
DiscordGuildMemberNotFoundError,
|
||||
)
|
||||
from modules.database import col_events, col_users
|
||||
from modules.database import _update_database_indexes, col_events, col_users
|
||||
from modules.utils import get_logger
|
||||
|
||||
logger: Logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PycordBot(LibPycordBot):
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
started: datetime
|
||||
cache: CacheMemcached | CacheRedis | None = None
|
||||
@@ -58,6 +58,7 @@ class PycordBot(LibPycordBot):
|
||||
@override
|
||||
async def start(self, *args: Any, **kwargs: Any) -> None:
|
||||
await self._schedule_tasks()
|
||||
await _update_database_indexes()
|
||||
|
||||
self.started = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
@@ -189,6 +190,12 @@ class PycordBot(LibPycordBot):
|
||||
self._("admin_event_started", "messages").format(event_name=event.name),
|
||||
)
|
||||
|
||||
await self.notify_users(
|
||||
guild,
|
||||
pycord_guild,
|
||||
self._("event_started", "messages").format(event_name=event.name),
|
||||
)
|
||||
|
||||
async def _process_events_end(self) -> None:
|
||||
# Get events to end
|
||||
events: List[PycordEvent] = await self._get_events(
|
||||
@@ -228,6 +235,17 @@ class PycordBot(LibPycordBot):
|
||||
# Get list of participants
|
||||
users: List[PycordUser] = await self._get_event_participants(event._id)
|
||||
|
||||
event_ended_string: str = self._("event_ended", "messages").format(
|
||||
event_name=event.name, stages=stages_string
|
||||
)
|
||||
|
||||
chunk_size: int = 2000
|
||||
|
||||
event_info_chunks: List[str] = [
|
||||
event_ended_string[i : i + chunk_size]
|
||||
for i in range(0, len(event_ended_string), chunk_size)
|
||||
]
|
||||
|
||||
for user in users:
|
||||
if str(event._id) not in user.event_channels:
|
||||
logger.warning(
|
||||
@@ -240,30 +258,58 @@ class PycordBot(LibPycordBot):
|
||||
# Send a notification about event start
|
||||
user_channel: TextChannel = guild.get_channel(user.event_channels[str(event._id)])
|
||||
|
||||
event_ended_string: str = self._("event_ended", "messages").format(
|
||||
event_name=event.name, stages=stages_string
|
||||
)
|
||||
|
||||
chunk_size: int = 2000
|
||||
|
||||
event_info_chunks: List[str] = [
|
||||
event_ended_string[i : i + chunk_size]
|
||||
for i in range(0, len(event_ended_string), chunk_size)
|
||||
]
|
||||
|
||||
for chunk in event_info_chunks:
|
||||
await user_channel.send(chunk)
|
||||
|
||||
# Lock each participant out
|
||||
await user.lock_event_channel(guild, event._id, channel=user_channel)
|
||||
|
||||
await event.end(cache=self.cache)
|
||||
|
||||
await self.notify_admins(
|
||||
guild,
|
||||
pycord_guild,
|
||||
self._("admin_event_ended", "messages").format(event_name=event.name),
|
||||
)
|
||||
|
||||
await event.end(cache=self.cache)
|
||||
await self._notify_general_channel_event_end(guild, pycord_guild, event, stages)
|
||||
|
||||
async def _notify_general_channel_event_end(
|
||||
self, guild: Guild, pycord_guild: PycordGuild, event: PycordEvent, stages: List[PycordEventStage]
|
||||
) -> None:
|
||||
event_ended_string: str = self._("event_ended_short", "messages").format(event_name=event.name)
|
||||
|
||||
await self.notify_users(
|
||||
guild,
|
||||
pycord_guild,
|
||||
event_ended_string,
|
||||
)
|
||||
|
||||
chunk_size: int = 2000
|
||||
|
||||
for stage in stages:
|
||||
header_full: str = self._("stage_entry_header", "messages").format(
|
||||
sequence=stage.sequence + 1, question=stage.question
|
||||
)
|
||||
|
||||
header_chunks: List[str] = [
|
||||
header_full[i : i + chunk_size] for i in range(0, len(header_full), chunk_size)
|
||||
]
|
||||
header_chunks_length: int = len(header_chunks)
|
||||
|
||||
files: List[File] | None = stage.get_media_files()
|
||||
|
||||
for index, chunk in enumerate(header_chunks):
|
||||
await self.notify_users(
|
||||
guild,
|
||||
pycord_guild,
|
||||
chunk,
|
||||
files=None if index != header_chunks_length - 1 else files,
|
||||
)
|
||||
|
||||
await self.notify_users(
|
||||
guild, pycord_guild, self._("stage_entry_footer", "messages").format(answer=stage.answer)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _get_events(query: Dict[str, Any]) -> List[PycordEvent]:
|
||||
@@ -290,18 +336,35 @@ class PycordBot(LibPycordBot):
|
||||
# TODO Add documentation
|
||||
@staticmethod
|
||||
async def notify_admins(guild: Guild, pycord_guild: PycordGuild, message: str) -> None:
|
||||
management_channel: TextChannel | None = guild.get_channel(pycord_guild.channel_id)
|
||||
management_channel: TextChannel | None = guild.get_channel(pycord_guild.management_channel_id)
|
||||
|
||||
if management_channel is None:
|
||||
logger.error(
|
||||
"Discord channel with ID %s in guild with ID %s could not be found!",
|
||||
pycord_guild.channel_id,
|
||||
pycord_guild.management_channel_id,
|
||||
guild.id,
|
||||
)
|
||||
return
|
||||
|
||||
await management_channel.send(message)
|
||||
|
||||
# TODO Add documentation
|
||||
@staticmethod
|
||||
async def notify_users(
|
||||
guild: Guild, pycord_guild: PycordGuild, message: str, files: Optional[List[File]] = None
|
||||
) -> None:
|
||||
general_channel: TextChannel | None = guild.get_channel(pycord_guild.general_channel_id)
|
||||
|
||||
if general_channel is None:
|
||||
logger.error(
|
||||
"Discord channel with ID %s in guild with ID %s could not be found!",
|
||||
pycord_guild.general_channel_id,
|
||||
guild.id,
|
||||
)
|
||||
return
|
||||
|
||||
await general_channel.send(message, files=files)
|
||||
|
||||
async def find_user(self, user: int | User, guild: int | Guild) -> PycordUser:
|
||||
"""Find User by its ID or User object.
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Module with class PycordEvent."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, tzinfo
|
||||
from logging import Logger
|
||||
from typing import Any, Dict, List, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -11,6 +11,7 @@ from libbot.cache.classes import Cache
|
||||
from pymongo import DESCENDING
|
||||
from pymongo.results import InsertOneResult
|
||||
|
||||
from classes.base.base_cacheable import BaseCacheable
|
||||
from classes.errors import EventNotFoundError
|
||||
from modules.database import col_events
|
||||
from modules.utils import get_logger, restore_from_cache
|
||||
@@ -19,7 +20,7 @@ logger: Logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PycordEvent:
|
||||
class PycordEvent(BaseCacheable):
|
||||
"""Object representation of an event in the database.
|
||||
|
||||
Attributes:
|
||||
@@ -82,9 +83,9 @@ class PycordEvent:
|
||||
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)
|
||||
return cls(**cls._entry_from_cache(cached_entry))
|
||||
|
||||
db_entry = await cls.__collection__.find_one(
|
||||
db_entry: Dict[str, Any] | None = await cls.__collection__.find_one(
|
||||
{"_id": event_id if isinstance(event_id, ObjectId) else ObjectId(event_id)}
|
||||
)
|
||||
|
||||
@@ -92,7 +93,7 @@ class PycordEvent:
|
||||
raise EventNotFoundError(event_id=event_id)
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{event_id}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{event_id}", cls._entry_to_cache(dict(db_entry)))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
@@ -123,7 +124,7 @@ class PycordEvent:
|
||||
raise EventNotFoundError(event_name=event_name, guild_id=guild_id)
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{db_entry['_id']}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{db_entry['_id']}", cls._entry_to_cache(db_entry))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
@@ -172,61 +173,24 @@ class PycordEvent:
|
||||
db_entry["_id"] = insert_result.inserted_id
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{guild_id}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{guild_id}", cls._entry_to_cache(db_entry))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
async def _set(self, cache: Optional[Cache] = None, **kwargs: Any) -> None:
|
||||
for key, value in kwargs.items():
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError(f"Attribute '{key}' does not exist in PycordEvent")
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
await self.__collection__.update_one({"_id": self._id}, {"$set": kwargs}, upsert=True)
|
||||
|
||||
self._update_cache(cache)
|
||||
|
||||
logger.info("Set attributes of event %s to %s", self._id, kwargs)
|
||||
await super()._set(cache, **kwargs)
|
||||
|
||||
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
|
||||
attributes: Dict[str, Any] = {}
|
||||
|
||||
for key in args:
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError(f"Attribute '{key}' does not exist in PycordEvent")
|
||||
|
||||
default_value: Any = self.get_default_value(key)
|
||||
|
||||
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("Reset attributes %s of event %s to default values", args, self._id)
|
||||
await super()._remove(*args, cache=cache)
|
||||
|
||||
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)
|
||||
super()._update_cache(cache)
|
||||
|
||||
def _delete_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
if cache is None:
|
||||
return
|
||||
|
||||
cache.delete(self._get_cache_key())
|
||||
super()._delete_cache(cache)
|
||||
|
||||
async def _update_event_stage_order(
|
||||
self,
|
||||
@@ -253,6 +217,32 @@ class PycordEvent:
|
||||
if stage_index != old_stage_index:
|
||||
await (await bot.find_event_stage(event_stage_id)).update(cache, sequence=stage_index)
|
||||
|
||||
@staticmethod
|
||||
def _entry_to_cache(db_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
cache_entry: Dict[str, Any] = db_entry.copy()
|
||||
|
||||
cache_entry["_id"] = str(cache_entry["_id"])
|
||||
cache_entry["created"] = cache_entry["created"].isoformat()
|
||||
cache_entry["ended"] = None if cache_entry["ended"] is None else cache_entry["ended"].isoformat()
|
||||
cache_entry["starts"] = cache_entry["starts"].isoformat()
|
||||
cache_entry["ends"] = cache_entry["ends"].isoformat()
|
||||
cache_entry["stage_ids"] = [str(stage_id) for stage_id in cache_entry["stage_ids"]]
|
||||
|
||||
return cache_entry
|
||||
|
||||
@staticmethod
|
||||
def _entry_from_cache(cache_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_entry: Dict[str, Any] = cache_entry.copy()
|
||||
|
||||
db_entry["_id"] = ObjectId(db_entry["_id"])
|
||||
db_entry["created"] = datetime.fromisoformat(db_entry["created"])
|
||||
db_entry["ended"] = None if db_entry["ended"] is None else datetime.fromisoformat(db_entry["ended"])
|
||||
db_entry["starts"] = datetime.fromisoformat(db_entry["starts"])
|
||||
db_entry["ends"] = datetime.fromisoformat(db_entry["ends"])
|
||||
db_entry["stage_ids"] = [ObjectId(stage_id) for stage_id in db_entry["stage_ids"]]
|
||||
|
||||
return db_entry
|
||||
|
||||
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
|
||||
"""Convert the object to a JSON representation.
|
||||
|
||||
@@ -266,14 +256,20 @@ class PycordEvent:
|
||||
"_id": self._id if not json_compatible else str(self._id),
|
||||
"name": self.name,
|
||||
"guild_id": self.guild_id,
|
||||
"created": self.created,
|
||||
"ended": self.ended,
|
||||
"created": self.created if not json_compatible else self.created.isoformat(),
|
||||
"ended": (
|
||||
self.ended
|
||||
if not json_compatible
|
||||
else (None if self.ended is None else self.ended.isoformat())
|
||||
),
|
||||
"is_cancelled": self.is_cancelled,
|
||||
"creator_id": self.creator_id,
|
||||
"starts": self.starts,
|
||||
"ends": self.ends,
|
||||
"starts": self.starts if not json_compatible else self.starts.isoformat(),
|
||||
"ends": self.ends if not json_compatible else self.ends.isoformat(),
|
||||
"thumbnail": self.thumbnail,
|
||||
"stage_ids": self.stage_ids,
|
||||
"stage_ids": (
|
||||
self.stage_ids if not json_compatible else [str(stage_id) for stage_id in self.stage_ids]
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -317,43 +313,19 @@ class PycordEvent:
|
||||
async def update(
|
||||
self,
|
||||
cache: Optional[Cache] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Update attribute(s) on the object and save the updated entry into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to update the cache.
|
||||
**kwargs (Any): Mapping of attributes in format `attribute_name=attribute_value` to update.
|
||||
|
||||
Raises:
|
||||
AttributeError: Provided attribute does not exist in the class.
|
||||
"""
|
||||
await self._set(cache=cache, **kwargs)
|
||||
await super().update(cache=cache, **kwargs)
|
||||
|
||||
async def reset(
|
||||
self,
|
||||
*args,
|
||||
*args: str,
|
||||
cache: Optional[Cache] = None,
|
||||
) -> None:
|
||||
"""Remove attribute(s) on the object, replace them with a default value and save the updated entry into the database.
|
||||
|
||||
Args:
|
||||
*args (str): List of attributes to remove.
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to update the cache.
|
||||
|
||||
Raises:
|
||||
AttributeError: Provided attribute does not exist in the class.
|
||||
"""
|
||||
await self._remove(*args, cache=cache)
|
||||
await super().reset(*args, cache=cache)
|
||||
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
"""Completely remove event data from database. Currently only removes the event record from events collection.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to update the cache.
|
||||
"""
|
||||
await self.__collection__.delete_one({"_id": self._id})
|
||||
self._delete_cache(cache)
|
||||
await super().purge(cache)
|
||||
|
||||
async def cancel(self, cache: Optional[Cache] = None) -> None:
|
||||
"""Cancel the event.
|
||||
@@ -456,7 +428,7 @@ class PycordEvent:
|
||||
|
||||
return self.ends.replace(tzinfo=ZoneInfo("UTC"))
|
||||
|
||||
def get_start_date_localized(self, tz: str | timezone | ZoneInfo) -> datetime:
|
||||
def get_start_date_localized(self, tz: tzinfo) -> datetime:
|
||||
"""Get the event start date in the provided timezone.
|
||||
|
||||
Returns:
|
||||
@@ -470,7 +442,7 @@ class PycordEvent:
|
||||
|
||||
return self.starts.replace(tzinfo=tz)
|
||||
|
||||
def get_end_date_localized(self, tz: str | timezone | ZoneInfo) -> datetime:
|
||||
def get_end_date_localized(self, tz: tzinfo) -> datetime:
|
||||
"""Get the event end date in the provided timezone.
|
||||
|
||||
Returns:
|
||||
|
@@ -10,6 +10,7 @@ from discord import File
|
||||
from libbot.cache.classes import Cache
|
||||
from pymongo.results import InsertOneResult
|
||||
|
||||
from classes.base.base_cacheable import BaseCacheable
|
||||
from classes.errors import EventStageNotFoundError
|
||||
from modules.database import col_stages
|
||||
from modules.utils import get_logger, restore_from_cache
|
||||
@@ -18,7 +19,7 @@ logger: Logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PycordEventStage:
|
||||
class PycordEventStage(BaseCacheable):
|
||||
__slots__ = (
|
||||
"_id",
|
||||
"event_id",
|
||||
@@ -45,24 +46,25 @@ class PycordEventStage:
|
||||
|
||||
@classmethod
|
||||
async def from_id(cls, stage_id: str | ObjectId, cache: Optional[Cache] = None) -> "PycordEventStage":
|
||||
"""Find event stage in the database.
|
||||
"""Find the event stage by its ID and construct PycordEventStage from database entry.
|
||||
|
||||
Args:
|
||||
stage_id (str | ObjectId): Stage's ID
|
||||
cache (:obj:`Cache`, optional): Cache engine to get the cache from
|
||||
stage_id (str | ObjectId): ID of the event stage to look up.
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to fetch and update the cache.
|
||||
|
||||
Returns:
|
||||
PycordEventStage: Event stage object
|
||||
PycordEventStage: Object of the found event stage.
|
||||
|
||||
Raises:
|
||||
EventStageNotFoundError: Event stage was not found
|
||||
EventStageNotFoundError: Event stage with such ID does not exist.
|
||||
InvalidId: Provided event stage ID is of invalid format.
|
||||
"""
|
||||
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)
|
||||
return cls(**cls._entry_from_cache(cached_entry))
|
||||
|
||||
db_entry = await cls.__collection__.find_one(
|
||||
db_entry: Dict[str, Any] | None = await cls.__collection__.find_one(
|
||||
{"_id": stage_id if isinstance(stage_id, ObjectId) else ObjectId(stage_id)}
|
||||
)
|
||||
|
||||
@@ -70,7 +72,7 @@ class PycordEventStage:
|
||||
raise EventStageNotFoundError(stage_id)
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{stage_id}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{stage_id}", cls._entry_to_cache(dict(db_entry)))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
@@ -103,98 +105,77 @@ class PycordEventStage:
|
||||
db_entry["_id"] = insert_result.inserted_id
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{guild_id}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{guild_id}", cls._entry_to_cache(db_entry))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
async def _set(self, cache: Optional[Cache] = None, **kwargs: Any) -> None:
|
||||
"""Set attribute data and save it into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
**kwargs (Any): Mapping of attribute names and respective values to be set
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
await self.__collection__.update_one({"_id": self._id}, {"$set": kwargs}, upsert=True)
|
||||
|
||||
self._update_cache(cache)
|
||||
|
||||
logger.info("Set attributes of event stage %s to %s", self._id, kwargs)
|
||||
await super()._set(cache, **kwargs)
|
||||
|
||||
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
|
||||
"""Remove attribute data and save it into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
*args (str): List of attributes to remove
|
||||
"""
|
||||
attributes: Dict[str, Any] = {}
|
||||
|
||||
for key in args:
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
default_value: Any = self.get_default_value(key)
|
||||
|
||||
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("Reset attributes %s of event stage %s to default values", args, self._id)
|
||||
await super()._remove(*args, cache=cache)
|
||||
|
||||
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)
|
||||
super()._update_cache(cache)
|
||||
|
||||
def _delete_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
if cache is None:
|
||||
return
|
||||
super()._delete_cache(cache)
|
||||
|
||||
cache.delete(self._get_cache_key())
|
||||
@staticmethod
|
||||
def _entry_to_cache(db_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
cache_entry: Dict[str, Any] = db_entry.copy()
|
||||
|
||||
cache_entry["_id"] = str(cache_entry["_id"])
|
||||
cache_entry["event_id"] = str(cache_entry["event_id"])
|
||||
cache_entry["created"] = cache_entry["created"].isoformat()
|
||||
|
||||
return cache_entry
|
||||
|
||||
@staticmethod
|
||||
def _entry_from_cache(cache_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_entry: Dict[str, Any] = cache_entry.copy()
|
||||
|
||||
db_entry["_id"] = ObjectId(db_entry["_id"])
|
||||
db_entry["event_id"] = ObjectId(db_entry["event_id"])
|
||||
db_entry["created"] = datetime.fromisoformat(db_entry["created"])
|
||||
|
||||
return db_entry
|
||||
|
||||
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
|
||||
"""Convert PycordEventStage object to a JSON representation.
|
||||
"""Convert the object to a JSON representation.
|
||||
|
||||
Args:
|
||||
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted
|
||||
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: JSON representation of PycordEventStage
|
||||
Dict[str, Any]: JSON representation of the object.
|
||||
"""
|
||||
return {
|
||||
"_id": self._id if not json_compatible else str(self._id),
|
||||
"event_id": self.event_id if not json_compatible else str(self.event_id),
|
||||
"event_id": (
|
||||
self.event_id
|
||||
if not json_compatible
|
||||
else (None if self.event_id is None else str(self.event_id))
|
||||
),
|
||||
"guild_id": self.guild_id,
|
||||
"sequence": self.sequence,
|
||||
"created": self.created,
|
||||
"created": self.created if not json_compatible else self.created.isoformat(),
|
||||
"creator_id": self.creator_id,
|
||||
"question": self.question,
|
||||
"answer": self.answer,
|
||||
"media": self.media,
|
||||
}
|
||||
|
||||
# TODO Add documentation
|
||||
@staticmethod
|
||||
def get_defaults() -> Dict[str, Any]:
|
||||
"""Get default values for the object attributes.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Mapping of attributes and their respective values in format `{"attribute_name:" attribute_value}`.
|
||||
"""
|
||||
return {
|
||||
"event_id": None,
|
||||
"guild_id": None,
|
||||
@@ -206,38 +187,40 @@ class PycordEventStage:
|
||||
"media": [],
|
||||
}
|
||||
|
||||
# TODO Add documentation
|
||||
@staticmethod
|
||||
def get_default_value(key: str) -> Any:
|
||||
"""Get default value of the attribute for the object.
|
||||
|
||||
Args:
|
||||
key (str): Name of the attribute.
|
||||
|
||||
Returns:
|
||||
Any: Default value of the attribute.
|
||||
|
||||
Raises:
|
||||
KeyError: There's no default value for the provided attribute.
|
||||
"""
|
||||
if key not in PycordEventStage.get_defaults():
|
||||
raise KeyError(f"There's no default value for key '{key}' in PycordEventStage")
|
||||
|
||||
return PycordEventStage.get_defaults()[key]
|
||||
|
||||
# TODO Add documentation
|
||||
async def update(
|
||||
self,
|
||||
cache: Optional[Cache] = None,
|
||||
**kwargs,
|
||||
):
|
||||
await self._set(cache=cache, **kwargs)
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
await super().update(cache=cache, **kwargs)
|
||||
|
||||
# TODO Add documentation
|
||||
async def reset(
|
||||
self,
|
||||
*args: str,
|
||||
cache: Optional[Cache] = None,
|
||||
*args,
|
||||
):
|
||||
await self._remove(cache, *args)
|
||||
) -> None:
|
||||
await super().reset(*args, cache=cache)
|
||||
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
"""Completely remove event stage data from database. Currently only removes the event stage record from events 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)
|
||||
await super().purge(cache)
|
||||
|
||||
# TODO Add documentation
|
||||
def get_media_files(self) -> List[File] | None:
|
||||
|
@@ -6,6 +6,7 @@ from bson import ObjectId
|
||||
from libbot.cache.classes import Cache
|
||||
from pymongo.results import InsertOneResult
|
||||
|
||||
from classes.base.base_cacheable import BaseCacheable
|
||||
from classes.errors import GuildNotFoundError
|
||||
from modules.database import col_guilds
|
||||
from modules.utils import get_logger, restore_from_cache
|
||||
@@ -14,16 +15,25 @@ logger: Logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PycordGuild:
|
||||
class PycordGuild(BaseCacheable):
|
||||
"""Dataclass of DB entry of a guild"""
|
||||
|
||||
__slots__ = ("_id", "id", "channel_id", "category_id", "timezone", "prefer_emojis")
|
||||
__slots__ = (
|
||||
"_id",
|
||||
"id",
|
||||
"general_channel_id",
|
||||
"management_channel_id",
|
||||
"category_id",
|
||||
"timezone",
|
||||
"prefer_emojis",
|
||||
)
|
||||
__short_name__ = "guild"
|
||||
__collection__ = col_guilds
|
||||
|
||||
_id: ObjectId
|
||||
id: int
|
||||
channel_id: int | None
|
||||
general_channel_id: int | None
|
||||
management_channel_id: int | None
|
||||
category_id: int | None
|
||||
timezone: str
|
||||
prefer_emojis: bool
|
||||
@@ -32,25 +42,25 @@ class PycordGuild:
|
||||
async def from_id(
|
||||
cls, guild_id: int, allow_creation: bool = True, cache: Optional[Cache] = None
|
||||
) -> "PycordGuild":
|
||||
"""Find guild in database and create new record if guild does not exist.
|
||||
"""Find the guild by its ID and construct PycordEventStage from database entry.
|
||||
|
||||
Args:
|
||||
guild_id (int): User's Discord ID
|
||||
allow_creation (:obj:`bool`, optional): Create new guild record if none found in the database
|
||||
cache (:obj:`Cache`, optional): Cache engine to get the cache from
|
||||
guild_id (int): ID of the guild to look up.
|
||||
allow_creation (:obj:`bool`, optional): Create a new record if none found in the database.
|
||||
cache (:obj:`Cache`, optional): Cache engine that will be used to fetch and update the cache.
|
||||
|
||||
Returns:
|
||||
PycordGuild: User object
|
||||
PycordGuild: Object of the found or newly created guild.
|
||||
|
||||
Raises:
|
||||
GuildNotFoundError: User was not found and creation was not allowed
|
||||
GuildNotFoundError: Guild with such ID does not exist and creation was not allowed.
|
||||
"""
|
||||
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)
|
||||
return cls(**cls._entry_from_cache(cached_entry))
|
||||
|
||||
db_entry = await cls.__collection__.find_one({"id": guild_id})
|
||||
db_entry: Dict[str, Any] | None = await cls.__collection__.find_one({"id": guild_id})
|
||||
|
||||
if db_entry is None:
|
||||
if not allow_creation:
|
||||
@@ -63,98 +73,71 @@ class PycordGuild:
|
||||
db_entry["_id"] = insert_result.inserted_id
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{guild_id}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{guild_id}", cls._entry_to_cache(db_entry))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
async def _set(self, cache: Optional[Cache] = None, **kwargs: Any) -> None:
|
||||
"""Set attribute data and save it into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
**kwargs (Any): Mapping of attribute names and respective values to be set
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
await self.__collection__.update_one({"_id": self._id}, {"$set": kwargs}, upsert=True)
|
||||
|
||||
self._update_cache(cache)
|
||||
|
||||
logger.info("Set attributes of guild %s to %s", self.id, kwargs)
|
||||
await super()._set(cache, **kwargs)
|
||||
|
||||
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
|
||||
"""Remove attribute data and save it into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
*args (str): List of attributes to remove
|
||||
"""
|
||||
attributes: Dict[str, Any] = {}
|
||||
|
||||
for key in args:
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
default_value: Any = self.get_default_value(key)
|
||||
|
||||
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("Reset attributes %s of guild %s to default values", args, self.id)
|
||||
await super()._remove(*args, cache=cache)
|
||||
|
||||
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)
|
||||
super()._update_cache(cache)
|
||||
|
||||
def _delete_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
if cache is None:
|
||||
return
|
||||
super()._delete_cache(cache)
|
||||
|
||||
cache.delete(self._get_cache_key())
|
||||
@staticmethod
|
||||
def _entry_to_cache(db_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
cache_entry: Dict[str, Any] = db_entry.copy()
|
||||
|
||||
cache_entry["_id"] = str(cache_entry["_id"])
|
||||
|
||||
return cache_entry
|
||||
|
||||
@staticmethod
|
||||
def _entry_from_cache(cache_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_entry: Dict[str, Any] = cache_entry.copy()
|
||||
|
||||
db_entry["_id"] = ObjectId(db_entry["_id"])
|
||||
|
||||
return db_entry
|
||||
|
||||
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
|
||||
"""Convert PycordGuild object to a JSON representation.
|
||||
"""Convert the object to a JSON representation.
|
||||
|
||||
Args:
|
||||
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted
|
||||
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: JSON representation of PycordGuild
|
||||
Dict[str, Any]: JSON representation of the object.
|
||||
"""
|
||||
return {
|
||||
"_id": self._id if not json_compatible else str(self._id),
|
||||
"id": self.id,
|
||||
"channel_id": self.channel_id,
|
||||
"general_channel_id": self.general_channel_id,
|
||||
"management_channel_id": self.management_channel_id,
|
||||
"category_id": self.category_id,
|
||||
"timezone": self.timezone,
|
||||
"prefer_emojis": self.prefer_emojis,
|
||||
}
|
||||
|
||||
# TODO Add documentation
|
||||
@staticmethod
|
||||
def get_defaults(guild_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Get default values for the object attributes.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Mapping of attributes and their respective values in format `{"attribute_name:" attribute_value}`.
|
||||
"""
|
||||
return {
|
||||
"id": guild_id,
|
||||
"channel_id": None,
|
||||
"general_channel_id": None,
|
||||
"management_channel_id": None,
|
||||
"category_id": None,
|
||||
"timezone": "UTC",
|
||||
"prefer_emojis": False,
|
||||
@@ -162,68 +145,50 @@ class PycordGuild:
|
||||
|
||||
@staticmethod
|
||||
def get_default_value(key: str) -> Any:
|
||||
"""Get default value of the attribute for the object.
|
||||
|
||||
Args:
|
||||
key (str): Name of the attribute.
|
||||
|
||||
Returns:
|
||||
Any: Default value of the attribute.
|
||||
|
||||
Raises:
|
||||
KeyError: There's no default value for the provided attribute.
|
||||
"""
|
||||
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]
|
||||
|
||||
# TODO Add documentation
|
||||
async def update(
|
||||
self,
|
||||
cache: Optional[Cache] = None,
|
||||
**kwargs,
|
||||
):
|
||||
await self._set(cache=cache, **kwargs)
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
await super().update(cache=cache, **kwargs)
|
||||
|
||||
# TODO Add documentation
|
||||
async def reset(
|
||||
self,
|
||||
*args: str,
|
||||
cache: Optional[Cache] = None,
|
||||
*args,
|
||||
):
|
||||
await self._remove(cache, *args)
|
||||
) -> None:
|
||||
await super().reset(*args, cache=cache)
|
||||
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
"""Completely remove guild data from database. Currently only removes the guild record from guilds collection.
|
||||
await super().purge(cache)
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
"""
|
||||
await self.__collection__.delete_one({"_id": self._id})
|
||||
|
||||
self._delete_cache(cache)
|
||||
|
||||
logger.info("Purged guild %s (%s) from the database", self.id, self._id)
|
||||
|
||||
# TODO Add documentation
|
||||
def is_configured(self) -> bool:
|
||||
"""Return whether all attributes required for bot's use on the server are set.
|
||||
|
||||
Returns:
|
||||
bool: `True` if yes and `False` if not.
|
||||
"""
|
||||
return (
|
||||
(self.id is not None)
|
||||
and (self.channel_id is not None)
|
||||
and (self.general_channel_id is not None)
|
||||
and (self.management_channel_id is not None)
|
||||
and (self.category_id is not None)
|
||||
and (self.timezone is not None)
|
||||
and (self.prefer_emojis is not None)
|
||||
)
|
||||
|
||||
# TODO Add documentation
|
||||
async def set_channel(self, channel_id: Optional[int] = None, cache: Optional[Cache] = None) -> None:
|
||||
await self._set(cache, channel_id=channel_id)
|
||||
|
||||
# TODO Add documentation
|
||||
async def reset_channel(self, cache: Optional[Cache] = None) -> None:
|
||||
await self._remove(cache, "channel_id")
|
||||
|
||||
# TODO Add documentation
|
||||
async def set_category(self, category_id: Optional[int] = None, cache: Optional[Cache] = None) -> None:
|
||||
await self._set(cache, category_id=category_id)
|
||||
|
||||
# TODO Add documentation
|
||||
async def reset_category(self, cache: Optional[Cache] = None) -> None:
|
||||
await self._remove(cache, "category_id")
|
||||
|
||||
# TODO Add documentation
|
||||
async def set_timezone(self, timezone: str, cache: Optional[Cache] = None) -> None:
|
||||
await self._set(cache, timezone=timezone)
|
||||
|
||||
# TODO Add documentation
|
||||
async def reset_timezone(self, cache: Optional[Cache] = None) -> None:
|
||||
await self._remove(cache, "timezone")
|
||||
|
@@ -17,6 +17,7 @@ from discord.abc import GuildChannel
|
||||
from libbot.cache.classes import Cache
|
||||
from pymongo.results import InsertOneResult
|
||||
|
||||
from classes.base.base_cacheable import BaseCacheable
|
||||
from classes.errors import (
|
||||
DiscordCategoryNotFoundError,
|
||||
DiscordChannelNotFoundError,
|
||||
@@ -33,7 +34,7 @@ logger: Logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PycordUser:
|
||||
class PycordUser(BaseCacheable):
|
||||
"""Dataclass of DB entry of a user"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -60,11 +61,6 @@ class PycordUser:
|
||||
registered_event_ids: List[ObjectId]
|
||||
completed_event_ids: List[ObjectId]
|
||||
|
||||
# TODO Review the redesign
|
||||
# event_channel_ids: {
|
||||
# "%event_id%": %channel_id%
|
||||
# }
|
||||
|
||||
@classmethod
|
||||
async def from_id(
|
||||
cls, user_id: int, guild_id: int, allow_creation: bool = True, cache: Optional[Cache] = None
|
||||
@@ -88,9 +84,11 @@ class PycordUser:
|
||||
)
|
||||
|
||||
if cached_entry is not None:
|
||||
return cls(**cached_entry)
|
||||
return cls(**cls._entry_from_cache(cached_entry))
|
||||
|
||||
db_entry = await cls.__collection__.find_one({"id": user_id, "guild_id": guild_id})
|
||||
db_entry: Dict[str, Any] | None = await cls.__collection__.find_one(
|
||||
{"id": user_id, "guild_id": guild_id}
|
||||
)
|
||||
|
||||
if db_entry is None:
|
||||
if not allow_creation:
|
||||
@@ -103,7 +101,7 @@ class PycordUser:
|
||||
db_entry["_id"] = insert_result.inserted_id
|
||||
|
||||
if cache is not None:
|
||||
cache.set_json(f"{cls.__short_name__}_{user_id}_{guild_id}", db_entry)
|
||||
cache.set_json(f"{cls.__short_name__}_{user_id}_{guild_id}", cls._entry_to_cache(db_entry))
|
||||
|
||||
return cls(**db_entry)
|
||||
|
||||
@@ -123,10 +121,14 @@ class PycordUser:
|
||||
"event_channels": self.event_channels,
|
||||
"is_jailed": self.is_jailed,
|
||||
"current_event_id": (
|
||||
self.current_event_id if not json_compatible else str(self.current_event_id)
|
||||
self.current_event_id
|
||||
if not json_compatible
|
||||
else (None if self.current_event_id is None else str(self.current_event_id))
|
||||
),
|
||||
"current_stage_id": (
|
||||
self.current_stage_id if not json_compatible else str(self.current_stage_id)
|
||||
self.current_stage_id
|
||||
if not json_compatible
|
||||
else (None if self.current_stage_id is None else str(self.current_stage_id))
|
||||
),
|
||||
"registered_event_ids": (
|
||||
self.registered_event_ids
|
||||
@@ -141,68 +143,59 @@ class PycordUser:
|
||||
}
|
||||
|
||||
async def _set(self, cache: Optional[Cache] = None, **kwargs: Any) -> None:
|
||||
"""Set attribute data and save it into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
**kwargs (Any): Mapping of attribute names and respective values to be set
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
await self.__collection__.update_one({"_id": self._id}, {"$set": kwargs}, upsert=True)
|
||||
|
||||
self._update_cache(cache)
|
||||
|
||||
logger.info("Set attributes of user %s to %s", self.id, kwargs)
|
||||
await super()._set(cache, **kwargs)
|
||||
|
||||
async def _remove(self, *args: str, cache: Optional[Cache] = None) -> None:
|
||||
"""Remove attribute data and save it into the database.
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
*args (str): List of attributes to remove
|
||||
"""
|
||||
attributes: Dict[str, Any] = {}
|
||||
|
||||
for key in args:
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError()
|
||||
|
||||
default_value: Any = self.get_default_value(key)
|
||||
|
||||
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("Reset attributes %s of user %s to default values", args, self.id)
|
||||
await super()._remove(*args, cache=cache)
|
||||
|
||||
def _get_cache_key(self) -> str:
|
||||
return f"{self.__short_name__}_{self.id}"
|
||||
return f"{self.__short_name__}_{self.id}_{self.guild_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)
|
||||
super()._update_cache(cache)
|
||||
|
||||
def _delete_cache(self, cache: Optional[Cache] = None) -> None:
|
||||
if cache is None:
|
||||
return
|
||||
super()._delete_cache(cache)
|
||||
|
||||
cache.delete(self._get_cache_key())
|
||||
@staticmethod
|
||||
def _entry_to_cache(db_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
cache_entry: Dict[str, Any] = db_entry.copy()
|
||||
|
||||
cache_entry["_id"] = str(cache_entry["_id"])
|
||||
cache_entry["current_event_id"] = (
|
||||
None if cache_entry["current_event_id"] is None else str(cache_entry["current_event_id"])
|
||||
)
|
||||
cache_entry["current_stage_id"] = (
|
||||
None if cache_entry["current_stage_id"] is None else str(cache_entry["current_stage_id"])
|
||||
)
|
||||
cache_entry["registered_event_ids"] = [
|
||||
str(event_id) for event_id in cache_entry["registered_event_ids"]
|
||||
]
|
||||
cache_entry["completed_event_ids"] = [
|
||||
str(event_id) for event_id in cache_entry["completed_event_ids"]
|
||||
]
|
||||
|
||||
return cache_entry
|
||||
|
||||
@staticmethod
|
||||
def _entry_from_cache(cache_entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_entry: Dict[str, Any] = cache_entry.copy()
|
||||
|
||||
db_entry["_id"] = ObjectId(db_entry["_id"])
|
||||
db_entry["current_event_id"] = (
|
||||
None if db_entry["current_event_id"] is None else ObjectId(db_entry["current_event_id"])
|
||||
)
|
||||
db_entry["current_stage_id"] = (
|
||||
None if db_entry["current_stage_id"] is None else ObjectId(db_entry["current_stage_id"])
|
||||
)
|
||||
db_entry["registered_event_ids"] = [
|
||||
ObjectId(event_id) for event_id in db_entry["registered_event_ids"]
|
||||
]
|
||||
db_entry["completed_event_ids"] = [
|
||||
ObjectId(event_id) for event_id in db_entry["completed_event_ids"]
|
||||
]
|
||||
|
||||
return db_entry
|
||||
|
||||
# TODO Add documentation
|
||||
@staticmethod
|
||||
@@ -226,14 +219,22 @@ class PycordUser:
|
||||
|
||||
return PycordUser.get_defaults()[key]
|
||||
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
"""Completely remove user data from database. Currently only removes the user record from users collection.
|
||||
async def update(
|
||||
self,
|
||||
cache: Optional[Cache] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
await super().update(cache=cache, **kwargs)
|
||||
|
||||
Args:
|
||||
cache (:obj:`Cache`, optional): Cache engine to write the update into
|
||||
"""
|
||||
await self.__collection__.delete_one({"_id": self._id})
|
||||
self._delete_cache(cache)
|
||||
async def reset(
|
||||
self,
|
||||
*args: str,
|
||||
cache: Optional[Cache] = None,
|
||||
) -> None:
|
||||
await super().reset(*args, cache=cache)
|
||||
|
||||
async def purge(self, cache: Optional[Cache] = None) -> None:
|
||||
await super().purge(cache)
|
||||
|
||||
# TODO Add documentation
|
||||
async def event_register(self, event_id: str | ObjectId, cache: Optional[Cache] = None) -> None:
|
||||
@@ -288,7 +289,7 @@ class PycordUser:
|
||||
raise DiscordGuildMemberNotFoundError(self.id, guild.id)
|
||||
|
||||
if discord_category is None:
|
||||
raise DiscordCategoryNotFoundError(pycord_guild.channel_id, guild.id)
|
||||
raise DiscordCategoryNotFoundError(pycord_guild.category_id, guild.id)
|
||||
|
||||
permission_overwrites: Dict[Role | Member, PermissionOverwrite] = {
|
||||
guild.default_role: PermissionOverwrite(
|
||||
|
@@ -44,10 +44,18 @@ class CogConfig(Cog):
|
||||
required=True,
|
||||
)
|
||||
@option(
|
||||
"channel",
|
||||
description=_("description", "commands", "config_set", "options", "channel"),
|
||||
"general_channel",
|
||||
description=_("description", "commands", "config_set", "options", "general_channel"),
|
||||
description_localizations=in_every_locale(
|
||||
"description", "commands", "config_set", "options", "channel"
|
||||
"description", "commands", "config_set", "options", "general_channel"
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
@option(
|
||||
"management_channel",
|
||||
description=_("description", "commands", "config_set", "options", "management_channel"),
|
||||
description_localizations=in_every_locale(
|
||||
"description", "commands", "config_set", "options", "management_channel"
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
@@ -72,27 +80,30 @@ class CogConfig(Cog):
|
||||
self,
|
||||
ctx: ApplicationContext,
|
||||
category: CategoryChannel,
|
||||
channel: TextChannel,
|
||||
general_channel: TextChannel,
|
||||
management_channel: TextChannel,
|
||||
timezone: str,
|
||||
prefer_emojis: bool,
|
||||
) -> None:
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
timezone_parsed: ZoneInfo = ZoneInfo(timezone)
|
||||
except ZoneInfoNotFoundError:
|
||||
await ctx.respond(
|
||||
self.bot._("timezone_invalid", "messages", locale=ctx.locale).format(timezone=timezone)
|
||||
self.bot._("timezone_invalid", "messages", locale=ctx.locale).format(timezone=timezone),
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
await guild.update(
|
||||
self.bot.cache,
|
||||
channel_id=channel.id,
|
||||
general_channel_id=general_channel.id,
|
||||
management_channel_id=management_channel.id,
|
||||
category_id=category.id,
|
||||
timezone=str(timezone_parsed),
|
||||
prefer_emojis=prefer_emojis,
|
||||
@@ -120,7 +131,7 @@ class CogConfig(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
await guild.purge(self.bot.cache)
|
||||
@@ -136,16 +147,19 @@ class CogConfig(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
await ctx.respond(
|
||||
self.bot._("config_show", "messages", locale=ctx.locale).format(
|
||||
channel_id=guild.channel_id,
|
||||
general_channel_id=guild.general_channel_id,
|
||||
management_channel_id=guild.management_channel_id,
|
||||
category_id=guild.category_id,
|
||||
timezone=guild.timezone,
|
||||
prefer_emojis=guild.prefer_emojis,
|
||||
|
@@ -84,11 +84,13 @@ class CogEvent(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
guild_timezone: ZoneInfo = ZoneInfo(guild.timezone)
|
||||
@@ -97,7 +99,9 @@ class CogEvent(Cog):
|
||||
start_date: datetime = datetime.strptime(start, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
|
||||
end_date: datetime = datetime.strptime(end, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
|
||||
except ValueError:
|
||||
await ctx.respond(self.bot._("event_dates_parsing_failed", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("event_dates_parsing_failed", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if not await validate_event_validity(ctx, name, start_date, end_date, to_utc=True):
|
||||
@@ -180,7 +184,7 @@ class CogEvent(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -190,7 +194,9 @@ class CogEvent(Cog):
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
guild_timezone: ZoneInfo = ZoneInfo(guild.timezone)
|
||||
@@ -202,7 +208,9 @@ class CogEvent(Cog):
|
||||
else datetime.strptime(start, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
|
||||
)
|
||||
except ValueError:
|
||||
await ctx.respond(self.bot._("event_start_date_parsing_failed", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("event_start_date_parsing_failed", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -212,7 +220,9 @@ class CogEvent(Cog):
|
||||
else datetime.strptime(end, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
|
||||
)
|
||||
except ValueError:
|
||||
await ctx.respond(self.bot._("event_end_date_parsing_failed", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("event_end_date_parsing_failed", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if not await validate_event_validity(
|
||||
@@ -280,7 +290,7 @@ class CogEvent(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -290,7 +300,9 @@ class CogEvent(Cog):
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
start_date: datetime = pycord_event.starts.replace(tzinfo=ZoneInfo("UTC"))
|
||||
@@ -305,7 +317,8 @@ class CogEvent(Cog):
|
||||
await ctx.respond(
|
||||
self.bot._("event_not_editable", "messages", locale=ctx.locale).format(
|
||||
event_name=pycord_event.name
|
||||
)
|
||||
),
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
|
@@ -34,11 +34,13 @@ class CogGuess(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
user: PycordUser = await self.bot.find_user(ctx.author, ctx.guild)
|
||||
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from bson.errors import InvalidId
|
||||
from discord import ApplicationContext, Cog, TextChannel, option, slash_command, File
|
||||
from discord import ApplicationContext, Cog, File, TextChannel, option, slash_command
|
||||
from discord.utils import basic_autocomplete
|
||||
from libbot.i18n import _, in_every_locale
|
||||
|
||||
@@ -39,7 +39,7 @@ class CogRegister(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -49,17 +49,21 @@ class CogRegister(Cog):
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
user: PycordUser = await self.bot.find_user(ctx.author, ctx.guild)
|
||||
|
||||
if user.is_jailed:
|
||||
await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if pycord_event._id in user.registered_event_ids:
|
||||
await ctx.respond(self.bot._("register_already_registered", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("register_already_registered", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
await user.event_register(pycord_event._id, cache=self.bot.cache)
|
||||
|
@@ -84,11 +84,13 @@ class CogStage(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -196,11 +198,13 @@ class CogStage(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -215,11 +219,13 @@ class CogStage(Cog):
|
||||
try:
|
||||
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
|
||||
except (InvalidId, EventStageNotFoundError):
|
||||
await ctx.respond(self.bot._("stage_not_found", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("stage_not_found", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if order is not None and order > len(pycord_event.stage_ids):
|
||||
await ctx.respond(self.bot._("stage_sequence_out_of_range", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("stage_sequence_out_of_range", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
processed_media: List[Dict[str, Any]] = (
|
||||
@@ -278,11 +284,13 @@ class CogStage(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -297,7 +305,7 @@ class CogStage(Cog):
|
||||
try:
|
||||
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
|
||||
except (InvalidId, EventStageNotFoundError):
|
||||
await ctx.respond(self.bot._("stage_not_found", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("stage_not_found", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
await pycord_event.remove_stage(self.bot, event_stage._id, cache=self.bot.cache)
|
||||
|
@@ -43,7 +43,7 @@ class CogUnregister(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -53,17 +53,21 @@ class CogUnregister(Cog):
|
||||
return
|
||||
|
||||
if not guild.is_configured():
|
||||
await ctx.respond(self.bot._("guild_unconfigured", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("guild_unconfigured", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
user: PycordUser = await self.bot.find_user(ctx.author, ctx.guild)
|
||||
|
||||
if user.is_jailed:
|
||||
await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
if pycord_event._id not in user.registered_event_ids:
|
||||
await ctx.respond(self.bot._("unregister_not_registered", "messages", locale=ctx.locale))
|
||||
await ctx.respond(
|
||||
self.bot._("unregister_not_registered", "messages", locale=ctx.locale), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
await user.event_unregister(pycord_event._id, cache=self.bot.cache)
|
||||
|
@@ -1,27 +1,27 @@
|
||||
from datetime import datetime
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from typing import Any, Dict, List
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from bson import ObjectId
|
||||
from bson.errors import InvalidId
|
||||
from discord import (
|
||||
ApplicationContext,
|
||||
File,
|
||||
SlashCommandGroup,
|
||||
TextChannel,
|
||||
User,
|
||||
option,
|
||||
File,
|
||||
TextChannel,
|
||||
)
|
||||
from discord.ext.commands import Cog
|
||||
from libbot.i18n import _, in_every_locale
|
||||
|
||||
from classes import PycordUser, PycordEvent, PycordGuild
|
||||
from classes import PycordEvent, PycordGuild, PycordUser
|
||||
from classes.errors import GuildNotFoundError
|
||||
from classes.pycord_bot import PycordBot
|
||||
from modules.database import col_users
|
||||
from modules.utils import is_operation_confirmed, get_logger
|
||||
from modules.utils import get_logger, get_utc_now, is_operation_confirmed
|
||||
|
||||
logger: Logger = get_logger(__name__)
|
||||
|
||||
@@ -54,35 +54,45 @@ class CogUser(Cog):
|
||||
try:
|
||||
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
|
||||
except (InvalidId, GuildNotFoundError):
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale))
|
||||
await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return
|
||||
|
||||
pycord_user: PycordUser = await self.bot.find_user(user.id, ctx.guild.id)
|
||||
events: List[PycordEvent] = []
|
||||
|
||||
utc_now: datetime = get_utc_now()
|
||||
|
||||
pipeline: List[Dict[str, Any]] = [
|
||||
{"$match": {"id": pycord_user.id}},
|
||||
{
|
||||
"$lookup": {
|
||||
"from": "events",
|
||||
"localField": "registered_event_ids",
|
||||
"foreignField": "_id",
|
||||
"let": {"event_ids": "$registered_event_ids"},
|
||||
"pipeline": [
|
||||
{
|
||||
"$match": {
|
||||
"$expr": {
|
||||
"$and": [
|
||||
{"$in": ["$_id", "$$event_ids"]},
|
||||
{"$eq": ["$ended", None]},
|
||||
{"$gt": ["$ends", utc_now]},
|
||||
{"$lt": ["$starts", utc_now]},
|
||||
{"$eq": ["$is_cancelled", False]},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"as": "registered_events",
|
||||
}
|
||||
},
|
||||
{
|
||||
"$match": {
|
||||
"registered_events.ended": None,
|
||||
"registered_events.ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
|
||||
"registered_events.starts": {"$lt": datetime.now(tz=ZoneInfo("UTC"))},
|
||||
"registered_events.is_cancelled": False,
|
||||
}
|
||||
},
|
||||
{"$match": {"registered_events.0": {"$exists": True}}},
|
||||
]
|
||||
|
||||
async for result in col_users.aggregate(pipeline):
|
||||
for registered_event in result["registered_events"]:
|
||||
events.append(PycordEvent(**registered_event))
|
||||
async with await col_users.aggregate(pipeline) as cursor:
|
||||
async for result in cursor:
|
||||
for registered_event in result["registered_events"]:
|
||||
events.append(PycordEvent(**registered_event))
|
||||
|
||||
for event in events:
|
||||
if pycord_user.current_event_id is not None and pycord_user.current_event_id != event._id:
|
||||
|
@@ -6,13 +6,13 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
from bson import ObjectId
|
||||
from bson.errors import InvalidId
|
||||
from discord import Activity, ActivityType, Cog, Member, TextChannel, File
|
||||
from discord import Activity, ActivityType, Cog, File, Member, TextChannel
|
||||
|
||||
from classes import PycordEvent, PycordGuild, PycordUser
|
||||
from classes.errors import GuildNotFoundError
|
||||
from classes.pycord_bot import PycordBot
|
||||
from modules.database import col_users
|
||||
from modules.utils import get_logger
|
||||
from modules.utils import get_logger, get_utc_now
|
||||
|
||||
logger: Logger = get_logger(__name__)
|
||||
|
||||
@@ -78,29 +78,39 @@ class CogUtility(Cog):
|
||||
user: PycordUser = await self.bot.find_user(member.id, member.guild.id)
|
||||
events: List[PycordEvent] = []
|
||||
|
||||
utc_now: datetime = get_utc_now()
|
||||
|
||||
pipeline: List[Dict[str, Any]] = [
|
||||
{"$match": {"id": user.id}},
|
||||
{
|
||||
"$lookup": {
|
||||
"from": "events",
|
||||
"localField": "registered_event_ids",
|
||||
"foreignField": "_id",
|
||||
"let": {"event_ids": "$registered_event_ids"},
|
||||
"pipeline": [
|
||||
{
|
||||
"$match": {
|
||||
"$expr": {
|
||||
"$and": [
|
||||
{"$in": ["$_id", "$$event_ids"]},
|
||||
{"$eq": ["$ended", None]},
|
||||
{"$gt": ["$ends", utc_now]},
|
||||
{"$lt": ["$starts", utc_now]},
|
||||
{"$eq": ["$is_cancelled", False]},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"as": "registered_events",
|
||||
}
|
||||
},
|
||||
{
|
||||
"$match": {
|
||||
"registered_events.ended": None,
|
||||
"registered_events.ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
|
||||
"registered_events.starts": {"$lt": datetime.now(tz=ZoneInfo("UTC"))},
|
||||
"registered_events.is_cancelled": False,
|
||||
}
|
||||
},
|
||||
{"$match": {"registered_events.0": {"$exists": True}}},
|
||||
]
|
||||
|
||||
async for result in col_users.aggregate(pipeline):
|
||||
for registered_event in result["registered_events"]:
|
||||
events.append(PycordEvent(**registered_event))
|
||||
async with await col_users.aggregate(pipeline) as cursor:
|
||||
async for result in cursor:
|
||||
for registered_event in result["registered_events"]:
|
||||
events.append(PycordEvent(**registered_event))
|
||||
|
||||
for event in events:
|
||||
if user.current_event_id is not None and user.current_event_id != event._id:
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"admin_user_completed_stage": "User **{display_name}** ({mention}) has completed the stage {stage_sequence} of the event **{event_name}**.",
|
||||
"config_reset": "Configuration has been reset. You can update it using `/config set`, otherwise no events can be held.",
|
||||
"config_set": "Configuration has been updated. You can review it anytime using `/config show`.",
|
||||
"config_show": "**Guild config**\n\nChannel: <#{channel_id}>\nCategory: <#{category_id}>\nTimezone: `{timezone}`\nPrefer emojis: `{prefer_emojis}`",
|
||||
"config_show": "**Guild config**\n\nCategory: <#{category_id}>\nGeneral channel: <#{general_channel_id}>\nManagement channel: <#{management_channel_id}>\nTimezone: `{timezone}`\nPrefer emojis: `{prefer_emojis}`",
|
||||
"event_cancelled": "Event **{event_name}** was cancelled.",
|
||||
"event_created": "Event **{event_name}** has been created and will take place <t:{start_time}:R>.",
|
||||
"event_dates_parsing_failed": "Could not parse start and end dates. Please, make sure these are provided in `DD.MM.YYYY HH:MM` format.",
|
||||
@@ -20,6 +20,7 @@
|
||||
"event_end_date_parsing_failed": "Could not parse the end date. Please, make sure it is provided in `DD.MM.YYYY HH:MM` format.",
|
||||
"event_end_past": "End date must not be in the past",
|
||||
"event_ended": "Event **{event_name}** has ended! Stages and respective answers are listed below.\n\n{stages}",
|
||||
"event_ended_short": "Event **{event_name}** has ended! Stages and respective answers are listed below.",
|
||||
"event_is_cancelled": "This event was cancelled.",
|
||||
"event_is_starting": "Event **{event_name}** is starting!\n\nUse slash command `/guess` to suggest your answers to each event stage.",
|
||||
"event_name_duplicate": "There can only be one active event with the same name",
|
||||
@@ -28,8 +29,9 @@
|
||||
"event_ongoing_not_editable": "Ongoing events cannot be modified.",
|
||||
"event_start_date_parsing_failed": "Could not parse the start date. Please, make sure it is provided in `DD.MM.YYYY HH:MM` format.",
|
||||
"event_start_past": "Start date must not be in the past",
|
||||
"event_started": "Event **{event_name}** has started! Use command `/register` to participate in the event.",
|
||||
"event_updated": "Event **{event_name}** has been updated and will take place <t:{start_time}:R>.",
|
||||
"guess_completed_event": "Congratulations! You have completed the event!",
|
||||
"guess_completed_event": "Congratulations! You have completed the event!\nPlease, do not share the answers with others until the event ends so that everyone can have fun. Thank you!",
|
||||
"guess_incorrect": "Provided answer is wrong.",
|
||||
"guess_incorrect_channel": "Usage outside own event channel is not allowed.",
|
||||
"guess_incorrect_event": "Your event could not be found. Please, contact the administrator.",
|
||||
@@ -45,6 +47,8 @@
|
||||
"stage_created": "Event stage has been created.",
|
||||
"stage_deleted": "Event stage has been deleted.",
|
||||
"stage_entry": "**Stage {sequence}**\nAnswer: ||{answer}||",
|
||||
"stage_entry_footer": "Answer: ||{answer}||",
|
||||
"stage_entry_header": "**Stage {sequence}**\nQuestion: {question}",
|
||||
"stage_not_found": "Event stage was not found.",
|
||||
"stage_sequence_out_of_range": "Stage sequence out of range.",
|
||||
"stage_updated": "Event stage has been updated.",
|
||||
@@ -70,7 +74,10 @@
|
||||
"category": {
|
||||
"description": "Category where channels for each user will be created"
|
||||
},
|
||||
"channel": {
|
||||
"general_channel": {
|
||||
"description": "Text channel for general notifications and bot usage"
|
||||
},
|
||||
"management_channel": {
|
||||
"description": "Text channel for admin notifications"
|
||||
},
|
||||
"timezone": {
|
||||
|
1
main.py
1
main.py
@@ -45,6 +45,7 @@ def main() -> None:
|
||||
# Perform migration if command line argument was provided
|
||||
if args.migrate:
|
||||
migrate_database()
|
||||
exit()
|
||||
|
||||
# if args.downgrade:
|
||||
# if not args.confirm:
|
||||
|
59
migrations/202505192040.py
Normal file
59
migrations/202505192040.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from mongodb_migrations.base import BaseMigration
|
||||
|
||||
|
||||
class Migration(BaseMigration):
|
||||
def upgrade(self):
|
||||
index_information_users: Dict[str, Any] = self.db.users.index_information()
|
||||
index_information_events: Dict[str, Any] = self.db.events.index_information()
|
||||
index_information_stages: Dict[str, Any] = self.db.stages.index_information()
|
||||
|
||||
# Update users collection
|
||||
if "user_id" in index_information_users:
|
||||
self.db.users.drop_index("user_id")
|
||||
|
||||
if "user_id-guild_id" not in index_information_users:
|
||||
self.db.users.create_index(["id", "guild_id"], name="user_id-guild_id", unique=True)
|
||||
|
||||
# Update events collection
|
||||
if "guild_id" in index_information_events:
|
||||
self.db.events.drop_index("guild_id")
|
||||
|
||||
if "event_name-guild_id" not in index_information_events:
|
||||
self.db.events.create_index(["name", "guild_id"], name="event_name-guild_id", unique=False)
|
||||
|
||||
# Update stages collection
|
||||
if "event_id-and-guild_id" in index_information_stages:
|
||||
self.db.stages.drop_index("event_id-and-guild_id")
|
||||
|
||||
if "event_id-guild_id" not in index_information_stages:
|
||||
self.db.stages.create_index(["event_id", "guild_id"], name="event_id-guild_id", unique=False)
|
||||
|
||||
def downgrade(self):
|
||||
index_information_users: Dict[str, Any] = self.db.users.index_information()
|
||||
index_information_events: Dict[str, Any] = self.db.events.index_information()
|
||||
index_information_stages: Dict[str, Any] = self.db.stages.index_information()
|
||||
|
||||
# Update users collection
|
||||
if "user_id-guild_id" in index_information_users:
|
||||
self.db.users.drop_index("user_id-guild_id")
|
||||
|
||||
if "user_id" not in index_information_users:
|
||||
self.db.users.create_index("id", name="user_id", unique=True)
|
||||
|
||||
# Update events collection
|
||||
if "event_name-guild_id" in index_information_events:
|
||||
self.db.events.drop_index("event_name-guild_id")
|
||||
|
||||
if "guild_id" not in index_information_events:
|
||||
self.db.events.create_index("guild_id", name="guild_id", unique=False)
|
||||
|
||||
# Update stages collection
|
||||
if "event_id-guild_id" in index_information_stages:
|
||||
self.db.stages.drop_index("event_id-guild_id")
|
||||
|
||||
if "event_id-and-guild_id" not in index_information_stages:
|
||||
self.db.stages.create_index(
|
||||
["event_id", "guild_id"], name="event_id-and-guild_id", unique=False
|
||||
)
|
@@ -2,8 +2,10 @@
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase
|
||||
from libbot.utils import config_get
|
||||
from pymongo import AsyncMongoClient
|
||||
from pymongo.asynchronous.collection import AsyncCollection
|
||||
from pymongo.asynchronous.database import AsyncDatabase
|
||||
|
||||
db_config: Mapping[str, Any] = config_get("database")
|
||||
|
||||
@@ -19,7 +21,7 @@ else:
|
||||
con_string = "mongodb://{0}:{1}/{2}".format(db_config["host"], db_config["port"], db_config["name"])
|
||||
|
||||
# Async declarations
|
||||
db_client = AsyncClient(con_string)
|
||||
db_client = AsyncMongoClient(con_string)
|
||||
db: AsyncDatabase = db_client.get_database(name=db_config["name"])
|
||||
|
||||
col_users: AsyncCollection = db.get_collection("users")
|
||||
@@ -27,10 +29,10 @@ col_guilds: AsyncCollection = db.get_collection("guilds")
|
||||
col_events: AsyncCollection = db.get_collection("events")
|
||||
col_stages: AsyncCollection = db.get_collection("stages")
|
||||
|
||||
|
||||
# Update indexes
|
||||
db.dispatch.get_collection("users").create_index("id", name="user_id", unique=True)
|
||||
db.dispatch.get_collection("guilds").create_index("id", name="guild_id", unique=True)
|
||||
db.dispatch.get_collection("events").create_index("guild_id", name="guild_id", unique=False)
|
||||
db.dispatch.get_collection("stages").create_index(
|
||||
["event_id", "guild_id"], name="event_id-and-guild_id", unique=False
|
||||
)
|
||||
async def _update_database_indexes() -> None:
|
||||
await col_users.create_index(["id", "guild_id"], name="user_id-guild_id", unique=True)
|
||||
await col_guilds.create_index("id", name="guild_id", unique=True)
|
||||
await col_events.create_index(["name", "guild_id"], name="event_name-guild_id", unique=False)
|
||||
await col_stages.create_index(["event_id", "guild_id"], name="event_id-guild_id", unique=False)
|
||||
|
@@ -7,7 +7,7 @@ from .autocomplete_utils import (
|
||||
autocomplete_user_registered_events,
|
||||
)
|
||||
from .cache_utils import restore_from_cache
|
||||
from .datetime_utils import get_unix_timestamp
|
||||
from .datetime_utils import get_unix_timestamp, get_utc_now
|
||||
from .event_utils import validate_event_validity
|
||||
from .git_utils import get_current_commit
|
||||
from .logging_utils import get_logger, get_logging_config
|
||||
|
@@ -49,31 +49,41 @@ async def autocomplete_user_available_events(ctx: AutocompleteContext) -> List[O
|
||||
async def autocomplete_user_registered_events(ctx: AutocompleteContext) -> List[OptionChoice]:
|
||||
"""Return list of active events user is registered in"""
|
||||
|
||||
utc_now: datetime = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
pipeline: List[Dict[str, Any]] = [
|
||||
{"$match": {"id": ctx.interaction.user.id}},
|
||||
{
|
||||
"$lookup": {
|
||||
"from": "events",
|
||||
"localField": "registered_event_ids",
|
||||
"foreignField": "_id",
|
||||
"let": {"event_ids": "$registered_event_ids"},
|
||||
"pipeline": [
|
||||
{
|
||||
"$match": {
|
||||
"$expr": {
|
||||
"$and": [
|
||||
{"$in": ["$_id", "$$event_ids"]},
|
||||
{"$eq": ["$ended", None]},
|
||||
{"$gt": ["$ends", utc_now]},
|
||||
{"$gt": ["$starts", utc_now]},
|
||||
{"$eq": ["$is_cancelled", False]},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"as": "registered_events",
|
||||
}
|
||||
},
|
||||
{
|
||||
"$match": {
|
||||
"registered_events.ended": None,
|
||||
"registered_events.ends": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
|
||||
"registered_events.starts": {"$gt": datetime.now(tz=ZoneInfo("UTC"))},
|
||||
"registered_events.is_cancelled": False,
|
||||
}
|
||||
},
|
||||
{"$match": {"registered_events.0": {"$exists": True}}},
|
||||
]
|
||||
|
||||
event_names: List[OptionChoice] = []
|
||||
|
||||
async for result in col_users.aggregate(pipeline):
|
||||
for registered_event in result["registered_events"]:
|
||||
event_names.append(OptionChoice(registered_event["name"], str(registered_event["_id"])))
|
||||
async with await col_users.aggregate(pipeline) as cursor:
|
||||
async for result in cursor:
|
||||
for registered_event in result["registered_events"]:
|
||||
event_names.append(OptionChoice(registered_event["name"], str(registered_event["_id"])))
|
||||
|
||||
return event_names
|
||||
|
||||
|
@@ -5,3 +5,8 @@ from zoneinfo import ZoneInfo
|
||||
# TODO Add documentation
|
||||
def get_unix_timestamp(date: datetime, to_utc: bool = False) -> int:
|
||||
return int((date if not to_utc else date.replace(tzinfo=ZoneInfo("UTC"))).timestamp())
|
||||
|
||||
|
||||
# TODO Add documentation
|
||||
def get_utc_now() -> datetime:
|
||||
return datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
@@ -23,15 +23,15 @@ async def validate_event_validity(
|
||||
end_date_internal: datetime = end_date.astimezone(ZoneInfo("UTC")) if to_utc else end_date
|
||||
|
||||
if start_date_internal < datetime.now(tz=ZoneInfo("UTC")):
|
||||
await ctx.respond(_("event_start_past", "messages", locale=ctx.locale))
|
||||
await ctx.respond(_("event_start_past", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
if end_date_internal < datetime.now(tz=ZoneInfo("UTC")):
|
||||
await ctx.respond(_("event_end_past", "messages", locale=ctx.locale))
|
||||
await ctx.respond(_("event_end_past", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
if start_date_internal >= end_date_internal:
|
||||
await ctx.respond(_("event_end_before_start", "messages", locale=ctx.locale))
|
||||
await ctx.respond(_("event_end_before_start", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
# TODO Add validation for concurrent events.
|
||||
@@ -47,7 +47,7 @@ async def validate_event_validity(
|
||||
query["_id"] = {"$ne": event_id}
|
||||
|
||||
if (await col_events.find_one(query)) is not None:
|
||||
await ctx.respond(_("event_name_duplicate", "messages", locale=ctx.locale))
|
||||
await ctx.respond(_("event_name_duplicate", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@@ -7,7 +7,7 @@ from libbot.i18n import _
|
||||
|
||||
async def is_operation_confirmed(ctx: ApplicationContext, confirm: bool) -> bool:
|
||||
if confirm is None or not confirm:
|
||||
await ctx.respond(ctx.bot._("operation_unconfirmed", "messages", locale=ctx.locale))
|
||||
await ctx.respond(ctx.bot._("operation_unconfirmed", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -18,7 +18,7 @@ async def is_event_status_valid(
|
||||
event: "PycordEvent",
|
||||
) -> bool:
|
||||
if event.is_cancelled:
|
||||
await ctx.respond(_("event_is_cancelled", "messages", locale=ctx.locale))
|
||||
await ctx.respond(_("event_is_cancelled", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
if (
|
||||
@@ -26,7 +26,7 @@ async def is_event_status_valid(
|
||||
<= datetime.now(tz=ZoneInfo("UTC"))
|
||||
<= event.ends.replace(tzinfo=ZoneInfo("UTC"))
|
||||
):
|
||||
await ctx.respond(_("event_ongoing_not_editable", "messages", locale=ctx.locale))
|
||||
await ctx.respond(_("event_ongoing_not_editable", "messages", locale=ctx.locale), ephemeral=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@@ -1,10 +1,10 @@
|
||||
aiodns~=3.2.0
|
||||
apscheduler~=3.11.0
|
||||
async_pymongo==0.1.11
|
||||
brotlipy~=0.7.0
|
||||
faust-cchardet~=2.1.19
|
||||
libbot[speed,pycord,cache]==4.1.0
|
||||
mongodb-migrations==1.3.1
|
||||
msgspec~=0.19.0
|
||||
pymongo~=4.12.1,>=4.9
|
||||
pytz~=2025.1
|
||||
typing_extensions>=4.11.0
|
Reference in New Issue
Block a user