81 lines
2.5 KiB
Python
81 lines
2.5 KiB
Python
"""Push notifications about new mail: the server's public key, and the devices
|
|||
|
|
that want them (Settings → Accounts → Notifications)."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Annotated
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, Request
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.api.errors import raise_api_error
|
||
|
|
from app.db import get_session
|
||
|
|
from app.identity.deps import CurrentSession, CurrentUser, require_csrf
|
||
|
|
from app.push import service, webpush
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/push", tags=["push"], dependencies=[Depends(require_csrf)])
|
||
|
|
|
||
|
|
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
||
|
|
|
||
|
|
|
||
|
|
class PushKeyOut(BaseModel):
|
||
|
|
public_key: str
|
||
|
|
|
||
|
|
|
||
|
|
class PushKeys(BaseModel):
|
||
|
|
p256dh: str = Field(min_length=1, max_length=200)
|
||
|
|
auth: str = Field(min_length=1, max_length=100)
|
||
|
|
|
||
|
|
|
||
|
|
class SubscriptionIn(BaseModel):
|
||
|
|
"""The browser's ``PushSubscription.toJSON()``."""
|
||
|
|
|
||
|
|
endpoint: str = Field(min_length=1, max_length=2000)
|
||
|
|
keys: PushKeys
|
||
|
|
|
||
|
|
|
||
|
|
class EndpointIn(BaseModel):
|
||
|
|
endpoint: str = Field(min_length=1, max_length=2000)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/key", response_model=PushKeyOut)
|
||
|
|
async def push_key(user: CurrentUser, db: DbSession) -> PushKeyOut:
|
||
|
|
"""The key a browser subscribes with (``applicationServerKey``)."""
|
||
|
|
return PushKeyOut(public_key=(await service.vapid(db)).public_b64)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/subscriptions")
|
||
|
|
async def add_subscription(
|
||
|
|
body: SubscriptionIn,
|
||
|
|
request: Request,
|
||
|
|
user: CurrentUser,
|
||
|
|
session: CurrentSession,
|
||
|
|
db: DbSession,
|
||
|
|
) -> dict[str, str]:
|
||
|
|
if not webpush.allowed_endpoint(body.endpoint):
|
||
|
|
raise_api_error(400, "push_service", "not a known push service")
|
||
|
|
try:
|
||
|
|
p256dh = webpush.b64u_decode(body.keys.p256dh)
|
||
|
|
auth = webpush.b64u_decode(body.keys.auth)
|
||
|
|
if len(p256dh) != 65 or len(auth) != 16:
|
||
|
|
raise ValueError
|
||
|
|
except ValueError:
|
||
|
|
raise_api_error(400, "push_keys", "invalid subscription keys")
|
||
|
|
await service.subscribe(
|
||
|
|
db,
|
||
|
|
user_id=user.id,
|
||
|
|
session_sid=session.sid,
|
||
|
|
endpoint=body.endpoint,
|
||
|
|
p256dh=body.keys.p256dh,
|
||
|
|
auth=body.keys.auth,
|
||
|
|
user_agent=request.headers.get("user-agent", ""),
|
||
|
|
)
|
||
|
|
return {"status": "ok"}
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/subscriptions/remove")
|
||
|
|
async def remove_subscription(body: EndpointIn, user: CurrentUser, db: DbSession) -> dict[str, str]:
|
||
|
|
await service.unsubscribe(db, user_id=user.id, endpoint=body.endpoint)
|
||
|
|
return {"status": "ok"}
|