use std::sync::Arc;
use axum::Extension;
use axum::Json;
use axum::extract::Path;
use axum::response::Response;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use time::format_description::well_known::Rfc3339;
use tokio_util::sync::CancellationToken;
use tracing::field::Empty;
use uuid::Uuid;
use chat_engine_sdk::models::{CapabilityValue, MessagePartInput};
use toolkit_security::SecurityContext;
use crate::api::rest::handlers::sessions::{identity_from_ctx, reject_body_identity};
use crate::domain::error::{ChatEngineError, Result};
use crate::domain::service::message_service::{DeleteOutcome, MessageService, SendMessageRequest};
#[derive(Debug, Deserialize)]
pub struct SendMessageBody {
pub session_id: Uuid,
#[serde(default)]
pub parts: Vec<MessagePartInput>,
#[serde(default)]
pub file_ids: Option<Vec<Uuid>>,
#[serde(default)]
pub parent_message_id: Option<Uuid>,
#[serde(default)]
pub capabilities: Option<Vec<CapabilityValue>>,
pub tenant_id: Option<JsonValue>,
pub user_id: Option<JsonValue>,
}
#[tracing::instrument(
skip(svc, ctx, body),
fields(
request_id = Empty,
session_id = Empty,
),
)]
pub async fn send_message(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<MessageService>>,
Json(body): Json<SendMessageBody>,
) -> Result<Response> {
reject_body_identity(&body.tenant_id, &body.user_id)?;
let identity = identity_from_ctx(&ctx)?;
tracing::Span::current().record("session_id", tracing::field::display(body.session_id));
let cancel = CancellationToken::new();
let req = SendMessageRequest {
session_id: body.session_id,
parts: body.parts,
file_ids: body.file_ids.unwrap_or_default(),
parent_message_id: body.parent_message_id,
capabilities: body.capabilities,
};
let event_stream = svc.send_message(req, identity, cancel).await?;
Ok(crate::api::rest::sse_delta_stream_response(event_stream))
}
#[derive(Debug, Serialize)]
pub struct DeleteMessageResponse {
pub message_id: Uuid,
pub deleted: bool,
pub deleted_count: u64,
pub deleted_at: String,
}
impl DeleteMessageResponse {
fn from_outcome(outcome: DeleteOutcome) -> Result<Self> {
let deleted_at = outcome.deleted_at.format(&Rfc3339).map_err(|err| {
ChatEngineError::internal(format!("failed to format deleted_at: {err}"))
})?;
Ok(Self {
message_id: outcome.message_id,
deleted: true,
deleted_count: outcome.deleted_count,
deleted_at,
})
}
}
#[tracing::instrument(
skip(svc, ctx),
fields(
session_id = Empty,
message_id = %message_id,
request_id = Empty,
),
)]
pub async fn delete_message(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<MessageService>>,
Path(message_id): Path<Uuid>,
) -> Result<Json<DeleteMessageResponse>> {
let identity = identity_from_ctx(&ctx)?;
let session_id = svc
.resolve_owned_message(&identity, message_id)
.await?
.session_id;
tracing::Span::current().record("session_id", tracing::field::display(session_id));
let outcome = svc
.delete_message_cascade(&identity, session_id, message_id)
.await?;
Ok(Json(DeleteMessageResponse::from_outcome(outcome)?))
}
#[cfg(test)]
#[path = "messages_tests.rs"]
mod messages_tests;