151 lines
5.0 KiB
Rust
151 lines
5.0 KiB
Rust
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<ServicesContext>,
|
|
}
|
|
|
|
impl EmojiService {
|
|
pub fn new(context: Arc<ServicesContext>) -> Self {
|
|
Self { context }
|
|
}
|
|
pub fn normalize_alias(alias: &str) -> Result<String, HTTPError> {
|
|
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<Uuid>,
|
|
except: Option<Uuid>,
|
|
) -> Result<(), HTTPError> {
|
|
let normalized = aliases
|
|
.iter()
|
|
.map(|a| Self::normalize_alias(a))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
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<String>,
|
|
) -> Result<emoji::Model, HTTPError> {
|
|
let db = &self.context.repositories.emoji.context.db;
|
|
let aliases = aliases
|
|
.into_iter()
|
|
.map(|a| Self::normalize_alias(&a))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
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<String>) -> Result<(), HTTPError> {
|
|
let aliases = aliases
|
|
.into_iter()
|
|
.map(|a| Self::normalize_alias(&a))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
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;
|
|
}
|
|
}
|
|
}
|