Update files
This commit is contained in:
parent
4efdeb62ff
commit
3be93d1353
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
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
|
||||
|
|
@ -242,6 +243,33 @@ async def get_post(
|
|||
'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']),
|
||||
|
|
@ -250,12 +278,16 @@ async def get_post(
|
|||
'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', []),
|
||||
'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()
|
||||
}
|
||||
}
|
||||
|
|
@ -318,6 +350,203 @@ async def update_post(
|
|||
)
|
||||
|
||||
|
||||
@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,
|
||||
|
|
@ -333,6 +562,22 @@ async def delete_post(
|
|||
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)})
|
||||
|
||||
|
|
@ -503,6 +748,293 @@ async def get_admins(user: dict = Depends(require_moderator)):
|
|||
)
|
||||
|
||||
|
||||
@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"""
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import urllib.parse
|
|||
from typing import Optional, Dict, Any
|
||||
from config import settings
|
||||
|
||||
MAX_AUTH_AGE_SECONDS = 60 * 60 # 1 час
|
||||
AUTH_AGE_TOLERANCE_SECONDS = 300 # 5 минут допуск
|
||||
MAX_AUTH_AGE_SECONDS = 60 * 60 * 24 # 24 часа (увеличено для модерации)
|
||||
AUTH_AGE_TOLERANCE_SECONDS = 60 * 60 # 1 час допуск
|
||||
|
||||
|
||||
def validate_init_data(init_data_raw: str, bot_token: Optional[str] = None) -> Dict[str, Any]:
|
||||
|
|
@ -85,9 +85,9 @@ def validate_init_data(init_data_raw: str, bot_token: Optional[str] = None) -> D
|
|||
age = abs(now - auth_date)
|
||||
|
||||
if age > MAX_AUTH_AGE_SECONDS:
|
||||
# Allow small tolerance in development
|
||||
if settings.IS_DEVELOPMENT and age <= MAX_AUTH_AGE_SECONDS + AUTH_AGE_TOLERANCE_SECONDS:
|
||||
print(f'⚠️ InitData expired ({age}s), but allowing due to tolerance in development.')
|
||||
# Allow tolerance (особенно для модерации, где сессии могут быть длинными)
|
||||
if age <= MAX_AUTH_AGE_SECONDS + AUTH_AGE_TOLERANCE_SECONDS:
|
||||
print(f'⚠️ InitData expired ({age}s), but allowing due to tolerance.')
|
||||
else:
|
||||
raise ValueError(f'Данные авторизации устарели (возраст: {age}с, макс: {MAX_AUTH_AGE_SECONDS}с)')
|
||||
except ValueError:
|
||||
|
|
|
|||
Loading…
Reference in New Issue