use std::sync::Arc;
use axum::Extension;
use axum::Json;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use serde_json::Value as JsonValue;
use tracing::field::Empty;
use uuid::Uuid;
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::reaction::{MessageReaction, ReactionType};
use crate::domain::service::reaction_service::{
CAPABILITY_FEEDBACK, ReactionService, ReactionsListing, SetReactionResponse,
};
#[derive(Debug, Deserialize)]
pub struct SetReactionBody {
pub reaction_type: ReactionType,
#[serde(default)]
pub tenant_id: Option<JsonValue>,
#[serde(default)]
pub user_id: Option<JsonValue>,
}
#[toolkit_macros::api_dto(response)]
#[derive(Debug)]
pub struct SetReactionResponseDto {
pub message_id: Uuid,
#[schema(value_type = String)]
pub reaction_type: ReactionType,
pub applied: bool,
}
impl From<SetReactionResponse> for SetReactionResponseDto {
fn from(r: SetReactionResponse) -> Self {
Self {
message_id: r.message_id,
reaction_type: r.reaction_type,
applied: r.applied,
}
}
}
#[toolkit_macros::api_dto(response)]
#[derive(Debug)]
pub struct ReactionDto {
pub user_id: String,
#[schema(value_type = String)]
pub reaction_type: ReactionType,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: time::OffsetDateTime,
}
impl From<MessageReaction> for ReactionDto {
fn from(r: MessageReaction) -> Self {
Self {
user_id: r.user_id,
reaction_type: r.reaction_type,
created_at: r.created_at,
updated_at: r.updated_at,
}
}
}
#[toolkit_macros::api_dto(response)]
#[derive(Debug)]
pub struct ListReactionsResponseDto {
pub message_id: Uuid,
pub reactions: Vec<ReactionDto>,
}
impl From<ReactionsListing> for ListReactionsResponseDto {
fn from(l: ReactionsListing) -> Self {
Self {
message_id: l.message_id,
reactions: l.reactions.into_iter().map(ReactionDto::from).collect(),
}
}
}
#[tracing::instrument(
skip(svc, ctx, body),
fields(
session_id = Empty,
message_id = Empty,
reaction = Empty,
),
)]
pub async fn set_reaction(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<ReactionService>>,
Path((session_id, message_id)): Path<(Uuid, Uuid)>,
Json(body): Json<SetReactionBody>,
) -> Response {
let span = tracing::Span::current();
span.record("session_id", tracing::field::display(session_id));
span.record("message_id", tracing::field::display(message_id));
span.record(
"reaction",
tracing::field::display(body.reaction_type.as_str()),
);
if let Err(err) = reject_body_identity(&body.tenant_id, &body.user_id) {
return err.into_response();
}
let identity = match identity_from_ctx(&ctx) {
Ok(id) => id,
Err(err) => return err.into_response(),
};
let service_outcome = svc
.set_reaction(&identity, session_id, message_id, body.reaction_type)
.await;
match service_outcome {
Ok((response, mutation)) => {
let dto = SetReactionResponseDto::from(response);
let http_response = (StatusCode::OK, Json(dto)).into_response();
drop(svc.spawn_plugin_notification(mutation));
http_response
}
Err(err) => map_reaction_error(err),
}
}
#[tracing::instrument(
skip(svc, ctx),
fields(
session_id = %session_id,
message_id = %message_id,
),
)]
pub async fn list_reactions(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<ReactionService>>,
Path((session_id, message_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<ListReactionsResponseDto>> {
let identity = identity_from_ctx(&ctx)?;
let listing = svc
.list_reactions(&identity, session_id, message_id)
.await?;
Ok(Json(ListReactionsResponseDto::from(listing)))
}
fn map_reaction_error(err: ChatEngineError) -> Response {
if let ChatEngineError::Conflict { reason } = &err
&& reason.contains(CAPABILITY_FEEDBACK)
{
return (
StatusCode::CONFLICT,
Json(serde_json::json!({
"error": "capability_disabled",
"capability": CAPABILITY_FEEDBACK,
})),
)
.into_response();
}
err.into_response()
}
#[cfg(test)]
#[path = "reactions_tests.rs"]
mod reactions_tests;