20 Commits

Author SHA1 Message Date
85f1205410 Merge pull request 'v0.3.0' (#28) from dev into main
Reviewed-on: #28
2025-02-16 18:47:09 +02:00
95aecd3c99 Caching will now be used from libbot 2025-02-16 17:44:13 +01:00
afbb36e182 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	cogs/data.py
2025-02-16 12:51:16 +01:00
kku
dae89fd1ac Removed warnings functionality 2025-02-10 12:50:56 +01:00
4fc37f72f3 Merge pull request 'v0.2.0' (#26) from dev into main
Reviewed-on: #26
2025-02-10 13:48:55 +02:00
kku
25f2595cf7 Added a note about a debug setting 2025-02-10 10:21:16 +01:00
a4967e5b77 Merge pull request 'Updated README for caching' (#25) from feature/cache into dev
Reviewed-on: #25
2025-02-10 11:17:47 +02:00
kku
0228983d52 Updated README for caching 2025-02-10 10:15:28 +01:00
39a90d39fd Merge pull request 'Moved create_cache_client() to cache_manager' (#24) from feature/cache into dev
Reviewed-on: #24
2025-02-10 00:45:37 +02:00
9e9b6bc7dc Moved create_cache_client() to cache_manager 2025-02-09 23:45:18 +01:00
d19a7381f3 Merge pull request 'Basic caching support' (#23) from feature/cache into dev
Reviewed-on: #23
2025-02-10 00:41:34 +02:00
cda570eb37 Changed default URI for Redis 2025-02-09 23:40:09 +01:00
a54ef39e53 Added basic Redis support 2025-02-09 23:37:44 +01:00
b3a7e3623a Implemented memcached caching 2025-02-09 23:00:18 +01:00
226721bb62 Updated username to replace discriminator with a username for data export 2025-02-09 20:57:22 +01:00
b9aeaf5c86 Merge pull request 'v0.1.0' (#20) from dev into main
Reviewed-on: #20
2024-12-27 23:38:13 +02:00
6087349622 Merge pull request 'v0.1.0-rc.4' (#19) from dev into main
Reviewed-on: #19
2024-12-27 21:31:46 +02:00
3010dc02bc Merge pull request 'v0.1.0-rc.3' (#15) from dev into main
Reviewed-on: #15
2024-12-16 22:08:22 +02:00
4afcbc93d5 Merge pull request 'Fixed minor issues' (#6) from dev into main
Reviewed-on: #6
2024-06-23 13:14:08 +03:00
72ccaa04a4 Merge pull request 'v0.1.0-rc.1' (#5) from dev into main
Reviewed-on: #5
2024-06-23 13:06:12 +03:00
16 changed files with 263 additions and 156 deletions

View File

@@ -73,6 +73,34 @@ Mandatory keys to modify:
After all of that you're good to go! Happy using :) After all of that you're good to go! Happy using :)
## Caching
Although general database access speed is fast, caching might become helpful for
bigger servers with many bot interactions. Currently, Redis and Memcached are supported.
Configuration happens through the config key `caching`.
Set `caching.type` to the service of you choice ("redis" or "memcached") and then update
the URI to access the service. It's Redis' default URI format for Redis and "address:port"
for Memcached.
Which one should I choose?
| Service | Read/write speed | Config flexibility |
|-----------|------------------|--------------------|
| Redis | High | Very flexible |
| Memcached | Very high | Basic |
> Performance difference between Redis and Memcached is generally quite low, so your setup
> should normally depend more on the configuration flexibility than on raw speed.
## Debugging
There's a config key `debug` that can be set to `true` for debugging purposes.
It should be set to `false` in production, otherwise log becomes very verbose and might
contain data that shouldn't normally be logged.
## Docker [Experimental] ## Docker [Experimental]
As an experiment, Docker deployment option has been added. As an experiment, Docker deployment option has been added.

0
classes/__init__.py Normal file
View File

View File

@@ -1,6 +1,20 @@
import logging
from logging import Logger
from libbot.cache.classes import CacheMemcached, CacheRedis
from libbot.cache.manager import create_cache_client
from libbot.pycord.classes import PycordBot from libbot.pycord.classes import PycordBot
logger: Logger = logging.getLogger(__name__)
class HoloBot(PycordBot): class HoloBot(PycordBot):
cache: CacheMemcached | CacheRedis | None = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._set_cache_engine()
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"])

View File

@@ -1,14 +1,15 @@
import logging import logging
from logging import Logger from logging import Logger
from typing import Any, Dict from typing import Any, Dict, Optional
from bson import ObjectId from bson import ObjectId
from discord import User, Member from discord import User, Member
from libbot.cache.classes import Cache
from libbot.utils import config_get from libbot.utils import config_get
from pymongo.results import InsertOneResult from pymongo.results import InsertOneResult
from errors import UserNotFoundError from errors import UserNotFoundError
from modules.database import col_warnings, col_users from modules.database import col_users
logger: Logger = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
@@ -29,74 +30,55 @@ class HoloUser:
@classmethod @classmethod
async def from_user( async def from_user(
cls, user: User | Member, allow_creation: bool = True cls,
user: User | Member,
allow_creation: bool = True,
cache: Optional[Cache] = None,
) -> "HoloUser": ) -> "HoloUser":
"""Get an object that has a proper binding between Discord ID and database """Get an object that has a proper binding between Discord ID and database
### Args: ### Args:
* `user` (User | Member): Object from which an ID can be extracted * `user` (User | Member): Object from which an ID can be extracted
* `allow_creation` (bool, optional): Whether to allow creation of a new user record if none found. Defaults to True.
* `cache` (Cache, optional): Cache engine to get the cache from
### Raises: ### Raises:
* `UserNotFoundError`: User with such ID does not seem to exist in database * `UserNotFoundError`: User with such ID does not seem to exist in database
""" """
db_entry: Dict[str, Any] | None = await col_users.find_one({"user": user.id}) if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"user_{user.id}")
if cached_entry is not None:
return cls(**cached_entry)
db_entry: Dict[str, Any] | None = await col_users.find_one({"id": user.id})
if db_entry is None: if db_entry is None:
if not allow_creation: if not allow_creation:
raise UserNotFoundError(user=user, user_id=user.id) raise UserNotFoundError(user=user, user_id=user.id)
db_entry = { db_entry = HoloUser.get_defaults(user.id)
"user": user.id,
"custom_role": None,
"custom_channel": None,
}
insert_result: InsertOneResult = await col_users.insert_one(db_entry) insert_result: InsertOneResult = await col_users.insert_one(db_entry)
db_entry["_id"] = insert_result.inserted_id() db_entry["_id"] = insert_result.inserted_id
db_entry["id"] = db_entry.pop("user") if cache is not None:
cache.set_json(f"user_{user.id}", db_entry)
return cls(**db_entry) return cls(**db_entry)
@classmethod @classmethod
async def from_id(cls, user_id: int) -> "HoloUser": async def from_id(cls, user_id: int) -> "HoloUser":
return NotImplemented raise NotImplementedError()
async def get_warnings(self) -> int: async def _set(self, key: str, value: Any, cache: Optional[Cache] = None) -> None:
"""Get number of warnings user has
### Returns:
* `int`: Number of warnings
"""
warns: Dict[str, Any] | None = await col_warnings.find_one({"user": self.id})
return 0 if warns is None else warns["warns"]
async def warn(self, count: int = 1, reason: str = "Not provided") -> None:
"""Warn and add count to warns number
### Args:
* `count` (int, optional): Count of warnings to be added. Defaults to 1.
"""
warns: Dict[str, Any] | None = await col_warnings.find_one({"user": self.id})
if warns is not None:
await col_warnings.update_one(
{"_id": self._id},
{"$set": {"warns": warns["warns"] + count}},
)
else:
await col_warnings.insert_one(document={"user": self.id, "warns": count})
logger.info("User %s was warned %s times due to: %s", self.id, count, reason)
async def _set(self, key: str, value: Any) -> None:
"""Set attribute data and save it into the database """Set attribute data and save it into the database
### Args: ### Args:
* `key` (str): Attribute to be changed * `key` (str): Attribute to be changed
* `value` (Any): Value to set * `value` (Any): Value to set
* `cache` (Cache, optional): Cache engine to write the update into
""" """
if not hasattr(self, key): if not hasattr(self, key):
raise AttributeError() raise AttributeError()
@@ -107,40 +89,99 @@ class HoloUser:
{"_id": self._id}, {"$set": {key: value}}, upsert=True {"_id": self._id}, {"$set": {key: value}}, upsert=True
) )
logger.info("Set attribute %s of user %s to %s", key, self.id, value) self._update_cache(cache)
async def _remove(self, key: str) -> None: 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 """Remove attribute data and save it into the database
### Args: ### Args:
* `key` (str): Attribute to be removed * `key` (str): Attribute to be removed
* `cache` (Cache, optional): Cache engine to write the update into
""" """
if not hasattr(self, key): if not hasattr(self, key):
raise AttributeError() raise AttributeError()
setattr(self, key, None) default_value: Any = HoloUser.get_default_value(key)
setattr(self, key, default_value)
await col_users.update_one( await col_users.update_one(
{"_id": self._id}, {"$unset": {key: None}}, upsert=True {"_id": self._id}, {"$set": {key: default_value}}, upsert=True
) )
logger.info("Removed attribute %s of user %s", key, self.id) self._update_cache(cache)
async def set_custom_channel(self, channel_id: int) -> None: logger.info("Removed attribute '%s' of user %s", key, self.id)
await self._set("custom_channel", channel_id)
async def set_custom_role(self, role_id: int) -> None: def _get_cache_key(self) -> str:
await self._set("custom_role", role_id) return f"user_{self.id}"
async def remove_custom_channel(self) -> None: def _update_cache(self, cache: Optional[Cache] = None) -> None:
await self._remove("custom_channel") if cache is None:
return
async def remove_custom_role(self) -> None: user_dict: Dict[str, Any] = self._to_dict()
await self._remove("custom_role")
async def purge(self) -> None: if user_dict is not None:
"""Completely remove user data from database. Will not remove transactions logs and warnings.""" 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())
@staticmethod
def get_defaults(user_id: int | None = None) -> Dict[str, Any]:
return {
"id": user_id,
"custom_role": None,
"custom_channel": None,
}
@staticmethod
def get_default_value(key: str) -> Any:
if key not in HoloUser.get_defaults():
raise KeyError(f"There's no default value for key '{key}' in HoloUser")
return HoloUser.get_defaults()[key]
def _to_dict(self) -> Dict[str, Any]:
return {
"_id": self._id,
"id": self.id,
"custom_role": self.custom_role,
"custom_channel": self.custom_channel,
}
async def set_custom_channel(
self, channel_id: int, cache: Optional[Cache] = None
) -> None:
await self._set("custom_channel", channel_id, cache=cache)
async def set_custom_role(
self, role_id: int, cache: Optional[Cache] = None
) -> None:
await self._set("custom_role", role_id, cache=cache)
async def remove_custom_channel(self, cache: Optional[Cache] = None) -> None:
await self._remove("custom_channel", cache=cache)
async def remove_custom_role(self, cache: Optional[Cache] = None) -> None:
await self._remove("custom_role", cache=cache)
async def purge(self, cache: Optional[Cache] = None) -> None:
"""Completely remove user data from database. Only removes the user record from users collection.
### Args:
* `cache` (Cache, optional): Cache engine to write the update into
"""
await col_users.delete_one({"_id": self._id}) await col_users.delete_one({"_id": self._id})
self._delete_cache(cache)
@staticmethod @staticmethod
async def is_moderator(member: User | Member) -> bool: async def is_moderator(member: User | Member) -> bool:

View File

@@ -56,7 +56,7 @@ class Analytics(commands.Cog):
# Insert entry into the database # Insert entry into the database
await col_analytics.insert_one( await col_analytics.insert_one(
{ {
"user": message.author.id, "user_id": message.author.id,
"channel": message.channel.id, "channel": message.channel.id,
"content": message.content, "content": message.content,
"stickers": stickers, "stickers": stickers,

View File

@@ -47,7 +47,9 @@ class CustomChannels(commands.Cog):
Command to create a custom channel for a user. Command to create a custom channel for a user.
""" """
holo_user_ctx: HoloUser = await HoloUser.from_user(ctx.user) holo_user_ctx: HoloUser = await HoloUser.from_user(
ctx.user, cache=self.client.cache
)
# Return if the user is using the command outside of a guild # Return if the user is using the command outside of a guild
if not hasattr(ctx.author, "guild"): if not hasattr(ctx.author, "guild"):
@@ -100,7 +102,9 @@ class CustomChannels(commands.Cog):
manage_channels=True, manage_channels=True,
) )
await holo_user_ctx.set_custom_channel(created_channel.id) await holo_user_ctx.set_custom_channel(
created_channel.id, cache=self.client.cache
)
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
@@ -136,7 +140,9 @@ class CustomChannels(commands.Cog):
Command to change properties of a custom channel. Command to change properties of a custom channel.
""" """
holo_user_ctx: HoloUser = await HoloUser.from_user(ctx.user) holo_user_ctx: HoloUser = await HoloUser.from_user(
ctx.user, cache=self.client.cache
)
custom_channel: TextChannel | None = ds_utils.get( custom_channel: TextChannel | None = ds_utils.get(
ctx.guild.channels, id=holo_user_ctx.custom_channel ctx.guild.channels, id=holo_user_ctx.custom_channel
@@ -182,7 +188,9 @@ class CustomChannels(commands.Cog):
"""Command /customchannel remove [<confirm>] """Command /customchannel remove [<confirm>]
Command to remove a custom channel. Requires additional confirmation.""" Command to remove a custom channel. Requires additional confirmation."""
holo_user_ctx: HoloUser = await HoloUser.from_user(ctx.user) holo_user_ctx: HoloUser = await HoloUser.from_user(
ctx.user, cache=self.client.cache
)
# Return if the user does not have a custom channel # Return if the user does not have a custom channel
if holo_user_ctx.custom_channel is None: if holo_user_ctx.custom_channel is None:
@@ -211,7 +219,7 @@ class CustomChannels(commands.Cog):
color=Color.FAIL, color=Color.FAIL,
) )
) )
await holo_user_ctx.remove_custom_channel() await holo_user_ctx.remove_custom_channel(cache=self.client.cache)
return return
# Return if the confirmation is missing # Return if the confirmation is missing
@@ -227,7 +235,7 @@ class CustomChannels(commands.Cog):
await custom_channel.delete(reason="Власник запросив видалення") await custom_channel.delete(reason="Власник запросив видалення")
await holo_user_ctx.remove_custom_channel() await holo_user_ctx.remove_custom_channel(cache=self.client.cache)
try: try:
await ctx.respond( await ctx.respond(

View File

@@ -14,7 +14,6 @@ from libbot.utils import config_get, json_write
from classes.holo_bot import HoloBot from classes.holo_bot import HoloBot
from classes.holo_user import HoloUser from classes.holo_user import HoloUser
from enums import Color from enums import Color
from modules.database import col_users
from modules.utils_sync import guild_name from modules.utils_sync import guild_name
logger: Logger = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
@@ -90,17 +89,19 @@ class Data(commands.Cog):
{ {
"id": member.id, "id": member.id,
"nick": member.nick, "nick": member.nick,
"username": f"{member.name}#{member.discriminator}", "username": member.name,
"bot": member.bot, "bot": member.bot,
} }
) )
# Temporary file must be written synchronously, # Temporary file must be written synchronously,
# otherwise it will not be there when ctx.respond() is be called # otherwise it will not be there when ctx.respond() is be called
# TODO Find a way to give this file to Pycord without FS operations
json_write(users, Path(f"tmp/{uuid}")) json_write(users, Path(f"tmp/{uuid}"))
await ctx.respond(file=File(Path(f"tmp/{uuid}"), filename="users.json")) await ctx.respond(file=File(Path(f"tmp/{uuid}"), filename="users.json"))
# TODO Deprecate this command
@data.command( @data.command(
name="migrate", name="migrate",
description="Мігрувати всіх користувачів до бази", description="Мігрувати всіх користувачів до бази",
@@ -164,20 +165,7 @@ class Data(commands.Cog):
if member.bot: if member.bot:
continue continue
if (await col_users.find_one({"user": member.id})) is None: await HoloUser.from_user(member, cache=self.client.cache)
user: Dict[str, Any] = {}
defaults: Dict[str, Any] = await config_get("user", "defaults")
user["user"] = member.id
for key in defaults:
user[key] = defaults[key]
await col_users.insert_one(document=user)
logging.info(
"Added DB record for user %s during migration", member.id
)
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(

View File

@@ -1,6 +1,5 @@
import logging import logging
from logging import Logger from logging import Logger
from typing import Dict, Any
from discord import Member, Message, TextChannel, MessageType from discord import Member, Message, TextChannel, MessageType
from discord import utils as ds_utils from discord import utils as ds_utils
@@ -8,6 +7,7 @@ from discord.ext import commands
from libbot.utils import config_get from libbot.utils import config_get
from classes.holo_bot import HoloBot from classes.holo_bot import HoloBot
from classes.holo_user import HoloUser
from modules.database import col_users from modules.database import col_users
logger: Logger = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
@@ -25,16 +25,7 @@ class Logger(commands.Cog):
and (message.author.bot is False) and (message.author.bot is False)
and (message.author.system is False) and (message.author.system is False)
): ):
if (await col_users.find_one({"user": message.author.id})) is None: await HoloUser.from_user(message.author, cache=self.client.cache)
user: Dict[str, Any] = {}
defaults: Dict[str, Any] = await config_get("user", "defaults")
user["user"] = message.author.id
for key in defaults:
user[key] = defaults[key]
await col_users.insert_one(document=user)
if ( if (
(message.type == MessageType.thread_created) (message.type == MessageType.thread_created)
@@ -69,30 +60,21 @@ class Logger(commands.Cog):
id=await config_get("rules", "channels", "text"), id=await config_get("rules", "channels", "text"),
) )
if welcome_chan is None:
logger.warning("Could not find a welcome channel by its id")
if ( if (
(member != self.client.user) (member != self.client.user)
and (member.bot is False) and (member.bot is False)
and (member.system is False) and (member.system is False)
): ):
await welcome_chan.send( if welcome_chan is not None and rules_chan is not None:
content=(await config_get("welcome", "messages")).format( await welcome_chan.send(
mention=member.mention, rules=rules_chan.mention content=(await config_get("welcome", "messages")).format(
mention=member.mention, rules=rules_chan.mention
)
) )
) else:
logger.warning("Could not find a welcome and/or rules channel by id")
if (await col_users.find_one({"user": member.id})) is None: await HoloUser.from_user(member, cache=self.client.cache)
user: Dict[str, Any] = {}
defaults: Dict[str, Any] = await config_get("user", "defaults")
user["user"] = member.id
for key in defaults:
user[key] = defaults[key]
await col_users.insert_one(document=user)
def setup(client: HoloBot) -> None: def setup(client: HoloBot) -> None:

View File

@@ -22,16 +22,20 @@
"port": 27017, "port": 27017,
"name": "holo_discord" "name": "holo_discord"
}, },
"cache": {
"type": null,
"memcached": {
"uri": "127.0.0.1:11211"
},
"redis": {
"uri": "redis://127.0.0.1:6379/0"
}
},
"logging": { "logging": {
"size": 512, "size": 512,
"location": "logs" "location": "logs"
}, },
"defaults": { "defaults": {},
"user": {
"custom_role": null,
"custom_channel": null
}
},
"categories": { "categories": {
"custom_channels": 0 "custom_channels": 0
}, },

15
main.py
View File

@@ -12,8 +12,15 @@ from classes.holo_bot import HoloBot
from modules.migrator import migrate_database from modules.migrator import migrate_database
from modules.scheduler import scheduler from modules.scheduler import scheduler
if not Path("config.json").exists():
print(
"Config file is missing: Make sure the configuration file 'config.json' is in place.",
flush=True,
)
sys.exit()
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO if not config_get("debug") else logging.DEBUG,
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s", format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
datefmt="[%X]", datefmt="[%X]",
) )
@@ -41,12 +48,6 @@ with contextlib.suppress(ImportError):
def main() -> None: def main() -> None:
if not Path("config.json").exists():
logger.error(
"Config file is missing: Make sure the configuration file 'config.json' is in place."
)
sys.exit()
# Perform migration if command line argument was provided # Perform migration if command line argument was provided
if args.migrate: if args.migrate:
logger.info("Performing migrations...") logger.info("Performing migrations...")

View File

@@ -0,0 +1,63 @@
import logging
from logging import Logger
from libbot.utils import config_set, config_delete
from mongodb_migrations.base import BaseMigration
logger: Logger = logging.getLogger(__name__)
class Migration(BaseMigration):
def upgrade(self):
try:
config_set(
"cache",
{
"type": None,
"memcached": {"uri": "127.0.0.1:11211"},
"redis": {"uri": "redis://127.0.0.1:6379/0"},
},
*[],
)
except Exception as exc:
logger.error(
"Could not upgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
self.db.users.update_many(
{"user": {"$exists": True}},
{"$rename": {"user": "id"}},
)
self.db.analytics.update_many(
{"user": {"$exists": True}},
{"$rename": {"user": "user_id"}},
)
self.db.warnings.update_many(
{"user": {"$exists": True}},
{"$rename": {"user": "user_id"}},
)
def downgrade(self):
try:
config_delete("cache", *[])
except Exception as exc:
logger.error(
"Could not downgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
self.db.users.update_many(
{"id": {"$exists": True}},
{"$rename": {"id": "user"}},
)
self.db.analytics.update_many(
{"user": {"$exists": True}},
{"$rename": {"user_id": "user"}},
)
self.db.warnings.update_many(
{"user": {"$exists": True}},
{"$rename": {"user_id": "user"}},
)

View File

@@ -2,9 +2,6 @@ from typing import Dict, Any
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase
from libbot.utils import config_get from libbot.utils import config_get
from pymongo import MongoClient
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.database import Database
db_config: Dict[str, Any] = config_get("database") db_config: Dict[str, Any] = config_get("database")
@@ -23,18 +20,12 @@ con_string: str = (
) )
db_client: AsyncClient = AsyncClient(con_string) db_client: AsyncClient = AsyncClient(con_string)
db_client_sync: MongoClient = MongoClient(con_string)
# Async declarations per default # Async declarations per default
db: AsyncDatabase = db_client.get_database(name=db_config["name"]) db: AsyncDatabase = db_client.get_database(name=db_config["name"])
col_users: AsyncCollection = db.get_collection("users") col_users: AsyncCollection = db.get_collection("users")
col_warnings: AsyncCollection = db.get_collection("warnings")
col_analytics: AsyncCollection = db.get_collection("analytics") col_analytics: AsyncCollection = db.get_collection("analytics")
# Sync declarations as a fallback # Update indexes
sync_db: Database = db_client_sync.get_database(name=db_config["name"]) db.dispatch.get_collection("users").create_index("id", unique=True)
sync_col_users: Collection = sync_db.get_collection("users")
sync_col_warnings: Collection = sync_db.get_collection("warnings")
sync_col_analytics: Collection = sync_db.get_collection("analytics")

View File

@@ -5,7 +5,9 @@ requests>=2.32.2
aiofiles~=24.1.0 aiofiles~=24.1.0
apscheduler>=3.10.0 apscheduler>=3.10.0
async_pymongo==0.1.11 async_pymongo==0.1.11
libbot[speed,pycord]==4.0.2 libbot[speed,pycord,cache]==4.1.0
mongodb-migrations==1.3.1 mongodb-migrations==1.3.1
pymemcache~=4.0.0
redis~=5.2.1
ujson~=5.10.0 ujson~=5.10.0
WaifuPicsPython==0.2.0 WaifuPicsPython==0.2.0

View File

@@ -1,14 +1,14 @@
{ {
"$jsonSchema": { "$jsonSchema": {
"required": [ "required": [
"user", "user_id",
"channel", "channel",
"content", "content",
"stickers", "stickers",
"attachments" "attachments"
], ],
"properties": { "properties": {
"user": { "user_id": {
"bsonType": "long", "bsonType": "long",
"description": "Discord ID of user" "description": "Discord ID of user"
}, },
@@ -17,7 +17,10 @@
"description": "Discord ID of a channel" "description": "Discord ID of a channel"
}, },
"content": { "content": {
"bsonType": ["null", "string"], "bsonType": [
"null",
"string"
],
"description": "Text of the message" "description": "Text of the message"
}, },
"stickers": { "stickers": {
@@ -40,7 +43,7 @@
"format": { "format": {
"bsonType": "array" "bsonType": "array"
}, },
"user": { "user_id": {
"bsonType": "string" "bsonType": "string"
} }
} }

View File

@@ -1,12 +1,12 @@
{ {
"$jsonSchema": { "$jsonSchema": {
"required": [ "required": [
"user", "id",
"custom_role", "custom_role",
"custom_channel" "custom_channel"
], ],
"properties": { "properties": {
"user": { "id": {
"bsonType": "long", "bsonType": "long",
"description": "Discord ID of user" "description": "Discord ID of user"
}, },

View File

@@ -1,18 +0,0 @@
{
"$jsonSchema": {
"required": [
"user",
"warnings"
],
"properties": {
"user": {
"bsonType": "long",
"description": "Discord ID of user"
},
"warnings": {
"bsonType": "int",
"description": "Number of warnings on count"
}
}
}
}