use axum::{ Extension, Json, Router, extract::Path, http::{HeaderMap, StatusCode}, routing::{get, post}, }; use sqlx::PgPool; use uuid::Uuid; use crate::{auth::verify_jwt, db::room_id_from_uuid}; use crate::{ db::{user_id_from_uuid, username_from_uuid}, realtime::Realtime, }; #[derive(sqlx::FromRow, serde::Serialize, Debug)] pub struct MessageRow { pub sender: String, pub message_type: String, pub content: String, pub sent_at: chrono::NaiveDateTime, } #[derive(sqlx::FromRow, serde::Serialize, Debug, Clone)] pub struct Message { pub sender: String, pub message_type: String, pub content: String, pub sent_at: String, } #[derive(serde::Deserialize)] pub struct NewMessagePayload { pub message_type: String, pub content: String, } pub fn routes() -> Router { Router::new() .route("/messages/{room_uuid}", get(list_messages)) .route("/messages/{room_uuid}", post(create_message)) } async fn list_messages( Path(room_uuid): Path, headers: HeaderMap, Extension(db): Extension, ) -> Result>, (StatusCode, String)> { let claims = verify_jwt(headers)?; let user_id = user_id_from_uuid(&db, claims.sub).await?; let room_id = room_id_from_uuid(&db, room_uuid).await?; let membership: Vec = sqlx::query_scalar("SELECT user_id FROM membership_ WHERE user_id = $1 AND room = $2") .bind(user_id) .bind(room_id) .fetch_all(&db) .await .unwrap_or_else(|_| Vec::new()); if membership.is_empty() { return Err(( StatusCode::UNAUTHORIZED, String::from("You are not a member of this room"), )); } let messages = sqlx::query_as::<_, MessageRow>( r#" SELECT u.username AS sender, r.uuid AS room, m.message_type, m.content, m.sent_at FROM message_ m JOIN user_ u ON u.id = m.sender JOIN room_ r ON r.id = m.room WHERE m.room = $1 ORDER BY m.id "#, ) .bind(room_id) .fetch_all(&db) .await .map_err(|e| { tracing::error!("failed to list messages: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to list messages".into(), ) })?; let messages: Vec = messages .into_iter() .map(|m| Message { sender: m.sender, message_type: m.message_type, content: m.content, sent_at: m.sent_at.format("%Y-%m-%d %H:%M:%S").to_string(), }) .collect(); Ok(Json(messages)) } async fn create_message( Path(room_uuid): Path, Extension(db): Extension, Extension(realtime): Extension, headers: HeaderMap, Json(payload): Json, ) -> Result<(StatusCode, Json), (StatusCode, String)> { let claims = verify_jwt(headers)?; let user_id = user_id_from_uuid(&db, claims.sub).await?; let room_id = room_id_from_uuid(&db, room_uuid).await?; let sent_at: chrono::NaiveDateTime = sqlx::query_scalar( "INSERT INTO message_ (sender, room, message_type, content) VALUES ($1, $2, $3, $4) RETURNING sent_at", ) .bind(user_id) .bind(room_id) .bind(&payload.message_type) .bind(&payload.content) .fetch_one(&db) .await .map_err(|e| { ( StatusCode::BAD_REQUEST, format!("Could not create message: {e}"), ) })?; let sender_name = username_from_uuid(&db, claims.sub).await?; let message = Message { sender: sender_name, message_type: payload.message_type, content: payload.content, sent_at: sent_at.format("%Y-%m-%d %H:%M:%S").to_string(), }; let rt_sender = realtime.sender_for(room_id); let _ = rt_sender.send(message.clone()); Ok((StatusCode::CREATED, Json(message))) }