Update files

This commit is contained in:
glpshchn 2025-12-15 03:15:53 +03:00
parent 4efdeb62ff
commit 3be93d1353
2 changed files with 539 additions and 7 deletions

View File

@ -1,6 +1,7 @@
""" """
Moderation app routes - main moderation functionality Moderation app routes - main moderation functionality
""" """
import secrets
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, List from typing import Optional, List
from fastapi import APIRouter, HTTPException, status, Depends, Query from fastapi import APIRouter, HTTPException, status, Depends, Query
@ -242,6 +243,33 @@ async def get_post(
'photoUrl': post['author'].get('photoUrl') '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 { return {
'post': { 'post': {
'id': str(post['_id']), 'id': str(post['_id']),
@ -250,12 +278,16 @@ async def get_post(
'hashtags': post.get('hashtags', []), 'hashtags': post.get('hashtags', []),
'tags': post.get('tags', []), 'tags': post.get('tags', []),
'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []), 'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []),
'comments': post.get('comments', []), 'comments': comments_serialized,
'likes': post.get('likes', []), 'likes': likes_serialized,
'commentsCount': len(comments_serialized),
'likesCount': len(likes_serialized),
'isNSFW': post.get('isNSFW', False), 'isNSFW': post.get('isNSFW', False),
'isHomo': post.get('isHomo', False),
'isArt': post.get('isArt', False), 'isArt': post.get('isArt', False),
'publishedToChannel': post.get('publishedToChannel', False), 'publishedToChannel': post.get('publishedToChannel', False),
'adminNumber': post.get('adminNumber'), 'adminNumber': post.get('adminNumber'),
'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None,
'createdAt': post.get('createdAt', datetime.utcnow()).isoformat() '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}") @router.delete("/posts/{post_id}")
async def delete_post( async def delete_post(
post_id: str, post_id: str,
@ -333,6 +562,22 @@ async def delete_post(
detail="Пост не найден" 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 # Delete post
await posts_collection().delete_one({'_id': ObjectId(post_id)}) 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") @router.post("/auth/verify")
async def verify_auth(user: dict = Depends(require_moderator)): async def verify_auth(user: dict = Depends(require_moderator)):
"""Verify authentication and get admin list""" """Verify authentication and get admin list"""

View File

@ -7,8 +7,8 @@ import urllib.parse
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from config import settings from config import settings
MAX_AUTH_AGE_SECONDS = 60 * 60 # 1 час MAX_AUTH_AGE_SECONDS = 60 * 60 * 24 # 24 часа (увеличено для модерации)
AUTH_AGE_TOLERANCE_SECONDS = 300 # 5 минут допуск AUTH_AGE_TOLERANCE_SECONDS = 60 * 60 # 1 час допуск
def validate_init_data(init_data_raw: str, bot_token: Optional[str] = None) -> Dict[str, Any]: 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) age = abs(now - auth_date)
if age > MAX_AUTH_AGE_SECONDS: if age > MAX_AUTH_AGE_SECONDS:
# Allow small tolerance in development # Allow tolerance (особенно для модерации, где сессии могут быть длинными)
if settings.IS_DEVELOPMENT and age <= MAX_AUTH_AGE_SECONDS + AUTH_AGE_TOLERANCE_SECONDS: if age <= MAX_AUTH_AGE_SECONDS + AUTH_AGE_TOLERANCE_SECONDS:
print(f'⚠️ InitData expired ({age}s), but allowing due to tolerance in development.') print(f'⚠️ InitData expired ({age}s), but allowing due to tolerance.')
else: else:
raise ValueError(f'Данные авторизации устарели (возраст: {age}с, макс: {MAX_AUTH_AGE_SECONDS}с)') raise ValueError(f'Данные авторизации устарели (возраст: {age}с, макс: {MAX_AUTH_AGE_SECONDS}с)')
except ValueError: except ValueError: