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

539 lines
18 KiB
Python
Raw Normal View History

2025-12-14 23:45:41 +00:00
"""
Moderation app routes - main moderation functionality
"""
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':
query['banned'] = {'$ne': True}
elif filter == 'banned':
query['banned'] = True
# 'all' - no filter
skip = (page - 1) * limit
# Get users
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:
serialized_users.append({
'id': str(u['_id']),
'telegramId': u.get('telegramId'),
'username': u.get('username'),
'firstName': u.get('firstName'),
'lastName': u.get('lastName'),
'photoUrl': u.get('photoUrl'),
'role': u.get('role', 'user'),
'banned': u.get('banned', False),
'bannedUntil': u.get('bannedUntil').isoformat() if u.get('bannedUntil') else None,
'createdAt': u.get('createdAt', datetime.utcnow()).isoformat(),
'lastActiveAt': u.get('lastActiveAt').isoformat() if u.get('lastActiveAt') else None
})
return {
'users': serialized_users,
'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("/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')
}
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': post.get('comments', []),
'likes': post.get('likes', []),
'isNSFW': post.get('isNSFW', False),
'isArt': post.get('isArt', False),
'publishedToChannel': post.get('publishedToChannel', False),
'adminNumber': post.get('adminNumber'),
'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="Пост не найден"
)
update_data = {}
if request.content is not None:
update_data['content'] = request.content
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}
)
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.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 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("/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="Ошибка сервера"
)