import logging
from typing import Annotated
from datetime import datetime, timezone
from fastapi import APIRouter, Request, Form
from devplacepy.models import MessageForm
from fastapi.responses import HTMLResponse, JSONResponse
from devplacepy.database import get_table, db
from devplacepy.attachments import get_attachments_batch
from devplacepy.templating import clear_unread_cache, clear_messages_cache
from devplacepy.utils import (
generate_uid,
require_user,
time_ago,
create_mention_notifications,
)
from devplacepy.seo import base_seo_context
from devplacepy.responses import respond, action_result
from devplacepy.schemas import MessagesOut
logger = logging.getLogger(__name__)
router = APIRouter()
def get_conversations(user_uid: str):
messages_table = get_table("messages")
raw = list(messages_table.find(sender_uid=user_uid)) + list(
messages_table.find(receiver_uid=user_uid)
)
seen = set()
all_messages = []
for m in raw:
if m["uid"] not in seen:
seen.add(m["uid"])
all_messages.append(m)
conversation_map = {}
other_uids = set()
for msg in all_messages:
other_uid = (
msg["receiver_uid"] if msg["sender_uid"] == user_uid else msg["sender_uid"]
)
other_uids.add(other_uid)
if (
other_uid not in conversation_map
or msg["created_at"] > conversation_map[other_uid]["last_message_at"]
):
conversation_map[other_uid] = {
"other_user": None,
"last_message": msg["content"],
"last_message_at": msg["created_at"],
"unread": msg["receiver_uid"] == user_uid and not msg["read"],
}
if other_uids:
from devplacepy.database import get_users_by_uids
users_map = get_users_by_uids(list(other_uids))
for uid, conv in conversation_map.items():
conv["other_user"] = users_map.get(uid)
conversations = sorted(
conversation_map.values(),
key=lambda c: c["last_message_at"],
reverse=True,
)
return conversations
def get_conversation_messages(user_uid: str, other_uid: str):
messages_table = get_table("messages")
raw = list(messages_table.find(sender_uid=user_uid, receiver_uid=other_uid)) + list(
messages_table.find(sender_uid=other_uid, receiver_uid=user_uid)
)
seen = set()
msgs = []
for m in raw:
if m["uid"] not in seen:
seen.add(m["uid"])
msgs.append(m)
msgs.sort(key=lambda m: m["created_at"])
if "messages" in db.tables:
with db:
db.query(
"UPDATE messages SET read = 1 WHERE receiver_uid = :me AND sender_uid = :other AND read = 0",
me=user_uid,
other=other_uid,
)
clear_messages_cache(user_uid)
from devplacepy.database import get_users_by_uids
user_ids = list({m["sender_uid"] for m in msgs} | {other_uid})
users_map = get_users_by_uids(user_ids)
other_user = users_map.get(other_uid)
result = []
msg_uids = [m["uid"] for m in msgs]
attachments_map = get_attachments_batch("message", msg_uids) if msg_uids else {}
for m in msgs:
result.append(
{
"message": m,
"sender": users_map.get(m["sender_uid"]),
"is_mine": m["sender_uid"] == user_uid,
"time_ago": time_ago(m["created_at"]),
"attachments": attachments_map.get(m["uid"], []),
}
)
return result, other_user
@router.get("", response_class=HTMLResponse)
async def messages_page(request: Request, with_uid: str = None, search: str = ""):
user = require_user(request)
conversations = get_conversations(user["uid"])
if search:
users_table = get_table("users")
found = users_table.find_one(username=search)
if found:
with_uid = found["uid"]
current_conversation = None
messages = []
other_user = None
if with_uid:
messages, other_user = get_conversation_messages(user["uid"], with_uid)
current_conversation = with_uid
seo_ctx = base_seo_context(
request,
title="Messages",
robots="noindex,nofollow",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Messages", "url": "/messages"},
],
)
return respond(
request,
"messages.html",
{
**seo_ctx,
"request": request,
"user": user,
"conversations": conversations,
"messages": messages,
"other_user": other_user,
"current_conversation": current_conversation,
"search": search,
},
model=MessagesOut,
)
@router.get("/search")
async def search_users(request: Request, q: str = ""):
user = require_user(request)
if not q or len(q) < 1:
return JSONResponse({"results": []})
if "users" in db.tables:
rows = db.query(
"SELECT uid, username FROM users WHERE username LIKE :q AND uid != :me LIMIT 10",
q=f"%{q}%",
me=user["uid"],
)
results = [{"uid": r["uid"], "username": r["username"]} for r in rows]
else:
results = []
return JSONResponse({"results": results})
@router.post("/send")
async def send_message(request: Request, data: Annotated[MessageForm, Form()]):
user = require_user(request)
content = data.content.strip()
receiver_uid = data.receiver_uid
if not get_table("users").find_one(uid=receiver_uid):
return action_result(request, "/messages")
messages_table = get_table("messages")
msg_uid = generate_uid()
messages_table.insert(
{
"uid": msg_uid,
"sender_uid": user["uid"],
"receiver_uid": receiver_uid,
"content": content,
"read": False,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
from devplacepy.attachments import link_attachments
link_attachments(data.attachment_uids, "message", msg_uid)
if user["uid"] != receiver_uid:
notifications = get_table("notifications")
notifications.insert(
{
"uid": generate_uid(),
"user_uid": receiver_uid,
"type": "message",
"message": f"{user['username']} sent you a message",
"related_uid": user["uid"],
"target_url": f"/messages?with_uid={user['uid']}",
"read": False,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
clear_unread_cache(receiver_uid)
clear_messages_cache(receiver_uid)
create_mention_notifications(
content, user["uid"], f"/messages?with_uid={receiver_uid}"
)
logger.info(f"Message {msg_uid} sent from {user['username']} to {receiver_uid}")
return action_result(
request, f"/messages?with_uid={receiver_uid}", data={"uid": msg_uid}
)