init
This commit is contained in:
@@ -658,6 +658,7 @@ impl MigrationTrait for Migration {
|
|||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
.col(ColumnDef::new(Alias::new("server_id")).uuid().null())
|
.col(ColumnDef::new(Alias::new("server_id")).uuid().null())
|
||||||
|
.col(ColumnDef::new(Alias::new("name")).string().not_null())
|
||||||
.col(ColumnDef::new(Alias::new("emoji_type")).string().not_null())
|
.col(ColumnDef::new(Alias::new("emoji_type")).string().not_null())
|
||||||
.col(ColumnDef::new(Alias::new("unicode_sequence")).text().null())
|
.col(ColumnDef::new(Alias::new("unicode_sequence")).text().null())
|
||||||
.col(ColumnDef::new(Alias::new("file_path")).text().null())
|
.col(ColumnDef::new(Alias::new("file_path")).text().null())
|
||||||
@@ -705,56 +706,6 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
seed_unicode_emojis(manager).await?;
|
seed_unicode_emojis(manager).await?;
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(Alias::new("emoji_alias"))
|
|
||||||
.if_not_exists()
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Alias::new("id"))
|
|
||||||
.uuid()
|
|
||||||
.not_null()
|
|
||||||
.primary_key(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(Alias::new("emoji_id")).uuid().not_null())
|
|
||||||
.col(ColumnDef::new(Alias::new("alias")).string().not_null())
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Alias::new("created_at"))
|
|
||||||
.timestamp_with_time_zone()
|
|
||||||
.not_null()
|
|
||||||
.default(Expr::current_timestamp()),
|
|
||||||
)
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Alias::new("updated_at"))
|
|
||||||
.timestamp_with_time_zone()
|
|
||||||
.not_null()
|
|
||||||
.default(Expr::current_timestamp()),
|
|
||||||
)
|
|
||||||
.foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_emoji_alias_emoji")
|
|
||||||
.from(Alias::new("emoji_alias"), Alias::new("emoji_id"))
|
|
||||||
.to(Alias::new("emoji"), Alias::new("id"))
|
|
||||||
.on_delete(ForeignKeyAction::Cascade),
|
|
||||||
)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_index(
|
|
||||||
Index::create()
|
|
||||||
.name("uq_emoji_alias_emoji")
|
|
||||||
.table(Alias::new("emoji_alias"))
|
|
||||||
.col(Alias::new("emoji_id"))
|
|
||||||
.col(Alias::new("alias"))
|
|
||||||
.unique()
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
seed_unicode_aliases(manager).await?;
|
|
||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
@@ -1019,7 +970,6 @@ impl MigrationTrait for Migration {
|
|||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
let tables = [
|
let tables = [
|
||||||
"computed_permission",
|
"computed_permission",
|
||||||
"emoji_alias",
|
|
||||||
"emoji",
|
"emoji",
|
||||||
"channel_user_read_state",
|
"channel_user_read_state",
|
||||||
"channel_user_permission",
|
"channel_user_permission",
|
||||||
@@ -1060,21 +1010,42 @@ impl MigrationTrait for Migration {
|
|||||||
/// number of bind parameters in a single statement to 999.
|
/// number of bind parameters in a single statement to 999.
|
||||||
async fn seed_unicode_emojis(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
async fn seed_unicode_emojis(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
const CHUNK_SIZE: usize = 250;
|
const CHUNK_SIZE: usize = 250;
|
||||||
let rows = unicode_rows()?;
|
let mut used_names = HashSet::with_capacity(3944);
|
||||||
|
let rows = unicode_rows()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, sequence, raw_name)| {
|
||||||
|
let base = normalize_unicode_name(&raw_name);
|
||||||
|
let mut name = base.clone();
|
||||||
|
if !used_names.insert(name.clone()) {
|
||||||
|
name = format!("{base}_u{}", sequence.replace(' ', ""));
|
||||||
|
if name.len() > 64 {
|
||||||
|
name.truncate(64);
|
||||||
|
}
|
||||||
|
if !used_names.insert(name.clone()) {
|
||||||
|
return Err(DbErr::Custom(format!(
|
||||||
|
"duplicate generated emoji name: {name}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((id, sequence, name))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, DbErr>>()?;
|
||||||
|
|
||||||
for chunk in rows.chunks(CHUNK_SIZE) {
|
for chunk in rows.chunks(CHUNK_SIZE) {
|
||||||
let mut insert = Query::insert();
|
let mut insert = Query::insert();
|
||||||
insert.into_table(Alias::new("emoji")).columns([
|
insert.into_table(Alias::new("emoji")).columns([
|
||||||
Alias::new("id"),
|
Alias::new("id"),
|
||||||
Alias::new("server_id"),
|
Alias::new("server_id"),
|
||||||
|
Alias::new("name"),
|
||||||
Alias::new("emoji_type"),
|
Alias::new("emoji_type"),
|
||||||
Alias::new("unicode_sequence"),
|
Alias::new("unicode_sequence"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (id, sequence, _) in chunk {
|
for (id, sequence, name) in chunk {
|
||||||
insert.values_panic([
|
insert.values_panic([
|
||||||
Expr::val(*id).into(),
|
Expr::val(*id).into(),
|
||||||
Expr::val(Option::<Uuid>::None).into(),
|
Expr::val(Option::<Uuid>::None).into(),
|
||||||
|
Expr::val(name.as_str()).into(),
|
||||||
Expr::val("unicode").into(),
|
Expr::val("unicode").into(),
|
||||||
Expr::val(sequence.as_str()).into(),
|
Expr::val(sequence.as_str()).into(),
|
||||||
]);
|
]);
|
||||||
@@ -1086,61 +1057,6 @@ async fn seed_unicode_emojis(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn seed_unicode_aliases(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
|
||||||
const CHUNK_SIZE: usize = 250;
|
|
||||||
let rows = unicode_rows()?;
|
|
||||||
let mut aliases = Vec::with_capacity(rows.len());
|
|
||||||
let mut used = HashSet::with_capacity(rows.len());
|
|
||||||
|
|
||||||
for (_, sequence, name) in rows {
|
|
||||||
let base = normalize_unicode_alias(&name);
|
|
||||||
let mut alias = base.clone();
|
|
||||||
if !used.insert(alias.clone()) {
|
|
||||||
alias = format!("{base}_u{}", sequence.replace(' ', ""));
|
|
||||||
if alias.len() > 64 {
|
|
||||||
alias.truncate(64);
|
|
||||||
}
|
|
||||||
if !used.insert(alias.clone()) {
|
|
||||||
return Err(DbErr::Custom(format!(
|
|
||||||
"duplicate generated emoji alias: {alias}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let emoji_id = Uuid::new_v5(
|
|
||||||
&Uuid::NAMESPACE_URL,
|
|
||||||
format!("https://oxspeak.local/unicode/{UNICODE_EMOJI_VERSION}/{sequence}").as_bytes(),
|
|
||||||
);
|
|
||||||
let id = Uuid::new_v5(
|
|
||||||
&Uuid::NAMESPACE_URL,
|
|
||||||
format!("https://oxspeak.local/unicode-alias/{UNICODE_EMOJI_VERSION}/{sequence}")
|
|
||||||
.as_bytes(),
|
|
||||||
);
|
|
||||||
aliases.push((id, emoji_id, alias));
|
|
||||||
}
|
|
||||||
|
|
||||||
for chunk in aliases.chunks(CHUNK_SIZE) {
|
|
||||||
let mut insert = Query::insert();
|
|
||||||
insert.into_table(Alias::new("emoji_alias")).columns([
|
|
||||||
Alias::new("id"),
|
|
||||||
Alias::new("emoji_id"),
|
|
||||||
Alias::new("alias"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (id, emoji_id, alias) in chunk {
|
|
||||||
insert.values_panic([
|
|
||||||
Expr::val(*id).into(),
|
|
||||||
Expr::val(*emoji_id).into(),
|
|
||||||
Expr::val(alias.as_str()).into(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.exec_stmt(insert).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unicode_rows() -> Result<Vec<(Uuid, String, String)>, DbErr> {
|
fn unicode_rows() -> Result<Vec<(Uuid, String, String)>, DbErr> {
|
||||||
UNICODE_EMOJI_DATA
|
UNICODE_EMOJI_DATA
|
||||||
.lines()
|
.lines()
|
||||||
@@ -1160,7 +1076,7 @@ fn unicode_rows() -> Result<Vec<(Uuid, String, String)>, DbErr> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_unicode_alias(name: &str) -> String {
|
fn normalize_unicode_name(name: &str) -> String {
|
||||||
let mut alias = String::with_capacity(name.len());
|
let mut alias = String::with_capacity(name.len());
|
||||||
let mut separator = false;
|
let mut separator = false;
|
||||||
|
|
||||||
@@ -1223,16 +1139,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unicode_aliases_are_normalized_for_the_existing_api_constraints() {
|
fn unicode_names_are_normalized_for_the_existing_api_constraints() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
normalize_unicode_alias("flag: United States"),
|
normalize_unicode_name("flag: United States"),
|
||||||
"flag_united_states"
|
"flag_united_states"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
normalize_unicode_alias("thumbs up: medium skin tone"),
|
normalize_unicode_name("thumbs up: medium skin tone"),
|
||||||
"thumbs_up_medium_skin_tone"
|
"thumbs_up_medium_skin_tone"
|
||||||
);
|
);
|
||||||
assert!(normalize_unicode_alias("woman technologist")
|
assert!(normalize_unicode_name("woman technologist")
|
||||||
.chars()
|
.chars()
|
||||||
.all(|character| character.is_ascii_alphanumeric() || character == '_'));
|
.all(|character| character.is_ascii_alphanumeric() || character == '_'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ pub struct CreateEmojiRequest {
|
|||||||
pub server_id: Option<Uuid>,
|
pub server_id: Option<Uuid>,
|
||||||
pub emoji_type: String,
|
pub emoji_type: String,
|
||||||
pub unicode_sequence: Option<String>,
|
pub unicode_sequence: Option<String>,
|
||||||
pub aliases: Vec<String>,
|
pub name: String,
|
||||||
pub mime_type: Option<String>,
|
pub mime_type: Option<String>,
|
||||||
pub is_animated: Option<bool>,
|
pub is_animated: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
pub struct UpdateEmojiRequest {
|
pub struct UpdateEmojiRequest {
|
||||||
pub aliases: Option<Vec<String>>,
|
pub name: Option<String>,
|
||||||
pub server_id: Option<Uuid>,
|
pub server_id: Option<Uuid>,
|
||||||
pub unicode_sequence: Option<String>,
|
pub unicode_sequence: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ pub struct EmojiResponse {
|
|||||||
pub server_id: Option<Uuid>,
|
pub server_id: Option<Uuid>,
|
||||||
pub emoji_type: String,
|
pub emoji_type: String,
|
||||||
pub unicode_sequence: Option<String>,
|
pub unicode_sequence: Option<String>,
|
||||||
pub aliases: Vec<String>,
|
pub name: String,
|
||||||
pub asset_url: Option<String>,
|
pub asset_url: Option<String>,
|
||||||
pub mime_type: Option<String>,
|
pub mime_type: Option<String>,
|
||||||
pub file_size: Option<i64>,
|
pub file_size: Option<i64>,
|
||||||
|
|||||||
+1
-2
@@ -9,6 +9,7 @@ pub struct Model {
|
|||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub server_id: Option<Uuid>,
|
pub server_id: Option<Uuid>,
|
||||||
|
pub name: String,
|
||||||
pub emoji_type: String,
|
pub emoji_type: String,
|
||||||
pub unicode_sequence: Option<String>,
|
pub unicode_sequence: Option<String>,
|
||||||
pub file_path: Option<String>,
|
pub file_path: Option<String>,
|
||||||
@@ -18,8 +19,6 @@ pub struct Model {
|
|||||||
pub sha256: Option<String>,
|
pub sha256: Option<String>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
#[sea_orm(has_many)]
|
|
||||||
pub aliases: HasMany<super::emoji_alias::Entity>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
use sea_orm::Set;
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use sea_orm::prelude::async_trait::async_trait;
|
|
||||||
|
|
||||||
#[sea_orm::model]
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
|
||||||
#[sea_orm(table_name = "emoji_alias")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
|
||||||
pub id: Uuid,
|
|
||||||
pub emoji_id: Uuid,
|
|
||||||
pub alias: String,
|
|
||||||
pub created_at: DateTimeUtc,
|
|
||||||
pub updated_at: DateTimeUtc,
|
|
||||||
#[sea_orm(belongs_to, from = "emoji_id", to = "id", on_delete = "Cascade")]
|
|
||||||
pub emoji: HasOne<super::emoji::Entity>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ActiveModelBehavior for ActiveModel {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Set(Uuid::new_v4()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ pub mod channel_user_permission;
|
|||||||
pub mod channel_user_read_state;
|
pub mod channel_user_read_state;
|
||||||
pub mod computed_permission;
|
pub mod computed_permission;
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod emoji_alias;
|
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod role_user;
|
pub mod role_user;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ pub use super::channel_user::Entity as ChannelUser;
|
|||||||
pub use super::channel_user_read_state::Entity as ChannelUserReadState;
|
pub use super::channel_user_read_state::Entity as ChannelUserReadState;
|
||||||
pub use super::computed_permission::Entity as ComputedPermission;
|
pub use super::computed_permission::Entity as ComputedPermission;
|
||||||
pub use super::emoji::Entity as Emoji;
|
pub use super::emoji::Entity as Emoji;
|
||||||
pub use super::emoji_alias::Entity as EmojiAlias;
|
|
||||||
pub use super::message::Entity as Message;
|
pub use super::message::Entity as Message;
|
||||||
pub use super::role::Entity as Group;
|
pub use super::role::Entity as Group;
|
||||||
pub use super::role_user::Entity as GroupMember;
|
pub use super::role_user::Entity as GroupMember;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::models::{emoji, emoji_alias};
|
use crate::models::emoji;
|
||||||
use crate::repositories::{AnyResult, RepositoryContext};
|
use crate::repositories::{AnyResult, RepositoryContext};
|
||||||
use sea_orm::{
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, ExprTrait, QueryFilter, QueryOrder};
|
||||||
ActiveModelTrait, ColumnTrait, EntityTrait, ExprTrait, QueryFilter, QueryOrder, Set,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -42,30 +40,16 @@ impl EmojiRepository {
|
|||||||
Ok(query.all(&self.context.db).await?)
|
Ok(query.all(&self.context.db).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn aliases(&self, emoji_id: Uuid) -> AnyResult<Vec<emoji_alias::Model>> {
|
pub async fn find_name(
|
||||||
Ok(emoji_alias::Entity::find()
|
|
||||||
.filter(emoji_alias::Column::EmojiId.eq(emoji_id))
|
|
||||||
.all(&self.context.db)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find_alias(
|
|
||||||
&self,
|
&self,
|
||||||
alias: &str,
|
name: &str,
|
||||||
server_id: Option<Uuid>,
|
server_id: Option<Uuid>,
|
||||||
) -> AnyResult<Option<emoji::Model>> {
|
) -> AnyResult<Option<emoji::Model>> {
|
||||||
let rows = self.list(server_id).await?;
|
Ok(self
|
||||||
for model in rows {
|
.list(server_id)
|
||||||
if self
|
.await?
|
||||||
.aliases(model.id)
|
.into_iter()
|
||||||
.await?
|
.find(|model| model.name == name))
|
||||||
.iter()
|
|
||||||
.any(|a| a.alias == alias)
|
|
||||||
{
|
|
||||||
return Ok(Some(model));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(&self, model: emoji::ActiveModel) -> AnyResult<emoji::Model> {
|
pub async fn create(&self, model: emoji::ActiveModel) -> AnyResult<emoji::Model> {
|
||||||
@@ -81,20 +65,4 @@ impl EmojiRepository {
|
|||||||
.rows_affected
|
.rows_affected
|
||||||
> 0)
|
> 0)
|
||||||
}
|
}
|
||||||
pub async fn add_alias(&self, emoji_id: Uuid, alias: String) -> AnyResult<emoji_alias::Model> {
|
|
||||||
Ok((emoji_alias::ActiveModel {
|
|
||||||
emoji_id: Set(emoji_id),
|
|
||||||
alias: Set(alias),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.insert(&self.context.db)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
pub async fn clear_aliases(&self, emoji_id: Uuid) -> AnyResult<()> {
|
|
||||||
emoji_alias::Entity::delete_many()
|
|
||||||
.filter(emoji_alias::Column::EmojiId.eq(emoji_id))
|
|
||||||
.exec(&self.context.db)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use axum::{
|
|||||||
http::{StatusCode, header},
|
http::{StatusCode, header},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use sea_orm::{EntityTrait, Set};
|
use sea_orm::Set;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -34,8 +34,7 @@ pub async fn get_all(
|
|||||||
) -> Result<Json<Vec<crate::domain::dto::emoji::EmojiResponse>>, HTTPError> {
|
) -> Result<Json<Vec<crate::domain::dto::emoji::EmojiResponse>>, HTTPError> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for emoji in state.repositories.emoji.list(query.server_id).await? {
|
for emoji in state.repositories.emoji.list(query.server_id).await? {
|
||||||
let aliases = state.repositories.emoji.aliases(emoji.id).await?;
|
result.push(mapper::response(emoji));
|
||||||
result.push(mapper::response(emoji, mapper::aliases(aliases)));
|
|
||||||
}
|
}
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
@@ -51,8 +50,7 @@ pub async fn get_by_id(
|
|||||||
.get_by_id(id)
|
.get_by_id(id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
let aliases = state.repositories.emoji.aliases(id).await?;
|
Ok(Json(mapper::response(emoji)))
|
||||||
Ok(Json(mapper::response(emoji, mapper::aliases(aliases))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(post, path = "/emojis", responses((status = 201, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
|
#[utoipa::path(post, path = "/emojis", responses((status = 201, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
|
||||||
@@ -63,7 +61,7 @@ pub async fn create(
|
|||||||
let mut server_id = None;
|
let mut server_id = None;
|
||||||
let mut emoji_type = None;
|
let mut emoji_type = None;
|
||||||
let mut unicode_sequence = None;
|
let mut unicode_sequence = None;
|
||||||
let mut aliases = Vec::new();
|
let mut name = None;
|
||||||
let mut file = None;
|
let mut file = None;
|
||||||
let mut mime = None;
|
let mut mime = None;
|
||||||
let mut animated = false;
|
let mut animated = false;
|
||||||
@@ -72,8 +70,8 @@ pub async fn create(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| HTTPError::BadRequest(e.to_string()))?
|
.map_err(|e| HTTPError::BadRequest(e.to_string()))?
|
||||||
{
|
{
|
||||||
let name = field.name().unwrap_or_default().to_string();
|
let field_name = field.name().unwrap_or_default().to_string();
|
||||||
if name == "file" {
|
if field_name == "file" {
|
||||||
mime = field.content_type().map(str::to_string);
|
mime = field.content_type().map(str::to_string);
|
||||||
file = Some(
|
file = Some(
|
||||||
field
|
field
|
||||||
@@ -87,7 +85,7 @@ pub async fn create(
|
|||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| HTTPError::BadRequest(e.to_string()))?;
|
.map_err(|e| HTTPError::BadRequest(e.to_string()))?;
|
||||||
match name.as_str() {
|
match field_name.as_str() {
|
||||||
"server_id" => {
|
"server_id" => {
|
||||||
server_id = Some(
|
server_id = Some(
|
||||||
value
|
value
|
||||||
@@ -97,13 +95,13 @@ pub async fn create(
|
|||||||
}
|
}
|
||||||
"emoji_type" => emoji_type = Some(value),
|
"emoji_type" => emoji_type = Some(value),
|
||||||
"unicode_sequence" => unicode_sequence = Some(value),
|
"unicode_sequence" => unicode_sequence = Some(value),
|
||||||
"alias" => aliases.push(value),
|
"name" => name = Some(value),
|
||||||
"aliases" => aliases.extend(value.split(',').map(str::to_string)),
|
|
||||||
"is_animated" => animated = value.parse().unwrap_or(false),
|
"is_animated" => animated = value.parse().unwrap_or(false),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let emoji_type = normalize_type(emoji_type.as_deref().unwrap_or("custom"))?;
|
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() {
|
if emoji_type == "unicode" && unicode_sequence.is_none() {
|
||||||
return Err(HTTPError::BadRequest("unicode_sequence is required".into()));
|
return Err(HTTPError::BadRequest("unicode_sequence is required".into()));
|
||||||
}
|
}
|
||||||
@@ -138,6 +136,7 @@ pub async fn create(
|
|||||||
let model = emoji::ActiveModel {
|
let model = emoji::ActiveModel {
|
||||||
id: Set(id),
|
id: Set(id),
|
||||||
server_id: Set(server_id),
|
server_id: Set(server_id),
|
||||||
|
name: Set(String::new()),
|
||||||
emoji_type: Set(emoji_type),
|
emoji_type: Set(emoji_type),
|
||||||
unicode_sequence: Set(unicode_sequence),
|
unicode_sequence: Set(unicode_sequence),
|
||||||
file_path: Set(path),
|
file_path: Set(path),
|
||||||
@@ -147,12 +146,8 @@ pub async fn create(
|
|||||||
sha256: Set(sha),
|
sha256: Set(sha),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let created = state.services.emoji.create(model, aliases).await?;
|
let created = state.services.emoji.create(model, name).await?;
|
||||||
let aliases = state.repositories.emoji.aliases(created.id).await?;
|
Ok((StatusCode::CREATED, Json(mapper::response(created))))
|
||||||
Ok((
|
|
||||||
StatusCode::CREATED,
|
|
||||||
Json(mapper::response(created, mapper::aliases(aliases))),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(put, path = "/emojis/{id}", request_body = UpdateEmojiRequest, params(("id" = Uuid, Path)), responses((status = 200, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
|
#[utoipa::path(put, path = "/emojis/{id}", request_body = UpdateEmojiRequest, params(("id" = Uuid, Path)), responses((status = 200, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
|
||||||
@@ -168,21 +163,11 @@ pub async fn update(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
let target_server_id = payload.server_id.or(existing.server_id);
|
let target_server_id = payload.server_id.or(existing.server_id);
|
||||||
let aliases_to_validate = match &payload.aliases {
|
let target_name = payload.name.as_deref().unwrap_or(&existing.name);
|
||||||
Some(aliases) => aliases.clone(),
|
|
||||||
None => state
|
|
||||||
.repositories
|
|
||||||
.emoji
|
|
||||||
.aliases(id)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|a| a.alias)
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
state
|
state
|
||||||
.services
|
.services
|
||||||
.emoji
|
.emoji
|
||||||
.aliases_available(&aliases_to_validate, target_server_id, Some(id))
|
.name_available(target_name, target_server_id, Some(id))
|
||||||
.await?;
|
.await?;
|
||||||
let mut active: emoji::ActiveModel = existing.into();
|
let mut active: emoji::ActiveModel = existing.into();
|
||||||
if let Some(server_id) = payload.server_id {
|
if let Some(server_id) = payload.server_id {
|
||||||
@@ -197,21 +182,11 @@ pub async fn update(
|
|||||||
if payload.unicode_sequence.is_some() {
|
if payload.unicode_sequence.is_some() {
|
||||||
active.unicode_sequence = Set(payload.unicode_sequence);
|
active.unicode_sequence = Set(payload.unicode_sequence);
|
||||||
}
|
}
|
||||||
let updated = state.repositories.emoji.update(active).await?;
|
if let Some(name) = payload.name {
|
||||||
if let Some(ref aliases) = payload.aliases {
|
active.name = Set(EmojiService::normalize_name(&name)?);
|
||||||
state
|
|
||||||
.services
|
|
||||||
.emoji
|
|
||||||
.aliases_available(aliases, updated.server_id, Some(id))
|
|
||||||
.await?;
|
|
||||||
state
|
|
||||||
.services
|
|
||||||
.emoji
|
|
||||||
.replace_aliases(id, aliases.clone())
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
let aliases = state.repositories.emoji.aliases(id).await?;
|
let updated = state.repositories.emoji.update(active).await?;
|
||||||
Ok(Json(mapper::response(updated, mapper::aliases(aliases))))
|
Ok(Json(mapper::response(updated)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_mime(bytes: &[u8]) -> Option<String> {
|
fn detect_mime(bytes: &[u8]) -> Option<String> {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use crate::{domain::dto::emoji::EmojiResponse, models::emoji};
|
use crate::{domain::dto::emoji::EmojiResponse, models::emoji};
|
||||||
|
|
||||||
pub fn response(model: emoji::Model, aliases: Vec<String>) -> EmojiResponse {
|
pub fn response(model: emoji::Model) -> EmojiResponse {
|
||||||
EmojiResponse {
|
EmojiResponse {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
server_id: model.server_id,
|
server_id: model.server_id,
|
||||||
emoji_type: model.emoji_type,
|
emoji_type: model.emoji_type,
|
||||||
unicode_sequence: model.unicode_sequence,
|
unicode_sequence: model.unicode_sequence,
|
||||||
aliases,
|
name: model.name,
|
||||||
asset_url: model
|
asset_url: model
|
||||||
.file_path
|
.file_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -19,7 +19,3 @@ pub fn response(model: emoji::Model, aliases: Vec<String>) -> EmojiResponse {
|
|||||||
updated_at: model.updated_at,
|
updated_at: model.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn aliases(models: Vec<crate::models::emoji_alias::Model>) -> Vec<String> {
|
|
||||||
models.into_iter().map(|a| a.alias).collect()
|
|
||||||
}
|
|
||||||
|
|||||||
+21
-69
@@ -1,7 +1,7 @@
|
|||||||
use crate::http::error::HTTPError;
|
use crate::http::error::HTTPError;
|
||||||
use crate::models::emoji;
|
use crate::models::emoji;
|
||||||
use crate::services::ServicesContext;
|
use crate::services::ServicesContext;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
|
use sea_orm::{ActiveModelTrait, Set, TransactionTrait};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -19,32 +19,23 @@ impl EmojiService {
|
|||||||
pub fn new(context: Arc<ServicesContext>) -> Self {
|
pub fn new(context: Arc<ServicesContext>) -> Self {
|
||||||
Self { context }
|
Self { context }
|
||||||
}
|
}
|
||||||
pub fn normalize_alias(alias: &str) -> Result<String, HTTPError> {
|
pub fn normalize_name(name: &str) -> Result<String, HTTPError> {
|
||||||
let alias = alias.trim().trim_matches(':').to_lowercase();
|
let name = name.trim().trim_matches(':').to_lowercase();
|
||||||
if alias.is_empty()
|
if name.is_empty()
|
||||||
|| alias.len() > 64
|
|| name.len() > 64
|
||||||
|| !alias
|
|| !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||||
.chars()
|
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+')
|
|
||||||
{
|
{
|
||||||
return Err(HTTPError::BadRequest("Invalid emoji alias".into()));
|
return Err(HTTPError::BadRequest("Invalid emoji name".into()));
|
||||||
}
|
}
|
||||||
Ok(alias)
|
Ok(name)
|
||||||
}
|
}
|
||||||
pub async fn aliases_available(
|
pub async fn name_available(
|
||||||
&self,
|
&self,
|
||||||
aliases: &[String],
|
name: &str,
|
||||||
server_id: Option<Uuid>,
|
server_id: Option<Uuid>,
|
||||||
except: Option<Uuid>,
|
except: Option<Uuid>,
|
||||||
) -> Result<(), HTTPError> {
|
) -> Result<(), HTTPError> {
|
||||||
let normalized = aliases
|
let name = Self::normalize_name(name)?;
|
||||||
.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
|
let scoped = self
|
||||||
.context
|
.context
|
||||||
.repositories
|
.repositories
|
||||||
@@ -55,74 +46,35 @@ impl EmojiService {
|
|||||||
if Some(model.id) == except {
|
if Some(model.id) == except {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for alias in self.context.repositories.emoji.aliases(model.id).await? {
|
if model.name == name {
|
||||||
if normalized.iter().any(|candidate| candidate == &alias.alias) {
|
return Err(HTTPError::BadRequest(
|
||||||
return Err(HTTPError::BadRequest(
|
"Emoji name already exists in this scope".into(),
|
||||||
"Emoji alias already exists in this scope".into(),
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
&self,
|
&self,
|
||||||
model: emoji::ActiveModel,
|
mut model: emoji::ActiveModel,
|
||||||
aliases: Vec<String>,
|
name: String,
|
||||||
) -> Result<emoji::Model, HTTPError> {
|
) -> Result<emoji::Model, HTTPError> {
|
||||||
let db = &self.context.repositories.emoji.context.db;
|
let db = &self.context.repositories.emoji.context.db;
|
||||||
let aliases = aliases
|
let name = Self::normalize_name(&name)?;
|
||||||
.into_iter()
|
|
||||||
.map(|a| Self::normalize_alias(&a))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
let server_id = match &model.server_id {
|
let server_id = match &model.server_id {
|
||||||
sea_orm::ActiveValue::Set(value) => *value,
|
sea_orm::ActiveValue::Set(value) => *value,
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
self.aliases_available(&aliases, server_id, None).await?;
|
self.name_available(&name, server_id, None).await?;
|
||||||
|
model.name = Set(name);
|
||||||
let result = db
|
let result = db
|
||||||
.transaction::<_, emoji::Model, anyhow::Error>(|txn| {
|
.transaction::<_, emoji::Model, anyhow::Error>(|txn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move { Ok(model.insert(txn).await?) })
|
||||||
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
|
.await
|
||||||
.map_err(|e| HTTPError::Internal(anyhow::anyhow!(e)))?;
|
.map_err(|e| HTTPError::Internal(anyhow::anyhow!(e)))?;
|
||||||
Ok(result)
|
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 {
|
pub fn hash(data: &[u8]) -> String {
|
||||||
Sha256::digest(data)
|
Sha256::digest(data)
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
Reference in New Issue
Block a user