use crate::{ core::state::AppState, domain::dto::emoji::{EmojiQueryParams, UpdateEmojiRequest}, http::error::HTTPError, models::emoji, routes::emoji::mapper, services::emoji::EmojiService, }; use axum::{ Json, body::Body, extract::{Multipart, Path, Query, State}, http::{StatusCode, header}, response::Response, }; use sea_orm::Set; use std::path::PathBuf; use uuid::Uuid; fn normalize_type(value: &str) -> Result { if value == "unicode" || value == "custom" { Ok(value.to_string()) } else { Err(HTTPError::BadRequest( "emoji_type must be unicode or custom".into(), )) } } #[utoipa::path(get, path = "/emojis", params(EmojiQueryParams), responses((status = 200, body = [crate::domain::dto::emoji::EmojiResponse])), tag = "Emojis")] pub async fn get_all( State(state): State, Query(query): Query, ) -> Result>, HTTPError> { let mut result = Vec::new(); for emoji in state.repositories.emoji.list(query.server_id).await? { result.push(mapper::response(emoji)); } Ok(Json(result)) } #[utoipa::path(get, path = "/emojis/{id}", params(("id" = Uuid, Path)), responses((status = 200, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")] pub async fn get_by_id( State(state): State, Path(id): Path, ) -> Result, HTTPError> { let emoji = state .repositories .emoji .get_by_id(id) .await? .ok_or(HTTPError::NotFound)?; Ok(Json(mapper::response(emoji))) } #[utoipa::path(post, path = "/emojis", responses((status = 201, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")] pub async fn create( State(state): State, mut multipart: Multipart, ) -> Result<(StatusCode, Json), HTTPError> { let mut server_id = None; let mut emoji_type = None; let mut unicode_sequence = None; let mut name = None; let mut file = None; let mut mime = None; let mut animated = false; while let Some(field) = multipart .next_field() .await .map_err(|e| HTTPError::BadRequest(e.to_string()))? { let field_name = field.name().unwrap_or_default().to_string(); if field_name == "file" { mime = field.content_type().map(str::to_string); file = Some( field .bytes() .await .map_err(|e| HTTPError::BadRequest(e.to_string()))?, ); continue; } let value = field .text() .await .map_err(|e| HTTPError::BadRequest(e.to_string()))?; match field_name.as_str() { "server_id" => { server_id = Some( value .parse() .map_err(|_| HTTPError::BadRequest("Invalid server_id".into()))?, ) } "emoji_type" => emoji_type = Some(value), "unicode_sequence" => unicode_sequence = Some(value), "name" => name = Some(value), "is_animated" => animated = value.parse().unwrap_or(false), _ => {} } } let emoji_type = normalize_type(emoji_type.as_deref().unwrap_or("custom"))?; let name = name.ok_or(HTTPError::BadRequest("name is required".into()))?; if emoji_type == "unicode" && unicode_sequence.is_none() { return Err(HTTPError::BadRequest("unicode_sequence is required".into())); } if emoji_type == "custom" && file.is_none() { return Err(HTTPError::BadRequest( "file is required for custom emojis".into(), )); } if let Some(server_id) = server_id { state .repositories .server .get_by_id(server_id) .await? .ok_or(HTTPError::BadRequest("Server not found".into()))?; } let id = Uuid::new_v4(); let mut path = None; let mut sha = None; let mut size = None; if let Some(bytes) = file { size = Some(bytes.len() as i64); let detected = detect_mime(&bytes) .ok_or_else(|| HTTPError::BadRequest("Unsupported or invalid image format".into()))?; mime = Some(detected); let (p, h) = EmojiService::save_asset(std::path::Path::new(&state.config.media.root), id, &bytes) .await?; path = Some(p); sha = Some(h); } let model = emoji::ActiveModel { id: Set(id), server_id: Set(server_id), name: Set(String::new()), emoji_type: Set(emoji_type), unicode_sequence: Set(unicode_sequence), file_path: Set(path), mime_type: Set(mime), file_size: Set(size), is_animated: Set(animated), sha256: Set(sha), ..Default::default() }; let created = state.services.emoji.create(model, name).await?; Ok((StatusCode::CREATED, Json(mapper::response(created)))) } #[utoipa::path(put, path = "/emojis/{id}", request_body = UpdateEmojiRequest, params(("id" = Uuid, Path)), responses((status = 200, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")] pub async fn update( State(state): State, Path(id): Path, Json(payload): Json, ) -> Result, HTTPError> { let existing = state .repositories .emoji .get_by_id(id) .await? .ok_or(HTTPError::NotFound)?; let target_server_id = payload.server_id.or(existing.server_id); let target_name = payload.name.as_deref().unwrap_or(&existing.name); state .services .emoji .name_available(target_name, target_server_id, Some(id)) .await?; let mut active: emoji::ActiveModel = existing.into(); if let Some(server_id) = payload.server_id { state .repositories .server .get_by_id(server_id) .await? .ok_or(HTTPError::BadRequest("Server not found".into()))?; active.server_id = Set(Some(server_id)); } if payload.unicode_sequence.is_some() { active.unicode_sequence = Set(payload.unicode_sequence); } if let Some(name) = payload.name { active.name = Set(EmojiService::normalize_name(&name)?); } let updated = state.repositories.emoji.update(active).await?; Ok(Json(mapper::response(updated))) } fn detect_mime(bytes: &[u8]) -> Option { if bytes.starts_with(b"\x89PNG\r\n\x1a\n") { Some("image/png".into()) } else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") { Some("image/gif".into()) } else if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" { Some("image/webp".into()) } else { None } } #[utoipa::path(delete, path = "/emojis/{id}", params(("id" = Uuid, Path)), responses((status = 204)), tag = "Emojis")] pub async fn delete( State(state): State, Path(id): Path, ) -> Result { let model = state .repositories .emoji .get_by_id(id) .await? .ok_or(HTTPError::NotFound)?; let deleted = state.repositories.emoji.delete(id).await?; if deleted { EmojiService::remove_asset( std::path::Path::new(&state.config.media.root), model.file_path.as_deref(), ) .await; Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) } } pub async fn asset( State(state): State, Path(id): Path, ) -> Result { let model = state .repositories .emoji .get_by_id(id) .await? .ok_or(HTTPError::NotFound)?; let path = model.file_path.ok_or(HTTPError::NotFound)?; let bytes = tokio::fs::read(PathBuf::from(&state.config.media.root).join(path)) .await .map_err(|_| HTTPError::NotFound)?; let mut response = Response::new(Body::from(bytes)); if let Some(mime) = model.mime_type { if let Ok(value) = mime.parse() { response.headers_mut().insert(header::CONTENT_TYPE, value); } } Ok(response) }