You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
4.8 KiB
Python
168 lines
4.8 KiB
Python
# backend/app/routers/notify.py
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlmodel import Session, select
|
|
|
|
from ..auth.deps import current_user
|
|
from ..auth.scope import scoped
|
|
from ..automation.event_bus import publish
|
|
from ..db import get_session
|
|
from ..models import (
|
|
Digest,
|
|
Notification,
|
|
NotificationGuard,
|
|
NotifyBucket,
|
|
NotifyStats,
|
|
Person,
|
|
SenderRule,
|
|
)
|
|
from ..schemas import (
|
|
DigestOut,
|
|
GuardOut,
|
|
GuardPatch,
|
|
NotificationOut,
|
|
ReclassifyNotifRequest,
|
|
SenderRuleCreate,
|
|
SenderRuleOut,
|
|
TriageOut,
|
|
)
|
|
from ..services.triage import detect_defer_pattern
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _out(n: Notification) -> NotificationOut:
|
|
return NotificationOut(
|
|
id=n.id,
|
|
bucket=n.bucket.value,
|
|
src=n.src,
|
|
app=n.app,
|
|
title=n.title,
|
|
sum=n.sum,
|
|
time=n.time,
|
|
why=n.why,
|
|
tone=n.tone,
|
|
act=n.act,
|
|
)
|
|
|
|
|
|
@router.get("/notifications", response_model=TriageOut)
|
|
def triage(s: Session = Depends(get_session), user: Person = Depends(current_user)):
|
|
rows = s.exec(
|
|
scoped(select(Notification), Notification, user.id).order_by(Notification.sort_order)
|
|
).all()
|
|
stats = s.get(NotifyStats, 1)
|
|
buckets: dict[str, list] = {"now": [], "later": [], "held": []}
|
|
for n in rows:
|
|
buckets[n.bucket.value].append(_out(n))
|
|
counts = {
|
|
"now": len(buckets["now"]),
|
|
"later": len(buckets["later"]) + (stats.later_more if stats else 0),
|
|
"held": len(buckets["held"]) + (stats.held_more if stats else 0),
|
|
}
|
|
return TriageOut(
|
|
now=buckets["now"],
|
|
later=buckets["later"],
|
|
held=buckets["held"],
|
|
counts=counts,
|
|
stats=(
|
|
{"total": stats.total, "seen": stats.seen, "filtered": stats.filtered} if stats else {}
|
|
),
|
|
)
|
|
|
|
|
|
@router.post("/notifications/{nid}/reclassify", response_model=NotificationOut)
|
|
def reclassify(nid: str, body: ReclassifyNotifRequest, s: Session = Depends(get_session)):
|
|
n = s.get(Notification, nid)
|
|
if not n:
|
|
raise HTTPException(404, "notification not found")
|
|
prev = n.bucket.value
|
|
n.bucket = NotifyBucket(body.to_bucket)
|
|
if body.to_bucket == "later":
|
|
n.why, n.tone, n.act = "내가 미룸", "muted", ""
|
|
elif body.to_bucket == "now" and prev == "held":
|
|
n.why, n.tone = "되돌림", "amber"
|
|
s.add(n)
|
|
s.commit()
|
|
if body.to_bucket == "later":
|
|
publish("notification.triaged", {"id": nid, "to": "later", "by": "user"})
|
|
for cand in detect_defer_pattern(s):
|
|
publish("automation.suggested", cand)
|
|
return _out(n)
|
|
|
|
|
|
@router.post("/notifications/{nid}/undo", response_model=NotificationOut)
|
|
def undo(nid: str, s: Session = Depends(get_session)):
|
|
"""held → now 되돌리기."""
|
|
n = s.get(Notification, nid)
|
|
if not n:
|
|
raise HTTPException(404, "notification not found")
|
|
n.bucket = NotifyBucket.now
|
|
n.why, n.tone = "되돌림", "amber"
|
|
s.add(n)
|
|
s.commit()
|
|
publish("notification.triaged", {"id": nid, "to": "now", "by": "undo"})
|
|
return _out(n)
|
|
|
|
|
|
@router.get("/notifications/guard", response_model=GuardOut)
|
|
def get_guard(s: Session = Depends(get_session)):
|
|
g = s.get(NotificationGuard, 1)
|
|
return GuardOut(on=g.on, until=g.until, held=g.held, quiet=g.quiet)
|
|
|
|
|
|
@router.post("/notifications/guard", response_model=GuardOut)
|
|
def set_guard(body: GuardPatch, s: Session = Depends(get_session)):
|
|
g = s.get(NotificationGuard, 1)
|
|
g.on = body.on
|
|
s.add(g)
|
|
s.commit()
|
|
return GuardOut(on=g.on, until=g.until, held=g.held, quiet=g.quiet)
|
|
|
|
|
|
@router.get("/notifications/digests", response_model=list[DigestOut])
|
|
def digests(s: Session = Depends(get_session)):
|
|
return s.exec(select(Digest).order_by(Digest.sort_order)).all()
|
|
|
|
|
|
@router.get("/notifications/senders", response_model=list[SenderRuleOut])
|
|
def senders(s: Session = Depends(get_session)):
|
|
rows = s.exec(select(SenderRule).order_by(SenderRule.sort_order)).all()
|
|
return [
|
|
SenderRuleOut(
|
|
id=r.id,
|
|
who=r.who,
|
|
from_label=r.from_label,
|
|
rule=r.rule,
|
|
tone=r.tone,
|
|
bucket=r.bucket.value,
|
|
)
|
|
for r in rows
|
|
]
|
|
|
|
|
|
@router.post("/notifications/senders", response_model=SenderRuleOut)
|
|
def add_sender(body: SenderRuleCreate, s: Session = Depends(get_session)):
|
|
rid = "sr-" + uuid.uuid4().hex[:6]
|
|
last = s.exec(select(SenderRule)).all()
|
|
r = SenderRule(
|
|
id=rid,
|
|
who=body.who,
|
|
from_label=body.from_label,
|
|
rule=body.rule,
|
|
bucket=NotifyBucket(body.bucket),
|
|
tone=body.tone,
|
|
sort_order=len(last),
|
|
)
|
|
s.add(r)
|
|
s.commit()
|
|
return SenderRuleOut(
|
|
id=r.id,
|
|
who=r.who,
|
|
from_label=r.from_label,
|
|
rule=r.rule,
|
|
tone=r.tone,
|
|
bucket=r.bucket.value,
|
|
)
|