use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use athena_chat::dto::commands::{
AddMembers, AddReaction, ArchiveRoom, CreateRoom, DeleteMessage, EditMessage, ListMessages,
ListRooms, MarkReadUpTo, RemoveMember, RemoveReaction, SearchMessages, SendMessage, UpdateRoom,
};
use athena_chat::error::ChatError;
use serde::Deserialize;
use uuid::Uuid;
use crate::AppState;
use crate::api::response::{
api_created, api_ok, bad_request_with_code, conflict_with_code, forbidden_with_code,
internal_error_with_code, not_found_with_code, service_unavailable_with_code,
unauthorized_with_code,
};
#[derive(Debug, Deserialize)]
struct UpdateRoomBody {
title: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ListMessagesQuery {
limit: Option<i64>,
before_seq: Option<i64>,
after_seq: Option<i64>,
}
#[derive(Debug, Deserialize)]
struct SendMessageBody {
client_message_id: Option<String>,
body_text: Option<String>,
body_json: Option<serde_json::Value>,
reply_to_message_id: Option<Uuid>,
metadata_json: Option<serde_json::Value>,
#[serde(default)]
attachments: Vec<athena_chat::dto::commands::AttachmentInput>,
}
#[derive(Debug, Deserialize)]
struct EditMessageBody {
body_text: Option<String>,
body_json: Option<serde_json::Value>,
metadata_json: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct ReadCursorBody {
message_id: Option<Uuid>,
seq: Option<i64>,
}
#[derive(Debug, Deserialize)]
struct AddMembersBody {
user_ids: Vec<String>,
role: Option<String>,
}
#[derive(Debug, Deserialize)]
struct AddReactionBody {
emoji: String,
}
fn chat_error_response(error: ChatError) -> HttpResponse {
match error {
ChatError::Unauthorized => unauthorized_with_code(
"Chat authentication required",
"Missing chat actor",
"CHAT_AUTH_REQUIRED",
),
ChatError::Forbidden(message) => {
forbidden_with_code("Chat action forbidden", message, "CHAT_FORBIDDEN")
}
ChatError::NotFound(message) => {
not_found_with_code("Chat resource not found", message, "CHAT_NOT_FOUND")
}
ChatError::Conflict(message) => {
conflict_with_code("Chat conflict", message, "CHAT_CONFLICT")
}
ChatError::Validation(message) => {
bad_request_with_code("Invalid chat request", message, "CHAT_VALIDATION_FAILED")
}
ChatError::Unavailable(message) => {
service_unavailable_with_code("Chat unavailable", message, "CHAT_UNAVAILABLE")
}
ChatError::Unsupported(message) => {
bad_request_with_code("Unsupported chat request", message, "CHAT_UNSUPPORTED")
}
ChatError::Internal(message) => {
internal_error_with_code("Chat request failed", message, "CHAT_INTERNAL")
}
}
}
async fn chat_ctx(
req: &HttpRequest,
state: &web::Data<AppState>,
) -> Result<athena_chat::ChatContext, HttpResponse> {
state.auth_resolver.resolve(req, state.get_ref()).await
}
#[get("/chat/rooms")]
async fn list_rooms(
req: HttpRequest,
state: web::Data<AppState>,
query: web::Query<ListRooms>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
match state.chat_app.list_rooms(ctx, query.into_inner()).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/rooms")]
async fn create_room(
req: HttpRequest,
state: web::Data<AppState>,
body: web::Json<CreateRoom>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
match state.chat_app.create_room(ctx, body.into_inner()).await {
Ok(value) => api_created("Chat room created", value),
Err(err) => chat_error_response(err),
}
}
#[get("/chat/rooms/{room_id}")]
async fn get_room(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
match state.chat_app.get_room(ctx, room_id.into_inner()).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[patch("/chat/rooms/{room_id}")]
async fn update_room(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
body: web::Json<UpdateRoomBody>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let body = body.into_inner();
let input = UpdateRoom {
room_id: room_id.into_inner(),
title: body.title,
};
match state.chat_app.update_room(ctx, input).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/rooms/{room_id}/archive")]
async fn archive_room(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
match state
.chat_app
.archive_room(
ctx,
ArchiveRoom {
room_id: room_id.into_inner(),
},
)
.await
{
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[get("/chat/rooms/{room_id}/messages")]
async fn list_messages(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
query: web::Query<ListMessagesQuery>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let query = query.into_inner();
let input = ListMessages {
room_id: room_id.into_inner(),
limit: query.limit,
before_seq: query.before_seq,
after_seq: query.after_seq,
};
match state.chat_app.list_messages(ctx, input).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/rooms/{room_id}/messages")]
async fn send_message(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
body: web::Json<SendMessageBody>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let body = body.into_inner();
let input = SendMessage {
room_id: room_id.into_inner(),
client_message_id: body.client_message_id,
body_text: body.body_text,
body_json: body.body_json,
reply_to_message_id: body.reply_to_message_id,
metadata_json: body.metadata_json,
attachments: body.attachments,
};
match state.chat_app.send_message(ctx, input).await {
Ok(value) => api_created("Chat message created", value),
Err(err) => chat_error_response(err),
}
}
#[patch("/chat/rooms/{room_id}/messages/{message_id}")]
async fn edit_message(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<(Uuid, Uuid)>,
body: web::Json<EditMessageBody>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let (room_id, message_id) = path.into_inner();
let body = body.into_inner();
let input = EditMessage {
room_id,
message_id,
body_text: body.body_text,
body_json: body.body_json,
metadata_json: body.metadata_json,
};
match state.chat_app.edit_message(ctx, input).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[delete("/chat/rooms/{room_id}/messages/{message_id}")]
async fn delete_message(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<(Uuid, Uuid)>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let (room_id, message_id) = path.into_inner();
match state
.chat_app
.delete_message(
ctx,
DeleteMessage {
room_id,
message_id,
},
)
.await
{
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/rooms/{room_id}/read-cursor")]
async fn mark_read_up_to(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
body: web::Json<ReadCursorBody>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let body = body.into_inner();
let input = MarkReadUpTo {
room_id: room_id.into_inner(),
message_id: body.message_id,
seq: body.seq,
};
match state.chat_app.mark_read_up_to(ctx, input).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[get("/chat/rooms/{room_id}/members")]
async fn list_members(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
match state.chat_app.list_members(ctx, room_id.into_inner()).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/rooms/{room_id}/members")]
async fn add_members(
req: HttpRequest,
state: web::Data<AppState>,
room_id: web::Path<Uuid>,
body: web::Json<AddMembersBody>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let body = body.into_inner();
let input = AddMembers {
room_id: room_id.into_inner(),
user_ids: body.user_ids,
role: body.role,
};
match state.chat_app.add_members(ctx, input).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[delete("/chat/rooms/{room_id}/members/{user_id}")]
async fn remove_member(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<(Uuid, String)>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let (room_id, user_id) = path.into_inner();
match state
.chat_app
.remove_member(ctx, RemoveMember { room_id, user_id })
.await
{
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/messages/{message_id}/reactions")]
async fn add_reaction(
req: HttpRequest,
state: web::Data<AppState>,
message_id: web::Path<Uuid>,
body: web::Json<AddReactionBody>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let body = body.into_inner();
let input = AddReaction {
message_id: message_id.into_inner(),
emoji: body.emoji,
};
match state.chat_app.add_reaction(ctx, input).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[delete("/chat/messages/{message_id}/reactions/{emoji}")]
async fn remove_reaction(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<(Uuid, String)>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
let (message_id, emoji) = path.into_inner();
match state
.chat_app
.remove_reaction(ctx, RemoveReaction { message_id, emoji })
.await
{
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
#[post("/chat/messages/search")]
async fn search_messages(
req: HttpRequest,
state: web::Data<AppState>,
body: web::Json<SearchMessages>,
) -> HttpResponse {
let ctx = match chat_ctx(&req, &state).await {
Ok(ctx) => ctx,
Err(resp) => return resp,
};
match state.chat_app.search_messages(ctx, body.into_inner()).await {
Ok(value) => api_ok(value),
Err(err) => chat_error_response(err),
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(list_rooms)
.service(create_room)
.service(get_room)
.service(update_room)
.service(archive_room)
.service(list_messages)
.service(send_message)
.service(edit_message)
.service(delete_message)
.service(mark_read_up_to)
.service(list_members)
.service(add_members)
.service(remove_member)
.service(add_reaction)
.service(remove_reaction)
.service(search_messages);
}
#[cfg(test)]
mod tests {
use super::chat_error_response;
use actix_web::body::to_bytes;
use athena_chat::error::ChatError;
use serde_json::Value;
#[actix_web::test]
async fn chat_not_found_response_includes_error_registry_fields() {
let response = chat_error_response(ChatError::NotFound("missing room".to_string()));
let body = to_bytes(response.into_body())
.await
.expect("body should serialize");
let json: Value = serde_json::from_slice(&body).expect("error response should be JSON");
assert_eq!(json["code"], "CHAT_NOT_FOUND");
assert_eq!(json["error_number"], 5002);
assert_eq!(json["docs_url"], "https://docs.athena-cluster.com/5002");
}
#[actix_web::test]
async fn chat_validation_response_includes_error_registry_fields() {
let response = chat_error_response(ChatError::Validation("bad payload".to_string()));
let body = to_bytes(response.into_body())
.await
.expect("body should serialize");
let json: Value = serde_json::from_slice(&body).expect("error response should be JSON");
assert_eq!(json["code"], "CHAT_VALIDATION_FAILED");
assert_eq!(json["error_number"], 5004);
assert_eq!(json["docs_url"], "https://docs.athena-cluster.com/5004");
}
}