860 lines
33 KiB
Python
860 lines
33 KiB
Python
"""
|
||
Admin panel routes
|
||
"""
|
||
from fastapi import APIRouter, Request, Depends, HTTPException, Form, status
|
||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||
from fastapi.templating import Jinja2Templates
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from typing import Optional
|
||
from web.utils.auth import get_current_user, verify_web_user, create_session, delete_session, is_owner_web
|
||
from web.utils.database import get_db
|
||
from web.utils.otp import verify_otp_code, get_user_by_identifier, create_otp_code
|
||
from web.utils.csrf import verify_csrf, get_csrf_token
|
||
from web.admin import views
|
||
from shared.database.models import User, Task
|
||
from web.admin.user_manager_web import add_user_web, remove_user_web, block_user_web, unblock_user_web, add_admin_web, remove_admin_web
|
||
from bot.modules.task_scheduler.queue import task_queue, Task as QueueTask, TaskStatus
|
||
from bot.modules.media_loader.ytdlp import get_media_info
|
||
from bot.modules.access_control.auth import is_authorized
|
||
from web.utils.bot_client import send_otp_to_user
|
||
from shared.config import settings
|
||
import logging
|
||
import time
|
||
import asyncio
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
templates = Jinja2Templates(directory="web/admin/templates")
|
||
|
||
|
||
async def get_csrf_token_for_template(request: Request) -> Optional[str]:
|
||
"""
|
||
Get CSRF token for template.
|
||
|
||
Args:
|
||
request: FastAPI Request object
|
||
|
||
Returns:
|
||
CSRF token or None
|
||
"""
|
||
from web.utils.csrf import get_csrf_token, generate_csrf_token
|
||
from web.utils.auth import get_session
|
||
|
||
token = await get_csrf_token(request)
|
||
if not token:
|
||
# Generate new token
|
||
token = generate_csrf_token()
|
||
session_id = request.cookies.get("session_id")
|
||
if session_id:
|
||
session_data = await get_session(session_id)
|
||
if session_data:
|
||
session_data["csrf_token"] = token
|
||
return token
|
||
|
||
|
||
# ==================== Authentication ====================
|
||
|
||
@router.get("/login", response_class=HTMLResponse)
|
||
async def login_page(request: Request):
|
||
"""Login page"""
|
||
csrf_token = await get_csrf_token_for_template(request)
|
||
return templates.TemplateResponse("login.html", {
|
||
"request": request,
|
||
"csrf_token": csrf_token
|
||
})
|
||
|
||
|
||
@router.post("/api/otp/request")
|
||
async def request_otp(
|
||
request: Request,
|
||
identifier: str = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Request OTP code by ID or username"""
|
||
# CSRF token check (softer for unauthorized users)
|
||
try:
|
||
await verify_csrf(request, csrf_token)
|
||
except HTTPException:
|
||
# For OTP request allow without CSRF (user not yet authorized)
|
||
# But check Origin/Referer for basic protection
|
||
origin = request.headers.get("Origin")
|
||
referer = request.headers.get("Referer")
|
||
if not origin and not referer:
|
||
logger.warning(f"OTP request without Origin/Referer from IP {request.client.host if request.client else 'unknown'}")
|
||
|
||
try:
|
||
user_id = None
|
||
|
||
# Try to find user by identifier
|
||
user = await get_user_by_identifier(identifier, db)
|
||
if user:
|
||
user_id = user.user_id
|
||
else:
|
||
# If user not found in database, try to determine user_id from identifier
|
||
try:
|
||
user_id = int(identifier)
|
||
except ValueError:
|
||
# This is username, try to find via Telegram API
|
||
try:
|
||
from bot.modules.task_scheduler.executor import get_app_client
|
||
app_client = get_app_client()
|
||
if app_client:
|
||
username = identifier.lstrip('@')
|
||
chat = await app_client.get_chat(username)
|
||
if chat and hasattr(chat, 'id'):
|
||
user_id = chat.id
|
||
logger.info(f"Найден пользователь {user_id} по username {username} через Telegram API")
|
||
except Exception as e:
|
||
logger.debug(f"Не удалось найти пользователя по username {identifier}: {e}")
|
||
|
||
if not user_id:
|
||
# User not found in database or via Telegram API
|
||
return JSONResponse(
|
||
{"success": False, "message": "Пользователь не найден. Убедитесь, что вы используете правильный User ID или username. Для первого входа используйте User ID."},
|
||
status_code=404
|
||
)
|
||
|
||
# Check authorization (this can work even if user doesn't exist in database)
|
||
if not await is_authorized(user_id):
|
||
return JSONResponse(
|
||
{"success": False, "message": "У вас нет доступа к боту. Обратитесь к администратору."},
|
||
status_code=403
|
||
)
|
||
|
||
# If user is authorized but doesn't exist in DB - create them
|
||
if not user:
|
||
from shared.database.models import User
|
||
from datetime import datetime
|
||
from bot.utils.user_info_updater import update_user_info_from_telegram
|
||
from sqlalchemy.exc import IntegrityError
|
||
|
||
# Check if user already exists (race condition protection)
|
||
existing_user = await db.get(User, user_id)
|
||
if existing_user:
|
||
user = existing_user
|
||
else:
|
||
# Create user with basic information
|
||
user = User(
|
||
user_id=user_id,
|
||
username=None,
|
||
first_name=None,
|
||
last_name=None,
|
||
is_admin=False,
|
||
is_blocked=False,
|
||
created_at=datetime.utcnow()
|
||
)
|
||
try:
|
||
db.add(user)
|
||
await db.commit()
|
||
logger.info(f"Automatically created user {user_id} on OTP request")
|
||
except IntegrityError:
|
||
# User was created by another request, fetch it
|
||
await db.rollback()
|
||
user = await db.get(User, user_id)
|
||
if not user:
|
||
logger.error(f"Failed to create or fetch user {user_id}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при создании пользователя"},
|
||
status_code=500
|
||
)
|
||
logger.info(f"User {user_id} already exists, using existing record")
|
||
|
||
# Try to get information from Telegram API (async, doesn't block response)
|
||
try:
|
||
await update_user_info_from_telegram(user_id, db_session=db)
|
||
except Exception as e:
|
||
logger.warning(f"Failed to get user {user_id} information from Telegram: {e}")
|
||
# Continue working, information will be updated by background task later
|
||
|
||
# Generate OTP code
|
||
code = await create_otp_code(user_id, db)
|
||
if not code:
|
||
return JSONResponse(
|
||
{"success": False, "message": "Не удалось создать код. Попробуйте позже."},
|
||
status_code=500
|
||
)
|
||
|
||
# Send code to user in Telegram
|
||
sent = await send_otp_to_user(user_id, code)
|
||
if not sent:
|
||
# If failed to send, return warning
|
||
logger.warning(f"Failed to send OTP to user {user_id}, but code created")
|
||
return JSONResponse({
|
||
"success": True,
|
||
"message": f"Код создан: **{code}**. Не удалось отправить в Telegram (бот может быть не запущен). Используйте этот код для входа.",
|
||
"code": code # Return code in response for debugging
|
||
})
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"message": "Код отправлен вам в Telegram. Проверьте сообщения от бота."
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Error requesting OTP: {e}", exc_info=True)
|
||
return JSONResponse(
|
||
{"success": False, "message": f"Ошибка при создании кода: {str(e)}"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/login")
|
||
async def login(
|
||
request: Request,
|
||
otp_code: str = Form(None),
|
||
user_id: int = Form(None),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""Handle login via OTP or direct user_id (for admins)"""
|
||
# Get IP address for rate limiting
|
||
client_ip = request.client.host if request.client else None
|
||
|
||
# If OTP code provided
|
||
if otp_code:
|
||
user_id_from_otp = await verify_otp_code(otp_code, db, ip_address=client_ip)
|
||
if not user_id_from_otp:
|
||
return templates.TemplateResponse(
|
||
"login.html",
|
||
{"request": request, "error": "Неверный или истекший код"},
|
||
status_code=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
user_id = user_id_from_otp
|
||
|
||
# If user_id provided directly (alternative login method)
|
||
if user_id:
|
||
# Check user authorization
|
||
if not await is_authorized(user_id):
|
||
return templates.TemplateResponse(
|
||
"login.html",
|
||
{"request": request, "error": "У вас нет доступа к боту. Обратитесь к администратору."},
|
||
status_code=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# If user is authorized but doesn't exist in DB - create them
|
||
existing_user = await db.get(User, user_id)
|
||
if not existing_user:
|
||
from datetime import datetime
|
||
from bot.utils.user_info_updater import update_user_info_from_telegram
|
||
from sqlalchemy.exc import IntegrityError
|
||
|
||
# Create user with basic information
|
||
new_user = User(
|
||
user_id=user_id,
|
||
username=None,
|
||
first_name=None,
|
||
last_name=None,
|
||
is_admin=False,
|
||
is_blocked=False,
|
||
created_at=datetime.utcnow()
|
||
)
|
||
try:
|
||
db.add(new_user)
|
||
await db.commit()
|
||
logger.info(f"Automatically created user {user_id} on login")
|
||
except IntegrityError:
|
||
# User was created by another request, fetch it
|
||
await db.rollback()
|
||
existing_user = await db.get(User, user_id)
|
||
if not existing_user:
|
||
logger.error(f"Failed to create or fetch user {user_id}")
|
||
return templates.TemplateResponse(
|
||
"login.html",
|
||
{"request": request, "error": "Ошибка при создании пользователя"},
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
logger.info(f"User {user_id} already exists, using existing record")
|
||
|
||
# Try to get information from Telegram API (async, doesn't block response)
|
||
try:
|
||
await update_user_info_from_telegram(user_id, db_session=db)
|
||
except Exception as e:
|
||
logger.warning(f"Failed to get user {user_id} information from Telegram: {e}")
|
||
# Continue working, information will be updated by background task later
|
||
else:
|
||
return templates.TemplateResponse(
|
||
"login.html",
|
||
{"request": request, "error": "Необходимо указать код или User ID"},
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# Create session
|
||
session_id = await create_session(user_id)
|
||
|
||
# Set cookie
|
||
response = RedirectResponse(url="/admin/", status_code=status.HTTP_302_FOUND)
|
||
response.set_cookie(
|
||
key="session_id",
|
||
value=session_id,
|
||
httponly=True,
|
||
secure=False, # Set True for HTTPS in production
|
||
samesite="lax",
|
||
max_age=86400 * 7 # 7 days
|
||
)
|
||
|
||
return response
|
||
|
||
|
||
@router.get("/logout")
|
||
async def logout(request: Request):
|
||
"""Logout"""
|
||
session_id = request.cookies.get("session_id")
|
||
if session_id:
|
||
await delete_session(session_id)
|
||
|
||
response = RedirectResponse(url="/admin/login", status_code=status.HTTP_302_FOUND)
|
||
response.delete_cookie("session_id")
|
||
return response
|
||
|
||
|
||
# ==================== Protected routes ====================
|
||
|
||
@router.get("/", response_class=HTMLResponse)
|
||
async def admin_dashboard(
|
||
request: Request,
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""Main admin page (dashboard)"""
|
||
try:
|
||
# Get statistics
|
||
users_stats = await views.get_users_stats(db)
|
||
tasks_stats = await views.get_tasks_stats(db)
|
||
|
||
# Get recent tasks
|
||
recent_tasks = await views.get_all_tasks(db, limit=10)
|
||
|
||
return templates.TemplateResponse(
|
||
"dashboard.html",
|
||
{
|
||
"request": request,
|
||
"current_user": current_user,
|
||
"users_stats": users_stats,
|
||
"tasks_stats": tasks_stats,
|
||
"recent_tasks": recent_tasks,
|
||
"is_owner": current_user.get("is_owner", False),
|
||
"csrf_token": await get_csrf_token_for_template(request)
|
||
}
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при загрузке дашборда: {e}")
|
||
raise HTTPException(status_code=500, detail="Ошибка при загрузке данных")
|
||
|
||
|
||
@router.get("/users", response_class=HTMLResponse)
|
||
async def users_page(
|
||
request: Request,
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
search: Optional[str] = None
|
||
):
|
||
"""User management page"""
|
||
try:
|
||
users = await views.get_all_users(db, search=search)
|
||
is_owner = current_user.get("is_owner", False)
|
||
|
||
return templates.TemplateResponse(
|
||
"users.html",
|
||
{
|
||
"request": request,
|
||
"current_user": current_user,
|
||
"users": users,
|
||
"is_owner": is_owner,
|
||
"search": search or "",
|
||
"csrf_token": await get_csrf_token_for_template(request)
|
||
}
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при загрузке пользователей: {e}")
|
||
raise HTTPException(status_code=500, detail="Ошибка при загрузке данных")
|
||
|
||
|
||
@router.get("/tasks", response_class=HTMLResponse)
|
||
async def tasks_page(
|
||
request: Request,
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
status_filter: Optional[str] = None,
|
||
user_id: Optional[int] = None
|
||
):
|
||
"""Task management page"""
|
||
try:
|
||
tasks = await views.get_all_tasks(
|
||
db,
|
||
status_filter=status_filter,
|
||
user_id_filter=user_id
|
||
)
|
||
|
||
return templates.TemplateResponse(
|
||
"tasks.html",
|
||
{
|
||
"request": request,
|
||
"current_user": current_user,
|
||
"tasks": tasks,
|
||
"status_filter": status_filter or "",
|
||
"user_id_filter": user_id,
|
||
"csrf_token": await get_csrf_token_for_template(request)
|
||
}
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при загрузке задач: {e}")
|
||
raise HTTPException(status_code=500, detail="Ошибка при загрузке данных")
|
||
|
||
|
||
@router.get("/create-task", response_class=HTMLResponse)
|
||
async def create_task_page(
|
||
request: Request,
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""Create task page"""
|
||
try:
|
||
# Get user tasks to display limits
|
||
user_tasks = await views.get_all_tasks(db, user_id_filter=current_user["user_id"])
|
||
active_tasks = [t for t in user_tasks if t.status in ["pending", "processing"]]
|
||
|
||
# Get CSRF token for form
|
||
csrf_token = await get_csrf_token_for_template(request)
|
||
|
||
return templates.TemplateResponse(
|
||
"create_task.html",
|
||
{
|
||
"request": request,
|
||
"current_user": current_user,
|
||
"active_tasks_count": len(active_tasks),
|
||
"max_concurrent_tasks": settings.MAX_CONCURRENT_TASKS,
|
||
"max_file_size": settings.max_file_size_bytes,
|
||
"max_duration": settings.max_duration_minutes_int,
|
||
"csrf_token": csrf_token
|
||
}
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при загрузке страницы создания задачи: {e}")
|
||
raise HTTPException(status_code=500, detail="Ошибка при загрузке данных")
|
||
|
||
|
||
# ==================== API Endpoints ====================
|
||
|
||
@router.post("/api/users/add")
|
||
async def api_add_user(
|
||
request: Request,
|
||
user_id: int = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Add user"""
|
||
# CSRF token check
|
||
await verify_csrf(request, csrf_token)
|
||
|
||
try:
|
||
result = await add_user_web(user_id, db)
|
||
if result["success"]:
|
||
return JSONResponse({"success": True, "message": result["message"]})
|
||
else:
|
||
return JSONResponse(
|
||
{"success": False, "message": result["message"]},
|
||
status_code=400
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при добавлении пользователя: {e}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при добавлении пользователя"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/api/users/remove")
|
||
async def api_remove_user(
|
||
request: Request,
|
||
user_id: int = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Remove user"""
|
||
# CSRF token check
|
||
await verify_csrf(request, csrf_token)
|
||
|
||
try:
|
||
result = await remove_user_web(user_id, db)
|
||
if result["success"]:
|
||
return JSONResponse({"success": True, "message": result["message"]})
|
||
else:
|
||
return JSONResponse(
|
||
{"success": False, "message": result["message"]},
|
||
status_code=400
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при удалении пользователя: {e}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при удалении пользователя"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/api/users/block")
|
||
async def api_block_user(
|
||
request: Request,
|
||
user_id: int = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Block user"""
|
||
# CSRF token check
|
||
await verify_csrf(request, csrf_token)
|
||
|
||
try:
|
||
result = await block_user_web(user_id, db)
|
||
if result["success"]:
|
||
return JSONResponse({"success": True, "message": result["message"]})
|
||
else:
|
||
return JSONResponse(
|
||
{"success": False, "message": result["message"]},
|
||
status_code=400
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при блокировке пользователя: {e}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при блокировке пользователя"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/api/users/unblock")
|
||
async def api_unblock_user(
|
||
request: Request,
|
||
user_id: int = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Unblock user"""
|
||
# CSRF token check
|
||
await verify_csrf(request, csrf_token)
|
||
|
||
try:
|
||
result = await unblock_user_web(user_id, db)
|
||
if result["success"]:
|
||
return JSONResponse({"success": True, "message": result["message"]})
|
||
else:
|
||
return JSONResponse(
|
||
{"success": False, "message": result["message"]},
|
||
status_code=400
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при разблокировке пользователя: {e}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при разблокировке пользователя"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/api/users/add-admin")
|
||
async def api_add_admin(
|
||
request: Request,
|
||
user_id: int = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Add administrator (Owner only)"""
|
||
# CSRF token check
|
||
await verify_csrf(request, csrf_token)
|
||
|
||
if not current_user.get("is_owner", False):
|
||
return JSONResponse(
|
||
{"success": False, "message": "Только Owner может добавлять администраторов"},
|
||
status_code=403
|
||
)
|
||
|
||
try:
|
||
result = await add_admin_web(user_id, db)
|
||
if result["success"]:
|
||
return JSONResponse({"success": True, "message": result["message"]})
|
||
else:
|
||
return JSONResponse(
|
||
{"success": False, "message": result["message"]},
|
||
status_code=400
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при добавлении администратора: {e}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при добавлении администратора"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/api/users/remove-admin")
|
||
async def api_remove_admin(
|
||
request: Request,
|
||
user_id: int = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Remove administrator (Owner only)"""
|
||
# CSRF token check
|
||
await verify_csrf(request, csrf_token)
|
||
|
||
if not current_user.get("is_owner", False):
|
||
return JSONResponse(
|
||
{"success": False, "message": "Только Owner может удалять администраторов"},
|
||
status_code=403
|
||
)
|
||
|
||
try:
|
||
result = await remove_admin_web(user_id, db)
|
||
if result["success"]:
|
||
return JSONResponse({"success": True, "message": result["message"]})
|
||
else:
|
||
return JSONResponse(
|
||
{"success": False, "message": result["message"]},
|
||
status_code=400
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при удалении администратора: {e}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при удалении администратора"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.post("/api/tasks/create")
|
||
async def api_create_task(
|
||
request: Request,
|
||
url: str = Form(...),
|
||
csrf_token: Optional[str] = Form(None),
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Create download task"""
|
||
# CSRF token check
|
||
try:
|
||
await verify_csrf(request, csrf_token)
|
||
except HTTPException as e:
|
||
logger.warning(f"CSRF check failed for task creation: {e.detail}")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Невалидный CSRF токен"},
|
||
status_code=403
|
||
)
|
||
|
||
from bot.modules.task_scheduler.executor import task_executor, set_app_client
|
||
from bot.modules.access_control.auth import is_authorized
|
||
from bot.utils.helpers import is_valid_url
|
||
|
||
user_id = current_user["user_id"]
|
||
|
||
# URL validation
|
||
if not is_valid_url(url):
|
||
return JSONResponse(
|
||
{"success": False, "message": "Некорректный или небезопасный URL"},
|
||
status_code=400
|
||
)
|
||
|
||
# Check authorization
|
||
if not await is_authorized(user_id):
|
||
return JSONResponse(
|
||
{"success": False, "message": "У вас нет доступа к боту"},
|
||
status_code=403
|
||
)
|
||
|
||
try:
|
||
# Check concurrent tasks count
|
||
user_tasks = await views.get_all_tasks(db, user_id_filter=user_id)
|
||
active_tasks = [t for t in user_tasks if t.status in ["pending", "processing"]]
|
||
if len(active_tasks) >= settings.MAX_CONCURRENT_TASKS:
|
||
return JSONResponse(
|
||
{
|
||
"success": False,
|
||
"message": f"Превышен лимит одновременных задач ({settings.MAX_CONCURRENT_TASKS})"
|
||
},
|
||
status_code=400
|
||
)
|
||
|
||
# Duplicate URL check will be performed atomically in task_queue.add_task()
|
||
url_normalized = url.strip()
|
||
|
||
# Get media information to check limits
|
||
# If getting info takes too long, skip check
|
||
media_info = None
|
||
try:
|
||
from shared.config import settings
|
||
media_info = await asyncio.wait_for(
|
||
get_media_info(url, cookies_file=settings.COOKIES_FILE),
|
||
timeout=10.0
|
||
)
|
||
except asyncio.TimeoutError:
|
||
logger.warning(f"Timeout getting media info for URL: {url}")
|
||
media_info = None
|
||
except Exception as e:
|
||
logger.warning(f"Failed to get media info: {e}")
|
||
media_info = None
|
||
|
||
if media_info:
|
||
# Check duration
|
||
max_duration = settings.max_duration_minutes_int
|
||
if max_duration and media_info.get('duration'):
|
||
duration_minutes = media_info['duration'] / 60
|
||
if duration_minutes > max_duration:
|
||
return JSONResponse(
|
||
{
|
||
"success": False,
|
||
"message": f"Файл слишком длинный ({duration_minutes:.1f} мин). Максимум: {max_duration} мин."
|
||
},
|
||
status_code=400
|
||
)
|
||
|
||
# Generate unique task_id using UUID
|
||
from bot.utils.helpers import generate_unique_task_id
|
||
task_id = generate_unique_task_id()
|
||
|
||
# Check that such ID doesn't exist yet (in case of collision, though probability is extremely low)
|
||
existing_task = await task_queue.get_task_by_id(task_id)
|
||
max_retries = 10
|
||
retries = 0
|
||
while existing_task and retries < max_retries:
|
||
task_id = generate_unique_task_id()
|
||
existing_task = await task_queue.get_task_by_id(task_id)
|
||
retries += 1
|
||
|
||
if existing_task:
|
||
# If after 10 attempts still collision (extremely unlikely), return error
|
||
logger.error(f"Failed to generate unique task_id after {max_retries} attempts")
|
||
return JSONResponse(
|
||
{"success": False, "message": "Ошибка при создании задачи. Попробуйте позже."},
|
||
status_code=500
|
||
)
|
||
|
||
task = QueueTask(
|
||
id=task_id,
|
||
user_id=user_id,
|
||
task_type="download",
|
||
url=url_normalized,
|
||
status=TaskStatus.PENDING
|
||
)
|
||
|
||
# Save task to database BEFORE adding to queue (race condition fix)
|
||
try:
|
||
from shared.database.models import Task as DBTask
|
||
from shared.database.user_helpers import ensure_user_exists
|
||
from datetime import datetime
|
||
from sqlalchemy.exc import IntegrityError
|
||
|
||
# Ensure user exists before creating task
|
||
await ensure_user_exists(user_id, db)
|
||
|
||
db_task = DBTask(
|
||
id=task_id,
|
||
user_id=user_id,
|
||
task_type=task.task_type,
|
||
status=task.status.value,
|
||
url=task.url,
|
||
progress=0,
|
||
created_at=datetime.utcnow(),
|
||
updated_at=datetime.utcnow()
|
||
)
|
||
db.add(db_task)
|
||
await db.commit()
|
||
logger.info(f"Task {task_id} saved to database")
|
||
except IntegrityError as e:
|
||
await db.rollback()
|
||
logger.error(f"IntegrityError saving task {task_id} to database (possibly duplicate ID): {e}", exc_info=True)
|
||
# Generate new task_id and retry
|
||
from bot.utils.helpers import generate_unique_task_id
|
||
task_id = generate_unique_task_id()
|
||
task.id = task_id
|
||
try:
|
||
from shared.database.user_helpers import ensure_user_exists
|
||
# Ensure user exists before creating task
|
||
await ensure_user_exists(user_id, db)
|
||
|
||
db_task = DBTask(
|
||
id=task_id,
|
||
user_id=user_id,
|
||
task_type=task.task_type,
|
||
status=task.status.value,
|
||
url=task.url,
|
||
progress=0,
|
||
created_at=datetime.utcnow(),
|
||
updated_at=datetime.utcnow()
|
||
)
|
||
db.add(db_task)
|
||
await db.commit()
|
||
logger.info(f"Task {task_id} saved to database with new ID")
|
||
except Exception as e2:
|
||
await db.rollback()
|
||
logger.error(f"Error saving task {task_id} to database again: {e2}", exc_info=True)
|
||
return JSONResponse(
|
||
{"success": False, "message": f"Ошибка при создании задачи: {str(e2)}"},
|
||
status_code=500
|
||
)
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"Error saving task {task_id} to database: {e}", exc_info=True)
|
||
return JSONResponse(
|
||
{"success": False, "message": f"Ошибка при создании задачи: {str(e)}"},
|
||
status_code=500
|
||
)
|
||
|
||
# Add to queue (with duplicate URL check) AFTER saving to database
|
||
success = await task_queue.add_task(task, check_duplicate_url=True)
|
||
if not success:
|
||
# If failed to add to queue, remove from database
|
||
try:
|
||
await db.delete(db_task)
|
||
await db.commit()
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"Error deleting task {task_id} from database after failed queue addition: {e}")
|
||
return JSONResponse(
|
||
{
|
||
"success": False,
|
||
"message": "Задача с этим URL уже обрабатывается. Дождитесь завершения или отмените предыдущую задачу."
|
||
},
|
||
status_code=400
|
||
)
|
||
|
||
# Start executor if needed
|
||
from bot.modules.task_scheduler.executor import get_app_client
|
||
app_client = get_app_client()
|
||
if app_client:
|
||
set_app_client(app_client)
|
||
if not task_executor._running:
|
||
await task_executor.start()
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"message": "Задача успешно создана и добавлена в очередь",
|
||
"task_id": task_id
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Error creating task: {e}", exc_info=True)
|
||
return JSONResponse(
|
||
{"success": False, "message": f"Ошибка при создании задачи: {str(e)}"},
|
||
status_code=500
|
||
)
|
||
|
||
|
||
@router.get("/api/stats")
|
||
async def api_stats(
|
||
request: Request,
|
||
current_user: dict = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""API: Get statistics"""
|
||
try:
|
||
users_stats = await views.get_users_stats(db)
|
||
tasks_stats = await views.get_tasks_stats(db)
|
||
return JSONResponse({
|
||
"users": users_stats,
|
||
"tasks": tasks_stats
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении статистики: {e}")
|
||
return JSONResponse(
|
||
{"error": "Ошибка при получении статистики"},
|
||
status_code=500
|
||
)
|