import asyncio from datetime import datetime, timezone from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from firebase_admin import messaging from app.core.config import config from app.db import models from app.services.crypto_service import encrypt_system_message from app.websocket.connection_manager import manager async def send_system_notification(db: AsyncSession, receiver_id: int, plain_text: str): """ Полный цикл отправки зашифрованного системного уведомления. Безопасен к коммитам SQLAlchemy и не блокирует асинхронный Event Loop. """ # 1. Извлекаем получателя из БД result = await db.execute(select(models.User).where(models.User.id == receiver_id)) user = result.scalars().first() if not user: print(f"[System Notice] Ошибка: Пользователь {receiver_id} не найден.") return False if not user.public_key: print(f"[System Notice] Предупреждение: У пользователя {user.username} не сгенерирован ключ E2EE. Отправка отменена.") return False # ФИКС: Атомарно сохраняем все нужные свойства пользователя в локальные переменные. # После db.commit() объект user станет недоступен, но эти переменные останутся в безопасности. u_id = user.id u_username = user.username u_public_key = user.public_key u_fcm_token = user.fcm_token try: # 2. Шифруем текст сообщения, используя сохраненный ключ encrypted_content = encrypt_system_message(plain_text, u_public_key) preview_text = plain_text if len(plain_text) <= 500 else f"{plain_text[:497]}..." encrypted_preview = encrypt_system_message(preview_text, u_public_key) # 3. Записываем сообщение в историю базы данных new_message = models.Message( sender_id=0, # Система receiver_id=u_id, content=encrypted_content, timestamp=datetime.now(timezone.utc), message_type="text" ) db.add(new_message) await db.commit() await db.refresh(new_message) # Подготавливаем JSON-пакет для WebSocket socket_payload = { "type": "private_message", "id": new_message.id, "sender_id": 0, "receiver_id": u_id, "content": encrypted_preview, "timestamp": new_message.timestamp.isoformat(), "message_type": "text" } # 4. Доставка через WebSocket (используем строковый u_id) is_delivered_via_ws = await manager.send_personal_message(socket_payload, user_id=str(u_id)) # 5. Если пользователь офлайн, отправляем Push-уведомление через Firebase FCM if u_fcm_token: try: unread_res = await db.execute( select(func.count(models.Message.id)) .where(models.Message.receiver_id == receiver_id, models.Message.read_at == None) ) unread_count = unread_res.scalar() or 0 fcm_message = messaging.Message( token=u_fcm_token, data={ "click_action": "FLUTTER_NOTIFICATION_CLICK", "type": "enc_message", "sender_id": "0", "receiver_id": str(u_id), "public_key": config.SYSTEM_CHAT_PUBLIC_KEY, "content": encrypted_preview, "timestamp": new_message.timestamp.isoformat(), "username": "Chepuhagram", "unread_count": str(unread_count), "message_id": str(new_message.id) }, android=messaging.AndroidConfig(priority="high"), apns=messaging.APNSConfig(payload=messaging.APNSPayload(aps=messaging.Aps(sound="default"))) ) # ФИКС: Выполняем синхронный сетевой запрос Firebase в отдельном потоке (Thread), # чтобы он не блокировал и не тормозил основной поток FastAPI loop = asyncio.get_running_loop() await loop.run_in_executor(None, messaging.send, fcm_message) print(f"[System Notice] Push успешно отправлен через FCM для {u_username}") except Exception as fcm_err: print(f"[System Notice] Не удалось отправить FCM пуш: {fcm_err}") return True except Exception as err: print(f"[System Notice] Критическая ошибка во время отправки уведомления: {err}") await db.rollback() return False