Files
packetserver/packetserver/http/routers/messages.py
2025-12-28 19:11:48 -05:00

207 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# packetserver/http/routers/messages.py
from fastapi import APIRouter, Depends, Query, HTTPException, Path, Request, Query
from fastapi.responses import HTMLResponse
from typing import Optional
from datetime import datetime
from persistent.mapping import PersistentMapping
import persistent.list
import transaction
from pydantic import BaseModel, Field, validator
from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser
from packetserver.http.database import DbDependency
from packetserver.http.server import templates
html_router = APIRouter(tags=["messages-html"])
router = APIRouter(prefix="/api/v1", tags=["messages"])
# Simple request model (only allow setting to true)
class MarkRetrievedRequest(BaseModel):
retrieved: bool = Field(..., description="Set to true to mark as retrieved")
@validator("retrieved")
def must_be_true(cls, v):
if not v:
raise ValueError("retrieved must be true")
return v
@router.get("/messages", response_class=HTMLResponse)
async def message_list(
request: Request,
db: DbDependency,
current_user: HttpUser = Depends(get_current_http_user),
msg_type: str = Query("received", alias="type"), # received, sent, all
search: Optional[str] = Query(None),
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=200),
):
username = current_user.username.upper().strip()
valid_types = {"received", "sent", "all"}
if msg_type not in valid_types:
msg_type = "received"
with db.transaction() as conn:
root = conn.root()
user_messages = root["messages"].get(username, [])
# Convert to list of dicts (as current code does)
messages = []
for msg_id, msg in user_messages.items():
messages.append({
"id": msg_id,
"from": msg["from"],
"to": msg["to"],
"text": msg["text"],
"timestamp": msg["timestamp"],
})
# Filter by type
if msg_type == "received":
filtered = [m for m in messages if username in m["to"]]
elif msg_type == "sent":
filtered = [m for m in messages if m["from"] == username]
else: # all
filtered = messages
# Apply search
if search:
search_lower = search.strip().lower()
filtered = [
m for m in filtered
if (search_lower in m["from"].lower() or
any(search_lower in t.lower() for t in m["to"]) or
search_lower in m["text"].lower())
]
# Sort newest first
filtered.sort(key=lambda m: m["timestamp"], reverse=True)
# Pagination
total = len(filtered)
start = (page - 1) * per_page
paginated = filtered[start:start + per_page]
total_pages = (total + per_page - 1) // per_page
return templates.TemplateResponse(
"message_list.html",
{
"request": request,
"messages": paginated,
"current_user": current_user,
"total": total,
"page": page,
"per_page": per_page,
"total_pages": total_pages,
"current_type": msg_type,
"current_search": search,
}
)
@router.get("/messages/{msg_id}")
async def get_message(
db: DbDependency,
msg_id: str = Path(..., description="UUID of the message (as string)"),
mark_retrieved: bool = Query(False, description="If true, mark message as retrieved/read"),
current_user: HttpUser = Depends(get_current_http_user)
):
with db.transaction() as conn:
root = conn.root()
username = current_user.username
messages_root = root.get('messages', {})
mailbox = messages_root.get(username)
if not mailbox:
raise HTTPException(status_code=404, detail="Mailbox not found")
# Find message by ID
target_msg = None
for msg in mailbox:
if str(msg.msg_id) == msg_id:
target_msg = msg
break
if not target_msg:
raise HTTPException(status_code=404, detail="Message not found")
# Optionally mark as retrieved
if mark_retrieved and not target_msg.retrieved:
target_msg.retrieved = True
target_msg._p_changed = True
mailbox._p_changed = True
# Explicit transaction for the write
transaction.get().commit()
return {
"id": str(target_msg.msg_id),
"from": target_msg.msg_from or "UNKNOWN",
"to": list(target_msg.msg_to),
"sent_at": target_msg.sent_at.isoformat() + "Z",
"text": target_msg.text,
"retrieved": target_msg.retrieved,
"has_attachments": len(target_msg.attachments) > 0,
# Future: "attachments": [...] metadata
}
@router.patch("/messages/{msg_id}")
async def mark_message_retrieved(
db: DbDependency,
msg_id: str = Path(..., description="Message UUID as string"),
payload: MarkRetrievedRequest = None,
current_user: HttpUser = Depends(get_current_http_user)
):
with db.transaction() as conn:
root = conn.root()
username = current_user.username
mailbox = root.get('messages', {}).get(username)
if not mailbox:
raise HTTPException(status_code=404, detail="Mailbox not found")
target_msg = None
for msg in mailbox:
if str(msg.msg_id) == msg_id:
target_msg = msg
break
if not target_msg:
raise HTTPException(status_code=404, detail="Message not found")
if target_msg.retrieved:
# Already marked idempotent success
return {"status": "already_retrieved", "id": msg_id}
target_msg.retrieved = True
target_msg._p_changed = True
mailbox._p_changed = True
transaction.get().commit()
return {"status": "marked_retrieved", "id": msg_id}
@html_router.get("/messages", response_class=HTMLResponse)
async def message_list_page(
db: DbDependency,
request: Request,
type: str = Query("received", alias="msg_type"), # matches your filter links
limit: Optional[int] = Query(50, le=100),
current_user: HttpUser = Depends(get_current_http_user)
):
from packetserver.http.server import templates
# Directly call the existing API endpoint function
api_resp = await get_messages(db, current_user=current_user, type=type, limit=limit, since=None)
messages = api_resp["messages"]
return templates.TemplateResponse(
"message_list.html",
{
"request": request,
"messages": messages,
"msg_type": type,
"current_user": current_user.username
}
)