@@ -1,5 +1,5 @@
|
|||||||
# packetserver/http/routers/messages.py
|
# packetserver/http/routers/messages.py
|
||||||
from fastapi import APIRouter, Depends, Query, HTTPException, Path, Request, Query
|
from fastapi import APIRouter, Depends, Query, HTTPException, Path, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -11,7 +11,6 @@ from pydantic import BaseModel, Field, validator
|
|||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.http.database import DbDependency
|
from packetserver.http.database import DbDependency
|
||||||
from packetserver.http.server import templates
|
|
||||||
|
|
||||||
|
|
||||||
html_router = APIRouter(tags=["messages-html"])
|
html_router = APIRouter(tags=["messages-html"])
|
||||||
@@ -28,78 +27,58 @@ class MarkRetrievedRequest(BaseModel):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
@router.get("/messages", response_class=HTMLResponse)
|
@router.get("/messages")
|
||||||
async def message_list(
|
async def get_messages(
|
||||||
request: Request,
|
|
||||||
db: DbDependency,
|
db: DbDependency,
|
||||||
current_user: HttpUser = Depends(get_current_http_user),
|
current_user: HttpUser = Depends(get_current_http_user),
|
||||||
msg_type: str = Query("received", alias="type"), # received, sent, all
|
type: str = Query("received", description="received, sent, or all"),
|
||||||
search: Optional[str] = Query(None),
|
limit: Optional[int] = Query(20, le=100, description="Max messages to return (default 20, max 100)"),
|
||||||
page: int = Query(1, ge=1),
|
since: Optional[str] = Query(None, description="ISO UTC timestamp filter (e.g. 2025-12-01T00:00:00Z)"),
|
||||||
per_page: int = Query(50, ge=1, le=200),
|
|
||||||
):
|
):
|
||||||
username = current_user.username.upper().strip()
|
if limit is None or limit < 1:
|
||||||
|
limit = 20
|
||||||
valid_types = {"received", "sent", "all"}
|
|
||||||
if msg_type not in valid_types:
|
|
||||||
msg_type = "received"
|
|
||||||
|
|
||||||
|
username = current_user.username
|
||||||
with db.transaction() as conn:
|
with db.transaction() as conn:
|
||||||
root = conn.root()
|
root = conn.root()
|
||||||
user_messages = root["messages"].get(username, [])
|
|
||||||
|
|
||||||
# Convert to list of dicts (as current code does)
|
if 'messages' not in root:
|
||||||
|
root['messages'] = PersistentMapping()
|
||||||
|
if username not in root['messages']:
|
||||||
|
root['messages'][username] = persistent.list.PersistentList()
|
||||||
|
|
||||||
|
mailbox = root['messages'][username]
|
||||||
|
|
||||||
|
since_dt = None
|
||||||
|
if since:
|
||||||
|
try:
|
||||||
|
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid 'since' format")
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
for msg_id, msg in user_messages.items():
|
for msg in mailbox:
|
||||||
|
if type == "received" and msg.msg_from == username:
|
||||||
|
continue
|
||||||
|
if type == "sent" and msg.msg_from != username:
|
||||||
|
continue
|
||||||
|
if since_dt and msg.sent_at < since_dt:
|
||||||
|
continue
|
||||||
|
|
||||||
messages.append({
|
messages.append({
|
||||||
"id": msg_id,
|
"id": str(msg.msg_id),
|
||||||
"from": msg["from"],
|
"from": msg.msg_from,
|
||||||
"to": msg["to"],
|
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
|
||||||
"text": msg["text"],
|
"sent_at": msg.sent_at.isoformat() + "Z",
|
||||||
"timestamp": msg["timestamp"],
|
"text": msg.text,
|
||||||
|
"has_attachments": len(msg.attachments) > 0,
|
||||||
|
"retrieved": msg.retrieved,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Filter by type
|
messages.sort(key=lambda m: m["sent_at"], reverse=True)
|
||||||
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
|
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
|
||||||
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}")
|
@router.get("/messages/{msg_id}")
|
||||||
async def get_message(
|
async def get_message(
|
||||||
|
|||||||
@@ -10,45 +10,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
|
|
||||||
<!-- Type tabs -->
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if current_type == 'received' %}active{% endif %}"
|
|
||||||
href="?type=received{% if current_search %}&search={{ current_search }}{% endif %}">Received</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if current_type == 'sent' %}active{% endif %}"
|
|
||||||
href="?type=sent{% if current_search %}&search={{ current_search }}{% endif %}">Sent</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if current_type == 'all' %}active{% endif %}"
|
|
||||||
href="?type=all{% if current_search %}&search={{ current_search }}{% endif %}">All</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Search form -->
|
|
||||||
<form method="get" class="d-flex">
|
|
||||||
<input type="hidden" name="type" value="{{ current_type }}">
|
|
||||||
<input type="text" name="search" class="form-control me-2" placeholder="Search messages..."
|
|
||||||
value="{{ current_search or '' }}">
|
|
||||||
<button type="submit" class="btn btn-outline-primary">Search</button>
|
|
||||||
{% if current_search %}
|
|
||||||
<a href="?type={{ current_type }}" class="btn btn-outline-secondary ms-2">Clear</a>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if total == 0 %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
{% if current_search %}
|
|
||||||
No messages found matching "{{ current_search }}".
|
|
||||||
{% else %}
|
|
||||||
No messages yet.
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<a href="?msg_type=received" class="btn btn-sm {% if msg_type == 'received' %}btn-primary{% else %}btn-outline-primary{% endif %}">Received</a>
|
<a href="?msg_type=received" class="btn btn-sm {% if msg_type == 'received' %}btn-primary{% else %}btn-outline-primary{% endif %}">Received</a>
|
||||||
<a href="?msg_type=sent" class="btn btn-sm {% if msg_type == 'sent' %}btn-primary{% else %}btn-outline-primary{% endif %}">Sent</a>
|
<a href="?msg_type=sent" class="btn btn-sm {% if msg_type == 'sent' %}btn-primary{% else %}btn-outline-primary{% endif %}">Sent</a>
|
||||||
@@ -56,9 +17,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<p class="text-muted text-center mb-3">
|
|
||||||
Showing {{ (page-1)*per_page + 1 }}–{{ min(page*per_page, total) }} of {{ total }} messages
|
|
||||||
</p>
|
|
||||||
<ul class="message-list">
|
<ul class="message-list">
|
||||||
{% for msg in messages %}
|
{% for msg in messages %}
|
||||||
<li>
|
<li>
|
||||||
@@ -72,41 +30,6 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if total_pages > 1 %}
|
|
||||||
<nav aria-label="Message pagination" class="mt-4">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
<!-- Previous -->
|
|
||||||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ page - 1 }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">Previous</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Page numbers (simple: show current ±2, plus first/last) -->
|
|
||||||
{% set nearby = range(max(1, page-2), min(total_pages+1, page+3)) %}
|
|
||||||
{% if 1 not in nearby %}
|
|
||||||
<li class="page-item"><a class="page-link" href="?page=1&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">1</a></li>
|
|
||||||
{% if page > 4 %}<li class="page-item disabled"><span class="page-link">...</span></li>{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for p in nearby %}
|
|
||||||
<li class="page-item {% if p == page %}active{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ p }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">{{ p }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if total_pages not in nearby %}
|
|
||||||
{% if page < total_pages - 3 %}<li class="page-item disabled"><span class="page-link">...</span></li>{% endif %}
|
|
||||||
<li class="page-item"><a class="page-link" href="?page={{ total_pages }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">{{ total_pages }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Next -->
|
|
||||||
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ page + 1 }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">Next</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No messages found.</p>
|
<p>No messages found.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user