241 lines
7.1 KiB
Rust
241 lines
7.1 KiB
Rust
use axum::{
|
||
extract::{Multipart, Path, State},
|
||
http::StatusCode,
|
||
response::{IntoResponse, Json},
|
||
};
|
||
use serde_json::json;
|
||
|
||
use crate::{
|
||
models::{EmoteResponse, UpdateEmoteRequest},
|
||
AppState,
|
||
};
|
||
|
||
/// GET /
|
||
/// Returns a simple health-check message.
|
||
pub async fn root() -> impl IntoResponse {
|
||
Json(json!({"status": "ok", "message": "mikebase server is running"}))
|
||
}
|
||
|
||
/// GET /json
|
||
/// Returns all emotes formatted as {"emotes": [...]}.
|
||
pub async fn list_emotes(State(state): State<AppState>) -> impl IntoResponse {
|
||
match state.db.list_emotes().await {
|
||
Ok(rows) => {
|
||
let emotes: Vec<EmoteResponse> = rows
|
||
.into_iter()
|
||
.map(|row| EmoteResponse {
|
||
name: row.name.clone(),
|
||
url: state.storage.public_url(&row.image_key),
|
||
created: row.created_dt(),
|
||
modified: row.modified_dt(),
|
||
})
|
||
.collect();
|
||
(StatusCode::OK, Json(json!({"emotes": emotes}))).into_response()
|
||
}
|
||
Err(e) => {
|
||
tracing::error!("Failed to list emotes: {e}");
|
||
(
|
||
StatusCode::INTERNAL_SERVER_ERROR,
|
||
Json(json!({"error": "Failed to list emotes"})),
|
||
)
|
||
.into_response()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// POST /emotes
|
||
/// Create a new emote. Accepts multipart/form-data with:
|
||
/// - `name` (text)
|
||
/// - `alias` (text, optional)
|
||
/// - `file` (binary – the emote image)
|
||
pub async fn create_emote(
|
||
State(state): State<AppState>,
|
||
mut multipart: Multipart,
|
||
) -> impl IntoResponse {
|
||
let mut name: Option<String> = None;
|
||
let mut alias: Option<String> = None;
|
||
let mut file_bytes: Option<bytes::Bytes> = None;
|
||
let mut file_name: Option<String> = None;
|
||
let mut content_type: Option<String> = None;
|
||
|
||
while let Ok(Some(field)) = multipart.next_field().await {
|
||
match field.name() {
|
||
Some("name") => {
|
||
name = field.text().await.ok();
|
||
}
|
||
Some("alias") => {
|
||
alias = field.text().await.ok();
|
||
}
|
||
Some("file") => {
|
||
file_name = field.file_name().map(|s| s.to_string());
|
||
content_type = field.content_type().map(|s| s.to_string());
|
||
file_bytes = field.bytes().await.ok();
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
let name = match name {
|
||
Some(n) if !n.is_empty() => n,
|
||
_ => {
|
||
return (
|
||
StatusCode::BAD_REQUEST,
|
||
Json(json!({"error": "Missing required field: name"})),
|
||
)
|
||
.into_response();
|
||
}
|
||
};
|
||
|
||
let (bytes, fname) = match (file_bytes, file_name) {
|
||
(Some(b), Some(n)) => (b, n),
|
||
_ => {
|
||
return (
|
||
StatusCode::BAD_REQUEST,
|
||
Json(json!({"error": "Missing required field: file"})),
|
||
)
|
||
.into_response();
|
||
}
|
||
};
|
||
|
||
let ct = content_type
|
||
.unwrap_or_else(|| mime_guess::from_path(&fname).first_or_octet_stream().to_string());
|
||
|
||
let key = format!("emoji/{fname}");
|
||
|
||
match state.storage.upload(&key, bytes, &ct).await {
|
||
Ok(_) => {}
|
||
Err(e) => {
|
||
tracing::error!("S3 upload failed: {e}");
|
||
return (
|
||
StatusCode::INTERNAL_SERVER_ERROR,
|
||
Json(json!({"error": "Failed to upload image"})),
|
||
)
|
||
.into_response();
|
||
}
|
||
}
|
||
|
||
match state
|
||
.db
|
||
.create_emote(&name, alias.as_deref(), &key)
|
||
.await
|
||
{
|
||
Ok(row) => {
|
||
let url = state.storage.public_url(&row.image_key);
|
||
let created = row.created_dt();
|
||
let modified = row.modified_dt();
|
||
let resp = EmoteResponse {
|
||
name: row.name,
|
||
url,
|
||
created,
|
||
modified,
|
||
};
|
||
(StatusCode::CREATED, Json(json!(resp))).into_response()
|
||
}
|
||
Err(e) => {
|
||
tracing::error!("DB insert failed: {e}");
|
||
(
|
||
StatusCode::INTERNAL_SERVER_ERROR,
|
||
Json(json!({"error": "Failed to create emote"})),
|
||
)
|
||
.into_response()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// PUT /emotes/:uuid
|
||
/// Update an existing emote's metadata.
|
||
/// Accepts JSON body with optional fields: `name`, `alias`, `image_key`.
|
||
pub async fn update_emote(
|
||
State(state): State<AppState>,
|
||
Path(uuid): Path<String>,
|
||
Json(payload): Json<UpdateEmoteRequest>,
|
||
) -> impl IntoResponse {
|
||
let alias_update: Option<Option<&str>> = payload.alias.as_ref().map(|a| Some(a.as_str()));
|
||
|
||
match state
|
||
.db
|
||
.update_emote(
|
||
&uuid,
|
||
payload.name.as_deref(),
|
||
alias_update,
|
||
payload.image_key.as_deref(),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Some(row)) => {
|
||
let url = state.storage.public_url(&row.image_key);
|
||
let created = row.created_dt();
|
||
let modified = row.modified_dt();
|
||
let resp = EmoteResponse {
|
||
name: row.name,
|
||
url,
|
||
created,
|
||
modified,
|
||
};
|
||
(StatusCode::OK, Json(json!(resp))).into_response()
|
||
}
|
||
Ok(None) => (
|
||
StatusCode::NOT_FOUND,
|
||
Json(json!({"error": "Emote not found"})),
|
||
)
|
||
.into_response(),
|
||
Err(e) => {
|
||
tracing::error!("DB update failed: {e}");
|
||
(
|
||
StatusCode::INTERNAL_SERVER_ERROR,
|
||
Json(json!({"error": "Failed to update emote"})),
|
||
)
|
||
.into_response()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// DELETE /emotes/:uuid
|
||
/// Delete an emote and its associated image from S3.
|
||
pub async fn delete_emote(
|
||
State(state): State<AppState>,
|
||
Path(uuid): Path<String>,
|
||
) -> impl IntoResponse {
|
||
// Fetch the row first so we can clean up S3.
|
||
let row = match state.db.get_emote_by_id(&uuid).await {
|
||
Ok(Some(r)) => r,
|
||
Ok(None) => {
|
||
return (
|
||
StatusCode::NOT_FOUND,
|
||
Json(json!({"error": "Emote not found"})),
|
||
)
|
||
.into_response();
|
||
}
|
||
Err(e) => {
|
||
tracing::error!("DB fetch failed: {e}");
|
||
return (
|
||
StatusCode::INTERNAL_SERVER_ERROR,
|
||
Json(json!({"error": "Failed to fetch emote"})),
|
||
)
|
||
.into_response();
|
||
}
|
||
};
|
||
|
||
// Delete from S3 (best-effort – don't abort if this fails).
|
||
if let Err(e) = state.storage.delete(&row.image_key).await {
|
||
tracing::warn!("S3 delete failed for key {}: {e}", row.image_key);
|
||
}
|
||
|
||
match state.db.delete_emote(&uuid).await {
|
||
Ok(true) => (StatusCode::NO_CONTENT).into_response(),
|
||
Ok(false) => (
|
||
StatusCode::NOT_FOUND,
|
||
Json(json!({"error": "Emote not found"})),
|
||
)
|
||
.into_response(),
|
||
Err(e) => {
|
||
tracing::error!("DB delete failed: {e}");
|
||
(
|
||
StatusCode::INTERNAL_SERVER_ERROR,
|
||
Json(json!({"error": "Failed to delete emote"})),
|
||
)
|
||
.into_response()
|
||
}
|
||
}
|
||
}
|