Spaces:
Running
Running
| # Thunder/utils/database.py | |
| import datetime | |
| from typing import Any, Dict, Optional | |
| from pymongo import AsyncMongoClient | |
| from pymongo.asynchronous.collection import AsyncCollection | |
| from pymongo.errors import DuplicateKeyError | |
| from Thunder.vars import Var | |
| from Thunder.utils.logger import logger | |
| class Database: | |
| def __init__(self, uri: str, database_name: str, *args, **kwargs): | |
| self._client = AsyncMongoClient(uri, *args, **kwargs) | |
| self.db = self._client[database_name] | |
| self.col: AsyncCollection = self.db.users | |
| self.banned_users_col: AsyncCollection = self.db.banned_users | |
| self.banned_channels_col: AsyncCollection = self.db.banned_channels | |
| self.token_col: AsyncCollection = self.db.tokens | |
| self.authorized_users_col: AsyncCollection = self.db.authorized_users | |
| self.restart_message_col: AsyncCollection = self.db.restart_message | |
| self.files_col: AsyncCollection = self.db.files | |
| self.file_ingest_locks_col: AsyncCollection = self.db.file_ingest_locks | |
| async def _deduplicate_users(self) -> None: | |
| pipeline = [ | |
| {"$sort": {"join_date": 1}}, | |
| {"$group": {"_id": "$id", "doc_id": {"$first": "$_id"}}}, | |
| {"$project": {"_id": "$doc_id"}} | |
| ] | |
| keep_ids = [] | |
| async for doc in self.col.aggregate(pipeline): | |
| keep_ids.append(doc["_id"]) | |
| if keep_ids: | |
| result = await self.col.delete_many({"_id": {"$nin": keep_ids}}) | |
| if result.deleted_count > 0: | |
| logger.warning(f"Deduplicated {result.deleted_count} duplicate user documents.") | |
| async def ensure_indexes(self, *, raise_on_error: bool = True) -> bool: | |
| try: | |
| await self.banned_users_col.create_index("user_id", unique=True) | |
| await self.banned_channels_col.create_index("channel_id", unique=True) | |
| await self.token_col.create_index("token", unique=True) | |
| await self.authorized_users_col.create_index("user_id", unique=True) | |
| try: | |
| await self.col.create_index("id", unique=True) | |
| except DuplicateKeyError: | |
| logger.warning("Duplicate users found, deduplicating...") | |
| await self._deduplicate_users() | |
| await self.col.create_index("id", unique=True) | |
| await self.token_col.create_index("expires_at", expireAfterSeconds=0) | |
| await self.token_col.create_index("activated") | |
| await self.restart_message_col.create_index("message_id", unique=True) | |
| await self.restart_message_col.create_index("timestamp", expireAfterSeconds=3600) | |
| await self.files_col.create_index("file_unique_id", unique=True) | |
| await self.files_col.create_index("public_hash", unique=True) | |
| await self.files_col.create_index("canonical_message_id", unique=True) | |
| await self.files_col.create_index("created_at") | |
| await self.files_col.create_index("last_seen_at") | |
| await self.file_ingest_locks_col.create_index("expires_at", expireAfterSeconds=0) | |
| logger.debug("Database indexes ensured.") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error in ensure_indexes: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise | |
| return False | |
| def new_user(self, user_id: int) -> dict: | |
| try: | |
| return { | |
| 'id': user_id, | |
| 'join_date': datetime.datetime.now(datetime.timezone.utc) | |
| } | |
| except Exception as e: | |
| logger.error(f"Error in new_user for user {user_id}: {e}", exc_info=True) | |
| raise | |
| async def add_user(self, user_id: int) -> bool: | |
| try: | |
| result = await self.col.update_one( | |
| {'id': user_id}, | |
| {'$setOnInsert': self.new_user(user_id)}, | |
| upsert=True | |
| ) | |
| if result.upserted_id: | |
| logger.debug(f"Added new user {user_id} to database.") | |
| return True | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error in add_user for user {user_id}: {e}", exc_info=True) | |
| raise | |
| async def is_user_exist(self, user_id: int) -> bool: | |
| """Read-only existence check. For user registration, use add_user() instead.""" | |
| try: | |
| user = await self.col.find_one({'id': user_id}, {'_id': 1}) | |
| return bool(user) | |
| except Exception as e: | |
| logger.error(f"Error in is_user_exist for user {user_id}: {e}", exc_info=True) | |
| raise | |
| async def total_users_count(self) -> int: | |
| try: | |
| return await self.col.count_documents({}) | |
| except Exception as e: | |
| logger.error(f"Error in total_users_count: {e}", exc_info=True) | |
| return 0 | |
| async def get_authorized_users_count(self) -> int: | |
| try: | |
| return await self.authorized_users_col.count_documents({}) | |
| except Exception as e: | |
| logger.error(f"Error in get_authorized_users_count: {e}", exc_info=True) | |
| return 0 | |
| async def get_regular_users_count(self) -> int: | |
| try: | |
| auth_ids = await self.authorized_users_col.distinct("user_id") | |
| return await self.col.count_documents({"id": {"$nin": auth_ids}}) | |
| except Exception as e: | |
| logger.error(f"Error in get_regular_users_count: {e}", exc_info=True) | |
| return 0 | |
| async def get_all_users(self): | |
| try: | |
| return self.col.find({}) | |
| except Exception as e: | |
| logger.error(f"Error in get_all_users: {e}", exc_info=True) | |
| return self.col.find({"_id": {"$exists": False}}) | |
| async def get_authorized_users_cursor(self): | |
| try: | |
| return self.authorized_users_col.find({}) | |
| except Exception as e: | |
| logger.error(f"Error in get_authorized_users_cursor: {e}", exc_info=True) | |
| return self.authorized_users_col.find({"_id": {"$exists": False}}) | |
| async def get_regular_users_cursor(self): | |
| try: | |
| auth_ids = await self.authorized_users_col.distinct("user_id") | |
| return self.col.find({"id": {"$nin": auth_ids}}) | |
| except Exception as e: | |
| logger.error(f"Error in get_regular_users_cursor: {e}", exc_info=True) | |
| return self.col.find({"_id": {"$exists": False}}) | |
| async def delete_user(self, user_id: int): | |
| try: | |
| await self.col.delete_one({'id': user_id}) | |
| logger.debug(f"Deleted user {user_id}.") | |
| except Exception as e: | |
| logger.error(f"Error in delete_user for user {user_id}: {e}", exc_info=True) | |
| raise | |
| async def add_banned_user( | |
| self, user_id: int, banned_by: Optional[int] = None, | |
| reason: Optional[str] = None | |
| ): | |
| try: | |
| ban_data = { | |
| "user_id": user_id, | |
| "banned_at": datetime.datetime.now(datetime.timezone.utc), | |
| "banned_by": banned_by, | |
| "reason": reason | |
| } | |
| await self.banned_users_col.update_one( | |
| {"user_id": user_id}, | |
| {"$set": ban_data}, | |
| upsert=True | |
| ) | |
| logger.debug(f"Added/Updated banned user {user_id}. Reason: {reason}") | |
| except Exception as e: | |
| logger.error(f"Error in add_banned_user for user {user_id}: {e}", exc_info=True) | |
| raise | |
| async def remove_banned_user(self, user_id: int) -> bool: | |
| try: | |
| result = await self.banned_users_col.delete_one({"user_id": user_id}) | |
| if result.deleted_count > 0: | |
| logger.debug(f"Removed banned user {user_id}.") | |
| return True | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error in remove_banned_user for user {user_id}: {e}", exc_info=True) | |
| return False | |
| async def is_user_banned(self, user_id: int) -> Optional[Dict[str, Any]]: | |
| try: | |
| return await self.banned_users_col.find_one({"user_id": user_id}) | |
| except Exception as e: | |
| logger.error(f"Error in is_user_banned for user {user_id}: {e}", exc_info=True) | |
| return None | |
| async def add_banned_channel( | |
| self, channel_id: int, banned_by: Optional[int] = None, | |
| reason: Optional[str] = None | |
| ): | |
| try: | |
| ban_data = { | |
| "channel_id": channel_id, | |
| "banned_at": datetime.datetime.now(datetime.timezone.utc), | |
| "banned_by": banned_by, | |
| "reason": reason | |
| } | |
| await self.banned_channels_col.update_one( | |
| {"channel_id": channel_id}, | |
| {"$set": ban_data}, | |
| upsert=True | |
| ) | |
| logger.debug(f"Added/Updated banned channel {channel_id}. Reason: {reason}") | |
| except Exception as e: | |
| logger.error(f"Error in add_banned_channel for channel {channel_id}: {e}", exc_info=True) | |
| raise | |
| async def remove_banned_channel(self, channel_id: int) -> bool: | |
| try: | |
| result = await self.banned_channels_col.delete_one({"channel_id": channel_id}) | |
| if result.deleted_count > 0: | |
| logger.debug(f"Removed banned channel {channel_id}.") | |
| return True | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error in remove_banned_channel for channel {channel_id}: {e}", exc_info=True) | |
| return False | |
| async def is_channel_banned(self, channel_id: int) -> Optional[Dict[str, Any]]: | |
| try: | |
| return await self.banned_channels_col.find_one({"channel_id": channel_id}) | |
| except Exception as e: | |
| logger.error(f"Error in is_channel_banned for channel {channel_id}: {e}", exc_info=True) | |
| return None | |
| async def save_main_token(self, user_id: int, token_value: str, expires_at: datetime.datetime, created_at: datetime.datetime, activated: bool) -> None: | |
| try: | |
| await self.token_col.update_one( | |
| {"user_id": user_id, "token": token_value}, | |
| {"$set": { | |
| "expires_at": expires_at, | |
| "created_at": created_at, | |
| "activated": activated | |
| } | |
| }, | |
| upsert=True | |
| ) | |
| logger.debug(f"Saved main token {token_value} for user {user_id} with activated status {activated}.") | |
| except Exception as e: | |
| logger.error(f"Error saving main token for user {user_id}: {e}", exc_info=True) | |
| raise | |
| async def add_restart_message(self, message_id: int, chat_id: int) -> None: | |
| try: | |
| await self.restart_message_col.insert_one({ | |
| "message_id": message_id, | |
| "chat_id": chat_id, | |
| "timestamp": datetime.datetime.now(datetime.timezone.utc) | |
| }) | |
| logger.debug(f"Added restart message {message_id} for chat {chat_id}.") | |
| except Exception as e: | |
| logger.error(f"Error adding restart message {message_id}: {e}", exc_info=True) | |
| async def get_restart_message(self) -> Optional[Dict[str, Any]]: | |
| try: | |
| return await self.restart_message_col.find_one(sort=[("timestamp", -1)]) | |
| except Exception as e: | |
| logger.error(f"Error getting restart message: {e}", exc_info=True) | |
| return None | |
| async def delete_restart_message(self, message_id: int) -> None: | |
| try: | |
| await self.restart_message_col.delete_one({"message_id": message_id}) | |
| logger.debug(f"Deleted restart message {message_id}.") | |
| except Exception as e: | |
| logger.error(f"Error deleting restart message {message_id}: {e}", exc_info=True) | |
| async def is_user_authorized(self, user_id: int) -> bool: | |
| try: | |
| user = await self.authorized_users_col.find_one({'user_id': user_id}, {'_id': 1}) | |
| return bool(user) | |
| except Exception as e: | |
| logger.error(f"Error in is_user_authorized for user {user_id}: {e}", exc_info=True) | |
| return False | |
| async def get_file_by_unique_id(self, file_unique_id: str) -> Optional[Dict[str, Any]]: | |
| try: | |
| return await self.files_col.find_one({"file_unique_id": file_unique_id}) | |
| except Exception as e: | |
| logger.error(f"Error getting file by unique_id {file_unique_id}: {e}", exc_info=True) | |
| return None | |
| async def get_file_by_hash( | |
| self, | |
| public_hash: str, | |
| *, | |
| raise_on_error: bool = True | |
| ) -> Optional[Dict[str, Any]]: | |
| try: | |
| return await self.files_col.find_one({"public_hash": public_hash}) | |
| except Exception as e: | |
| logger.error(f"Error getting file by hash {public_hash}: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise | |
| return None | |
| async def get_file_by_message_id(self, canonical_message_id: int) -> Optional[Dict[str, Any]]: | |
| try: | |
| return await self.files_col.find_one({"canonical_message_id": canonical_message_id}) | |
| except Exception as e: | |
| logger.error( | |
| f"Error getting file by message_id {canonical_message_id}: {e}", | |
| exc_info=True | |
| ) | |
| return None | |
| async def create_file_record(self, file_record: Dict[str, Any]) -> None: | |
| try: | |
| await self.files_col.insert_one(file_record) | |
| except Exception as e: | |
| logger.error( | |
| f"Error creating canonical file record for {file_record.get('file_unique_id')}: {e}", | |
| exc_info=True | |
| ) | |
| raise | |
| async def replace_file_record(self, file_record: Dict[str, Any]) -> None: | |
| try: | |
| await self.files_col.replace_one( | |
| {"file_unique_id": file_record["file_unique_id"]}, | |
| file_record, | |
| upsert=True | |
| ) | |
| except Exception as e: | |
| logger.error( | |
| f"Error replacing canonical file record for {file_record.get('file_unique_id')}: {e}", | |
| exc_info=True | |
| ) | |
| raise | |
| async def touch_file_record( | |
| self, | |
| public_hash: str, | |
| *, | |
| reused: bool = False, | |
| raise_on_error: bool = False | |
| ) -> bool: | |
| try: | |
| update_doc: Dict[str, Any] = { | |
| "$set": {"last_seen_at": datetime.datetime.now(datetime.timezone.utc)}, | |
| "$inc": {"seen_count": 1} | |
| } | |
| if reused: | |
| update_doc["$inc"]["reuse_count"] = 1 | |
| await self.files_col.update_one({"public_hash": public_hash}, update_doc) | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error touching canonical file {public_hash}: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise | |
| return False | |
| async def update_file_id( | |
| self, | |
| public_hash: str, | |
| file_id: str, | |
| *, | |
| raise_on_error: bool = False | |
| ) -> bool: | |
| try: | |
| await self.files_col.update_one( | |
| {"public_hash": public_hash}, | |
| { | |
| "$set": { | |
| "file_id": file_id, | |
| "last_seen_at": datetime.datetime.now(datetime.timezone.utc) | |
| } | |
| } | |
| ) | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error updating file_id for {public_hash}: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise | |
| return False | |
| async def acquire_file_ingest_claim( | |
| self, | |
| file_unique_id: str, | |
| *, | |
| ttl_seconds: int = 60 | |
| ) -> bool: | |
| now = datetime.datetime.now(datetime.timezone.utc) | |
| claim_fields = { | |
| "created_at": now, | |
| "expires_at": now + datetime.timedelta(seconds=ttl_seconds) | |
| } | |
| try: | |
| await self.file_ingest_locks_col.insert_one({ | |
| "_id": file_unique_id, | |
| **claim_fields | |
| }) | |
| return True | |
| except DuplicateKeyError: | |
| try: | |
| result = await self.file_ingest_locks_col.find_one_and_update( | |
| { | |
| "_id": file_unique_id, | |
| "$or": [ | |
| {"expires_at": {"$lte": now}}, | |
| {"expires_at": {"$exists": False}} | |
| ] | |
| }, | |
| { | |
| "$set": claim_fields | |
| }, | |
| return_document=False | |
| ) | |
| return bool(result) | |
| except Exception as e: | |
| logger.error(f"Error updating ingest claim for {file_unique_id}: {e}", exc_info=True) | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error acquiring ingest claim for {file_unique_id}: {e}", exc_info=True) | |
| raise | |
| async def release_file_ingest_claim(self, file_unique_id: str) -> bool: | |
| try: | |
| await self.file_ingest_locks_col.delete_one({"_id": file_unique_id}) | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error releasing ingest claim for {file_unique_id}: {e}", exc_info=True) | |
| return False | |
| async def is_file_ingest_claim_active(self, file_unique_id: str) -> bool: | |
| try: | |
| claim = await self.file_ingest_locks_col.find_one( | |
| { | |
| "_id": file_unique_id, | |
| "expires_at": {"$gt": datetime.datetime.now(datetime.timezone.utc)} | |
| }, | |
| {"_id": 1} | |
| ) | |
| return bool(claim) | |
| except Exception as e: | |
| logger.error(f"Error checking ingest claim for {file_unique_id}: {e}", exc_info=True) | |
| raise | |
| async def close(self): | |
| if self._client: | |
| await self._client.close() | |
| db = Database(Var.DATABASE_URL, Var.NAME) | |