nakama/moderation/backend-py/routes/mod_app.py

1170 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Moderation app routes - main moderation functionality
"""
import secrets
from datetime import datetime, timedelta
from typing import Optional, List
from fastapi import APIRouter, HTTPException, status, Depends, Query
from bson import ObjectId
from models import (
UserResponse, PostResponse, ReportResponse, AdminResponse,
UpdatePostRequest, UpdateReportRequest, BanUserRequest
)
from database import (
users_collection, posts_collection, reports_collection,
moderation_admins_collection
)
from utils.auth import require_moderator, require_owner, normalize_username
router = APIRouter()
@router.get("/users")
async def get_users(
filter: str = Query('active', description="Filter: active, banned, all"),
page: int = Query(1, ge=1),
limit: int = Query(50, ge=1, le=100),
user: dict = Depends(require_moderator)
):
"""Get users list with filtering"""
try:
query = {}
if filter == 'active':
# Активные пользователи: не забанены И активны за последние 7 дней
seven_days_ago = datetime.utcnow() - timedelta(days=7)
query['banned'] = {'$ne': True}
query['lastActiveAt'] = {'$gte': seven_days_ago, '$exists': True, '$ne': None}
elif filter == 'inactive':
# Неактивные пользователи: не забанены И не активны более 7 дней ИЛИ нет lastActiveAt
seven_days_ago = datetime.utcnow() - timedelta(days=7)
query['banned'] = {'$ne': True}
query['$or'] = [
{'lastActiveAt': {'$lt': seven_days_ago}},
{'lastActiveAt': {'$exists': False}},
{'lastActiveAt': None}
]
elif filter == 'banned':
query['banned'] = True
# 'all' - no filter
skip = (page - 1) * limit
# Get users - сортируем по дате последнего входа (lastActiveAt)
# Для активных и неактивных сортируем по lastActiveAt, для остальных по createdAt
if filter in ['active', 'inactive']:
# Сортируем по lastActiveAt (сначала самые активные/недавние)
# Пользователи без lastActiveAt идут в конец
cursor = users_collection().find(query).sort([
('lastActiveAt', -1), # Сначала по lastActiveAt (убывание)
('createdAt', -1) # Потом по createdAt для пользователей без lastActiveAt
]).skip(skip).limit(limit)
else:
# Для banned и all сортируем по createdAt
cursor = users_collection().find(query).sort('createdAt', -1).skip(skip).limit(limit)
users = await cursor.to_list(length=limit)
# Get total count
total = await users_collection().count_documents(query)
# Serialize users
serialized_users = []
for u in users:
# Проверяем, является ли пользователь админом модерации
is_admin = False
if u.get('telegramId'):
admin = await moderation_admins_collection().find_one({
'telegramId': str(u.get('telegramId'))
})
is_admin = admin is not None
# Обработка дат
banned_until = None
if u.get('bannedUntil'):
if isinstance(u.get('bannedUntil'), datetime):
banned_until = u.get('bannedUntil').isoformat()
else:
banned_until = str(u.get('bannedUntil'))
created_at = datetime.utcnow().isoformat()
if u.get('createdAt'):
if isinstance(u.get('createdAt'), datetime):
created_at = u.get('createdAt').isoformat()
else:
created_at = str(u.get('createdAt'))
last_active_at = None
if u.get('lastActiveAt'):
if isinstance(u.get('lastActiveAt'), datetime):
last_active_at = u.get('lastActiveAt').isoformat()
else:
last_active_at = str(u.get('lastActiveAt'))
serialized_users.append({
'id': str(u['_id']),
'telegramId': str(u.get('telegramId')) if u.get('telegramId') else None,
'username': u.get('username', ''),
'firstName': u.get('firstName', ''),
'lastName': u.get('lastName', ''),
'photoUrl': u.get('photoUrl', ''),
'role': u.get('role', 'user'),
'banned': bool(u.get('banned', False)),
'bannedUntil': banned_until,
'createdAt': created_at,
'lastActiveAt': last_active_at,
'isAdmin': is_admin
})
return {
'users': serialized_users,
'total': total,
'totalPages': (total + limit - 1) // limit,
'currentPage': page
}
except Exception as e:
print(f"[ModApp] ❌ Ошибка получения пользователей: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Ошибка сервера: {str(e)}"
)
@router.put("/users/{user_id}/ban")
async def ban_user(
user_id: str,
request: BanUserRequest,
current_user: dict = Depends(require_moderator)
):
"""Ban or unban user"""
try:
target_user = await users_collection().find_one({'_id': ObjectId(user_id)})
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
update_data = {'banned': request.banned}
if request.banned and request.days:
update_data['bannedUntil'] = datetime.utcnow() + timedelta(days=request.days)
elif not request.banned:
update_data['bannedUntil'] = None
await users_collection().update_one(
{'_id': ObjectId(user_id)},
{'$set': update_data}
)
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка бана пользователя: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/posts")
async def get_posts(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
author: Optional[str] = None,
tag: Optional[str] = None,
user: dict = Depends(require_moderator)
):
"""Get posts list with filtering"""
try:
query = {}
if author:
query['author'] = ObjectId(author)
if tag:
query['tags'] = tag
skip = (page - 1) * limit
# Get posts with populated author
pipeline = [
{'$match': query},
{'$sort': {'createdAt': -1}},
{'$skip': skip},
{'$limit': limit},
{
'$lookup': {
'from': 'users',
'localField': 'author',
'foreignField': '_id',
'as': 'author'
}
},
{'$unwind': {'path': '$author', 'preserveNullAndEmptyArrays': True}}
]
posts = await posts_collection().aggregate(pipeline).to_list(length=limit)
total = await posts_collection().count_documents(query)
# Serialize posts
serialized_posts = []
for post in posts:
author_data = None
if post.get('author'):
author_data = {
'id': str(post['author']['_id']),
'username': post['author'].get('username'),
'firstName': post['author'].get('firstName'),
'lastName': post['author'].get('lastName'),
'photoUrl': post['author'].get('photoUrl')
}
serialized_posts.append({
'id': str(post['_id']),
'author': author_data,
'content': post.get('content'),
'hashtags': post.get('hashtags', []),
'tags': post.get('tags', []),
'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []),
'commentsCount': len(post.get('comments', [])),
'likesCount': len(post.get('likes', [])),
'isNSFW': post.get('isNSFW', False),
'isArt': post.get('isArt', False),
'publishedToChannel': post.get('publishedToChannel', False),
'adminNumber': post.get('adminNumber'),
'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None,
'createdAt': post.get('createdAt', datetime.utcnow()).isoformat()
})
return {
'posts': serialized_posts,
'total': total,
'totalPages': (total + limit - 1) // limit,
'currentPage': page
}
except Exception as e:
print(f"[ModApp] Ошибка получения постов: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/posts/{post_id}")
async def get_post(
post_id: str,
user: dict = Depends(require_moderator)
):
"""Get single post with comments"""
try:
pipeline = [
{'$match': {'_id': ObjectId(post_id)}},
{
'$lookup': {
'from': 'users',
'localField': 'author',
'foreignField': '_id',
'as': 'author'
}
},
{'$unwind': {'path': '$author', 'preserveNullAndEmptyArrays': True}}
]
posts = await posts_collection().aggregate(pipeline).to_list(length=1)
if not posts:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
post = posts[0]
# Serialize
author_data = None
if post.get('author'):
author_data = {
'id': str(post['author']['_id']),
'username': post['author'].get('username'),
'firstName': post['author'].get('firstName'),
'lastName': post['author'].get('lastName'),
'photoUrl': post['author'].get('photoUrl')
}
# Serialize comments
comments_serialized = []
for comment in post.get('comments', []):
comment_author_id = comment.get('author')
if isinstance(comment_author_id, ObjectId):
comment_author_id = str(comment_author_id)
elif isinstance(comment_author_id, dict):
comment_author_id = str(comment_author_id.get('_id', ''))
comments_serialized.append({
'id': str(comment.get('_id', '')),
'author': comment_author_id,
'content': comment.get('content'),
'createdAt': comment.get('createdAt', datetime.utcnow()).isoformat() if comment.get('createdAt') else None,
'editedAt': comment.get('editedAt').isoformat() if comment.get('editedAt') else None
})
# Serialize likes (convert ObjectIds to strings)
likes_serialized = []
for like in post.get('likes', []):
if isinstance(like, ObjectId):
likes_serialized.append(str(like))
elif isinstance(like, dict):
likes_serialized.append(str(like.get('_id', '')))
else:
likes_serialized.append(str(like))
return {
'post': {
'id': str(post['_id']),
'author': author_data,
'content': post.get('content'),
'hashtags': post.get('hashtags', []),
'tags': post.get('tags', []),
'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []),
'comments': comments_serialized,
'likes': likes_serialized,
'commentsCount': len(comments_serialized),
'likesCount': len(likes_serialized),
'isNSFW': post.get('isNSFW', False),
'isHomo': post.get('isHomo', False),
'isArt': post.get('isArt', False),
'publishedToChannel': post.get('publishedToChannel', False),
'adminNumber': post.get('adminNumber'),
'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None,
'createdAt': post.get('createdAt', datetime.utcnow()).isoformat()
}
}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка получения поста: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.put("/posts/{post_id}")
async def update_post(
post_id: str,
request: UpdatePostRequest,
user: dict = Depends(require_moderator)
):
"""Update post"""
try:
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
# Модераторы и админы могут редактировать любой пост через панель модерации
# Доступ к панели модерации уже проверен через require_moderator
update_data = {}
if request.content is not None:
update_data['content'] = request.content
# Обновить хэштеги из контента, если hashtags не переданы явно
if request.hashtags is not None:
update_data['hashtags'] = [tag.replace('#', '').lower() if tag.startswith('#') else tag.lower() for tag in request.hashtags]
else:
# Извлечь хэштеги из контента (поддержка кириллицы)
import re
hashtags = re.findall(r'#([\wа-яА-ЯёЁ]+)', request.content)
update_data['hashtags'] = [tag.lower() for tag in hashtags]
if request.tags is not None:
update_data['tags'] = [t.lower() for t in request.tags]
if request.isNSFW is not None:
update_data['isNSFW'] = request.isNSFW
if request.isHomo is not None:
update_data['isHomo'] = request.isHomo
if request.isArt is not None:
update_data['isArt'] = request.isArt
if update_data:
update_data['editedAt'] = datetime.utcnow()
await posts_collection().update_one(
{'_id': ObjectId(post_id)},
{'$set': update_data}
)
# Get updated post with author
updated_post = await posts_collection().find_one({'_id': ObjectId(post_id)})
author = None
if updated_post.get('author'):
author_doc = await users_collection().find_one({'_id': updated_post['author']})
if author_doc:
author = {
'id': str(author_doc['_id']),
'username': author_doc.get('username'),
'firstName': author_doc.get('firstName'),
'lastName': author_doc.get('lastName')
}
return {
"post": {
"id": str(updated_post['_id']),
"author": author,
"content": updated_post.get('content'),
"hashtags": updated_post.get('hashtags', []),
"tags": updated_post.get('tags', []),
"images": updated_post.get('images', []) or ([updated_post.get('imageUrl')] if updated_post.get('imageUrl') else []),
"isNSFW": updated_post.get('isNSFW', False),
"isArt": updated_post.get('isArt', False),
"publishedToChannel": updated_post.get('publishedToChannel', False),
"adminNumber": updated_post.get('adminNumber'),
"editedAt": updated_post.get('editedAt').isoformat() if updated_post.get('editedAt') else None,
"createdAt": updated_post.get('createdAt', datetime.utcnow()).isoformat()
}
}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка обновления поста: {e}")
import traceback
traceback.print_exc()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.delete("/posts/{post_id}/comments/{comment_id}")
async def delete_comment(
post_id: str,
comment_id: str,
user: dict = Depends(require_moderator)
):
"""Delete comment from post"""
try:
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
# Find and remove comment
comments = post.get('comments', [])
comment_found = False
for i, comment in enumerate(comments):
if str(comment.get('_id')) == comment_id:
comments.pop(i)
comment_found = True
break
if not comment_found:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Комментарий не найден"
)
# Update post
await posts_collection().update_one(
{'_id': ObjectId(post_id)},
{'$set': {'comments': comments}}
)
# Get updated comments with populated authors
updated_post = await posts_collection().find_one({'_id': ObjectId(post_id)})
comments_serialized = []
for comment in updated_post.get('comments', []):
comment_author_id = comment.get('author')
if isinstance(comment_author_id, ObjectId):
# Populate author
author = await users_collection().find_one({'_id': comment_author_id})
if author:
comments_serialized.append({
'id': str(comment.get('_id', '')),
'author': {
'id': str(author['_id']),
'username': author.get('username'),
'firstName': author.get('firstName'),
'lastName': author.get('lastName'),
'photoUrl': author.get('photoUrl')
},
'content': comment.get('content'),
'createdAt': comment.get('createdAt', datetime.utcnow()).isoformat() if comment.get('createdAt') else None
})
return {"comments": comments_serialized}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка удаления комментария: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.delete("/posts/{post_id}/images/{index}")
async def delete_post_image(
post_id: str,
index: int,
user: dict = Depends(require_moderator)
):
"""Delete specific image from post"""
try:
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
images = post.get('images', []) or []
if index < 0 or index >= len(images):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный индекс изображения"
)
# Remove image
removed_image = images.pop(index)
# Update post
update_data = {'images': images}
if images:
update_data['imageUrl'] = images[0] # Set first image as main
else:
update_data['imageUrl'] = None
await posts_collection().update_one(
{'_id': ObjectId(post_id)},
{'$set': update_data}
)
# Delete from MinIO
if removed_image and 'nakama-media/' in removed_image:
from utils.minio_client import delete_file
try:
path_match = removed_image.split('nakama-media/')
if len(path_match) > 1:
file_path = path_match[-1]
await delete_file(file_path)
print(f"✅ Удалено изображение из MinIO: {file_path}")
except Exception as e:
print(f"❌ Ошибка удаления изображения из MinIO: {e}")
return {"images": images}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка удаления изображения: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/posts/{post_id}/ban")
async def ban_post_author(
post_id: str,
request: dict,
user: dict = Depends(require_moderator)
):
"""Ban post author"""
try:
days = request.get('days', 7)
duration_days = max(int(days) if days else 7, 1)
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
author_id = post.get('author')
if not author_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Автор поста не найден"
)
# Ban author
await users_collection().update_one(
{'_id': author_id},
{
'$set': {
'banned': True,
'bannedUntil': datetime.utcnow() + timedelta(days=duration_days)
}
}
)
# Get updated user
banned_user = await users_collection().find_one({'_id': author_id})
return {
"user": {
"id": str(banned_user['_id']),
"username": banned_user.get('username'),
"firstName": banned_user.get('firstName'),
"lastName": banned_user.get('lastName'),
"banned": banned_user.get('banned', False),
"bannedUntil": banned_user.get('bannedUntil').isoformat() if banned_user.get('bannedUntil') else None
}
}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка бана автора: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.delete("/posts/{post_id}")
async def delete_post(
post_id: str,
user: dict = Depends(require_moderator)
):
"""Delete post"""
try:
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
# Delete images from MinIO if needed
from utils.minio_client import delete_file
images = post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else [])
for image_url in images:
if image_url and 'nakama-media/' in image_url:
try:
# Extract path from URL
path_match = image_url.split('nakama-media/')
if len(path_match) > 1:
file_path = path_match[-1]
await delete_file(file_path)
print(f"✅ Удалено изображение из MinIO: {file_path}")
except Exception as e:
print(f"❌ Ошибка удаления изображения из MinIO: {e}")
# Delete post
await posts_collection().delete_one({'_id': ObjectId(post_id)})
return {"success": True, "message": "Пост удален"}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка удаления поста: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/reports")
async def get_reports(
status_filter: str = Query('pending', alias='status'),
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
user: dict = Depends(require_moderator)
):
"""Get reports list"""
try:
query = {}
if status_filter != 'all':
query['status'] = status_filter
skip = (page - 1) * limit
# Get reports with populated fields
pipeline = [
{'$match': query},
{'$sort': {'createdAt': -1}},
{'$skip': skip},
{'$limit': limit},
{
'$lookup': {
'from': 'users',
'localField': 'reporter',
'foreignField': '_id',
'as': 'reporter'
}
},
{'$unwind': {'path': '$reporter', 'preserveNullAndEmptyArrays': True}},
{
'$lookup': {
'from': 'posts',
'localField': 'post',
'foreignField': '_id',
'as': 'post'
}
},
{'$unwind': {'path': '$post', 'preserveNullAndEmptyArrays': True}}
]
reports = await reports_collection().aggregate(pipeline).to_list(length=limit)
total = await reports_collection().count_documents(query)
# Serialize
serialized_reports = []
for report in reports:
reporter_data = None
if report.get('reporter'):
reporter_data = {
'id': str(report['reporter']['_id']),
'username': report['reporter'].get('username'),
'firstName': report['reporter'].get('firstName')
}
post_data = None
if report.get('post'):
post_data = {
'id': str(report['post']['_id']),
'content': report['post'].get('content'),
'images': report['post'].get('images', [])
}
serialized_reports.append({
'id': str(report['_id']),
'reporter': reporter_data,
'post': post_data,
'reason': report.get('reason'),
'status': report.get('status'),
'createdAt': report.get('createdAt', datetime.utcnow()).isoformat()
})
return {
'reports': serialized_reports,
'total': total,
'totalPages': (total + limit - 1) // limit,
'currentPage': page
}
except Exception as e:
print(f"[ModApp] Ошибка получения репортов: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.put("/reports/{report_id}")
async def update_report(
report_id: str,
request: UpdateReportRequest,
user: dict = Depends(require_moderator)
):
"""Update report status"""
try:
report = await reports_collection().find_one({'_id': ObjectId(report_id)})
if not report:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Репорт не найден"
)
await reports_collection().update_one(
{'_id': ObjectId(report_id)},
{
'$set': {
'status': request.status,
'reviewedBy': user['_id']
}
}
)
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка обновления репорта: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/admins")
async def get_admins(user: dict = Depends(require_moderator)):
"""Get list of moderation admins"""
try:
admins = await moderation_admins_collection().find().sort('adminNumber', 1).to_list(length=100)
serialized_admins = []
for admin in admins:
serialized_admins.append({
'id': str(admin['_id']),
'telegramId': admin.get('telegramId'),
'username': admin.get('username'),
'firstName': admin.get('firstName'),
'lastName': admin.get('lastName'),
'adminNumber': admin.get('adminNumber'),
'addedBy': admin.get('addedBy'),
'createdAt': admin.get('createdAt', datetime.utcnow()).isoformat()
})
return {'admins': serialized_admins}
except Exception as e:
print(f"[ModApp] Ошибка получения админов: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка получения списка админов"
)
@router.post("/admins/initiate-add")
async def initiate_add_admin(
request: dict,
user: dict = Depends(require_owner)
):
"""Initiate adding admin (owner only)"""
try:
user_id = request.get('userId')
admin_number = request.get('adminNumber')
if not user_id or not admin_number:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Не указан ID пользователя или номер админа"
)
if admin_number < 1 or admin_number > 10:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Номер админа должен быть от 1 до 10"
)
# Check if number is taken
existing_admin = await moderation_admins_collection().find_one({'adminNumber': admin_number})
if existing_admin:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Номер админа уже занят"
)
# Check if user exists
target_user = await users_collection().find_one({'_id': ObjectId(user_id)})
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
# Check if already admin
is_already_admin = await moderation_admins_collection().find_one({
'telegramId': target_user.get('telegramId')
})
if is_already_admin:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь уже является админом"
)
# Generate code
code = str(secrets.randbelow(900000) + 100000)
# Save confirmation
from database import admin_confirmations_collection
await admin_confirmations_collection().insert_one({
'userId': target_user.get('telegramId'),
'code': code,
'adminNumber': admin_number,
'action': 'add',
'expiresAt': datetime.utcnow() + timedelta(minutes=5),
'createdAt': datetime.utcnow()
})
# Send code to owner email
from utils.email_service import send_admin_confirmation_code
try:
await send_admin_confirmation_code(
code,
'add',
{
'username': target_user.get('username'),
'firstName': target_user.get('firstName'),
'adminNumber': admin_number
}
)
except Exception as email_error:
print(f'[ModApp] Ошибка отправки кода на email: {email_error}')
# Continue anyway - code is saved
return {
"success": True,
"message": "Код подтверждения отправлен на email владельца",
"username": target_user.get('username')
}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка инициирования добавления админа: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка отправки кода подтверждения"
)
@router.post("/admins/confirm-add")
async def confirm_add_admin(
request: dict,
user: dict = Depends(require_owner)
):
"""Confirm adding admin (owner only)"""
try:
user_id = request.get('userId')
code = request.get('code')
if not user_id or not code:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Не указан ID пользователя или код"
)
target_user = await users_collection().find_one({'_id': ObjectId(user_id)})
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
# Find confirmation
from database import admin_confirmations_collection
confirmation = await admin_confirmations_collection().find_one({
'userId': target_user.get('telegramId'),
'code': code,
'action': 'add'
})
if not confirmation:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный код подтверждения"
)
# Check expiration
if datetime.utcnow() > confirmation['expiresAt']:
await admin_confirmations_collection().delete_one({'_id': confirmation['_id']})
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Код истек"
)
# Create admin
await moderation_admins_collection().insert_one({
'telegramId': target_user.get('telegramId'),
'username': target_user.get('username'),
'firstName': target_user.get('firstName'),
'lastName': target_user.get('lastName'),
'adminNumber': confirmation['adminNumber'],
'addedBy': str(user['_id']),
'createdAt': datetime.utcnow()
})
# Delete confirmation
await admin_confirmations_collection().delete_one({'_id': confirmation['_id']})
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка подтверждения добавления админа: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка добавления админа"
)
@router.post("/admins/initiate-remove")
async def initiate_remove_admin(
request: dict,
user: dict = Depends(require_owner)
):
"""Initiate removing admin (owner only)"""
try:
admin_id = request.get('adminId')
if not admin_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Не указан ID админа"
)
admin = await moderation_admins_collection().find_one({'_id': ObjectId(admin_id)})
if not admin:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Администратор не найден"
)
# Generate code
code = str(secrets.randbelow(900000) + 100000)
# Save confirmation
from database import admin_confirmations_collection
await admin_confirmations_collection().insert_one({
'userId': admin.get('telegramId'),
'code': code,
'adminNumber': admin.get('adminNumber'),
'action': 'remove',
'expiresAt': datetime.utcnow() + timedelta(minutes=5),
'createdAt': datetime.utcnow()
})
# Send code to owner email
from utils.email_service import send_admin_confirmation_code
try:
await send_admin_confirmation_code(
code,
'remove',
{
'username': admin.get('username'),
'firstName': admin.get('firstName'),
'adminNumber': admin.get('adminNumber')
}
)
except Exception as email_error:
print(f'[ModApp] Ошибка отправки кода на email: {email_error}')
return {
"success": True,
"message": "Код подтверждения отправлен на email владельца",
"username": admin.get('username')
}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка инициирования удаления админа: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка отправки кода подтверждения"
)
@router.post("/admins/confirm-remove")
async def confirm_remove_admin(
request: dict,
user: dict = Depends(require_owner)
):
"""Confirm removing admin (owner only)"""
try:
admin_id = request.get('adminId')
code = request.get('code')
if not admin_id or not code:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Не указан ID админа или код"
)
admin = await moderation_admins_collection().find_one({'_id': ObjectId(admin_id)})
if not admin:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Администратор не найден"
)
# Find confirmation
from database import admin_confirmations_collection
confirmation = await admin_confirmations_collection().find_one({
'userId': admin.get('telegramId'),
'code': code,
'action': 'remove'
})
if not confirmation:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный код подтверждения"
)
# Delete admin
await moderation_admins_collection().delete_one({'_id': ObjectId(admin_id)})
# Delete confirmation
await admin_confirmations_collection().delete_one({'_id': confirmation['_id']})
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка подтверждения удаления админа: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка удаления админа"
)
@router.post("/auth/verify")
async def verify_auth(user: dict = Depends(require_moderator)):
"""Verify authentication and get admin list"""
try:
# Get admin list
admins = await moderation_admins_collection().find().sort('adminNumber', 1).to_list(length=100)
admin_list = []
for admin in admins:
admin_list.append({
'adminNumber': admin.get('adminNumber'),
'username': admin.get('username'),
'firstName': admin.get('firstName'),
'telegramId': admin.get('telegramId')
})
return {
'success': True,
'admins': admin_list,
'user': {
'id': str(user['_id']),
'username': user.get('username'),
'role': user.get('role')
}
}
except Exception as e:
print(f"[ModApp] Ошибка верификации: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)