FileToLink / Thunder /utils /database.py
Fyaz Mohammed
fix: atomic user registration to eliminate TOCTOU race condition
658e097
Raw
History Blame Contribute Delete
18.7 kB
# 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)