110 lines
5.3 KiB
Python
110 lines
5.3 KiB
Python
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
|