use crate::http::error::HTTPError; use crate::models::emoji; use crate::services::ServicesContext; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait}; use sha2::{Digest, Sha256}; use std::{ path::{Path, PathBuf}, sync::Arc, }; use tokio::fs; use uuid::Uuid; #[derive(Debug, Clone)] pub struct EmojiService { context: Arc, } impl EmojiService { pub fn new(context: Arc) -> Self { Self { context } } pub fn normalize_alias(alias: &str) -> Result { let alias = alias.trim().trim_matches(':').to_lowercase(); if alias.is_empty() || alias.len() > 64 || !alias .chars() .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+') { return Err(HTTPError::BadRequest("Invalid emoji alias".into())); } Ok(alias) } pub async fn aliases_available( &self, aliases: &[String], server_id: Option, except: Option, ) -> Result<(), HTTPError> { let normalized = aliases .iter() .map(|a| Self::normalize_alias(a)) .collect::, _>>()?; let mut unique = std::collections::HashSet::new(); if normalized.iter().any(|alias| !unique.insert(alias)) { return Err(HTTPError::BadRequest("Duplicate emoji alias".into())); } let scoped = self .context .repositories .emoji .list_exact_scope(server_id) .await?; for model in scoped { if Some(model.id) == except { continue; } for alias in self.context.repositories.emoji.aliases(model.id).await? { if normalized.iter().any(|candidate| candidate == &alias.alias) { return Err(HTTPError::BadRequest( "Emoji alias already exists in this scope".into(), )); } } } Ok(()) } pub async fn create( &self, model: emoji::ActiveModel, aliases: Vec, ) -> Result { let db = &self.context.repositories.emoji.context.db; let aliases = aliases .into_iter() .map(|a| Self::normalize_alias(&a)) .collect::, _>>()?; let server_id = match &model.server_id { sea_orm::ActiveValue::Set(value) => *value, _ => None, }; self.aliases_available(&aliases, server_id, None).await?; let result = db .transaction::<_, emoji::Model, anyhow::Error>(|txn| { Box::pin(async move { let model = model.insert(txn).await?; for alias in aliases { crate::models::emoji_alias::ActiveModel { emoji_id: Set(model.id), alias: Set(alias), ..Default::default() } .insert(txn) .await?; } Ok(model) }) }) .await .map_err(|e| HTTPError::Internal(anyhow::anyhow!(e)))?; Ok(result) } pub async fn replace_aliases(&self, id: Uuid, aliases: Vec) -> Result<(), HTTPError> { let aliases = aliases .into_iter() .map(|a| Self::normalize_alias(&a)) .collect::, _>>()?; let db = &self.context.repositories.emoji.context.db; let txn = db.begin().await?; crate::models::emoji_alias::Entity::delete_many() .filter(crate::models::emoji_alias::Column::EmojiId.eq(id)) .exec(&txn) .await?; for alias in aliases { crate::models::emoji_alias::ActiveModel { emoji_id: Set(id), alias: Set(alias), ..Default::default() } .insert(&txn) .await?; } txn.commit().await?; Ok(()) } pub fn hash(data: &[u8]) -> String { format!("{:x}", Sha256::digest(data)) } pub async fn save_asset( root: &Path, id: Uuid, data: &[u8], ) -> Result<(String, String), HTTPError> { let dir = root.join("emoji"); fs::create_dir_all(&dir) .await .map_err(|e| HTTPError::InternalServerError(e.to_string()))?; let relative = PathBuf::from("emoji").join(id.to_string()); let path = root.join(&relative); fs::write(&path, data) .await .map_err(|e| HTTPError::InternalServerError(e.to_string()))?; Ok((relative.to_string_lossy().into_owned(), Self::hash(data))) } pub async fn remove_asset(root: &Path, path: Option<&str>) { if let Some(path) = path { let _ = fs::remove_file(root.join(path)).await; } } }