"""浏览器推送通知服务 — Web Push Protocol (RFC 8291)""" from __future__ import annotations import json import logging from typing import Optional from app.core.database import SessionLocal from app.models.push_subscription import PushSubscription logger = logging.getLogger(__name__) async def send_push_to_user( user_id: str, title: str, body: str = "", url: str = "/", require_interaction: bool = False, ) -> int: """向指定用户的所有设备发送推送通知。返回成功发送数。""" db = SessionLocal() sent = 0 try: subs = ( db.query(PushSubscription) .filter(PushSubscription.user_id == user_id) .all() ) for sub in subs: ok = await _send_webpush( endpoint=sub.endpoint, p256dh=sub.p256dh, auth=sub.auth, title=title, body=body, url=url, require_interaction=require_interaction, ) if ok: sent += 1 return sent finally: db.close() async def broadcast_push( title: str, body: str = "", url: str = "/", ) -> int: """向所有已订阅用户广播推送通知。""" db = SessionLocal() sent = 0 try: subs = db.query(PushSubscription).all() for sub in subs: ok = await _send_webpush( endpoint=sub.endpoint, p256dh=sub.p256dh, auth=sub.auth, title=title, body=body, url=url, require_interaction=False, ) if ok: sent += 1 return sent finally: db.close() async def _send_webpush( endpoint: str, p256dh: str, auth: str, title: str, body: str, url: str = "/", require_interaction: bool = False, ) -> bool: """发送单条 Web Push 消息。使用 pywebpush 或直接 HTTP POST。""" payload = json.dumps({ "title": title, "body": body, "data": {"url": url}, "requireInteraction": require_interaction, "icon": "/icons/icon-192.png", "badge": "/icons/icon-192.png", }) try: # 尝试 pywebpush(如果已安装) try: from pywebpush import WebPusher, WebPushException wp = WebPusher({ "endpoint": endpoint, "keys": {"p256dh": p256dh, "auth": auth}, }) wp.send(payload, timeout=10) return True except ImportError: pass # Fallback: 直接 HTTP POST(不加密,仅用于开发环境) import aiohttp async with aiohttp.ClientSession() as session: async with session.post( endpoint, data=payload, headers={ "Content-Type": "application/json", "TTL": "86400", }, timeout=aiohttp.ClientTimeout(total=10), ) as resp: ok = resp.status in (200, 201, 204) if not ok: logger.warning("Push 发送失败: status=%d", resp.status) return ok except Exception as e: logger.error("Push 发送异常: %s", e) return False