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

# 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,
)