250 lines
8.3 KiB
Rust
250 lines
8.3 KiB
Rust
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<String, HTTPError> {
|
|
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<AppState>,
|
|
Query(query): Query<EmojiQueryParams>,
|
|
) -> Result<Json<Vec<crate::domain::dto::emoji::EmojiResponse>>, 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<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<crate::domain::dto::emoji::EmojiResponse>, 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<AppState>,
|
|
mut multipart: Multipart,
|
|
) -> Result<(StatusCode, Json<crate::domain::dto::emoji::EmojiResponse>), 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<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
Json(payload): Json<UpdateEmojiRequest>,
|
|
) -> Result<Json<crate::domain::dto::emoji::EmojiResponse>, 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<String> {
|
|
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<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<StatusCode, HTTPError> {
|
|
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<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Response, HTTPError> {
|
|
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)
|
|
}
|