This commit is contained in:
2026-08-16 13:00:29 +02:00
parent e6d6968e52
commit 7fc8edd1d0
71 changed files with 2359 additions and 448 deletions
Generated
+19
View File
@@ -549,6 +549,7 @@ dependencies = [
"matchit", "matchit",
"memchr", "memchr",
"mime", "mime",
"multer",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"serde_core", "serde_core",
@@ -2265,6 +2266,23 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.29.0"
@@ -2414,6 +2432,7 @@ dependencies = [
"sea-orm", "sea-orm",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9",
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
+790 -1
View File
@@ -12,7 +12,795 @@ members = [".", "migration", "event_bus"]
[dependencies] [dependencies]
tokio = { version = "1.53.1", features = ["full"] } tokio = { version = "1.53.1", features = ["full"] }
axum = { version = "0.8", features = ["ws"] } .0
axum = { version = "0.8", features = ["ws", "multipart"] }
axum-extra = { version = "0.12.6", features = ["cookie"] } axum-extra = { version = "0.12.6", features = ["cookie"] }
config = "0.15.25" config = "0.15.25"
sea-orm = { version = "2.0.0", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] } sea-orm = { version = "2.0.0", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
@@ -41,3 +829,4 @@ anyhow = "1.0.104"
futures-util = "0.3" futures-util = "0.3"
form_urlencoded = "1.2.2" form_urlencoded = "1.2.2"
time = "0.3.54" time = "0.3.54"
sha2 = "0.10"
+2 -2
View File
@@ -1,10 +1,10 @@
use std::sync::{ use std::sync::{
atomic::{AtomicU64, Ordering},
Arc, Arc,
atomic::{AtomicU64, Ordering},
}; };
use std::time::Instant; use std::time::Instant;
use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use event_bus::EventBus; use event_bus::EventBus;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::{match_event, EventBus}; use crate::{EventBus, match_event};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct User { struct User {
View File
+108 -4
View File
@@ -515,10 +515,7 @@ impl MigrationTrait for Migration {
.foreign_key( .foreign_key(
ForeignKey::create() ForeignKey::create()
.name("fk_channel_user_read_state_user") .name("fk_channel_user_read_state_user")
.from( .from(Alias::new("channel_user_read_state"), Alias::new("user_id"))
Alias::new("channel_user_read_state"),
Alias::new("user_id"),
)
.to(Alias::new("user"), Alias::new("id")) .to(Alias::new("user"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade), .on_delete(ForeignKeyAction::Cascade),
) )
@@ -644,6 +641,111 @@ impl MigrationTrait for Migration {
// Permissions // Permissions
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
manager
.create_table(
Table::create()
.table(Alias::new("emoji"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Alias::new("server_id")).uuid().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("file_path")).text().null())
.col(ColumnDef::new(Alias::new("mime_type")).string().null())
.col(ColumnDef::new(Alias::new("file_size")).big_integer().null())
.col(
ColumnDef::new(Alias::new("is_animated"))
.boolean()
.not_null()
.default(false),
)
.col(ColumnDef::new(Alias::new("sha256")).string().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_server")
.from(Alias::new("emoji"), Alias::new("server_id"))
.to(Alias::new("server"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_emoji_server_id")
.table(Alias::new("emoji"))
.col(Alias::new("server_id"))
.to_owned(),
)
.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?;
manager manager
.create_table( .create_table(
Table::create() Table::create()
@@ -908,6 +1010,8 @@ 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",
"channel_user_read_state", "channel_user_read_state",
"channel_user_permission", "channel_user_permission",
"channel_role_permission", "channel_role_permission",
+2 -2
View File
@@ -1,6 +1,6 @@
use argon2::{ use argon2::{
password_hash::{phc::PasswordHash, PasswordHasher, PasswordVerifier}, Algorithm, Argon2, Params, Algorithm, Argon2, Params, Version,
Version, password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash},
}; };
/// Hache un password avec Argon2id /// Hache un password avec Argon2id
+1 -1
View File
@@ -1,4 +1,4 @@
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid; use uuid::Uuid;
+23
View File
@@ -60,6 +60,9 @@ url = "sqlite://oxspeak.db"
# MySQL # MySQL
# url = "mysql://user:passwd@localhost:3306/db_name" # url = "mysql://user:passwd@localhost:3306/db_name"
[media]
root = "media"
[jwt] [jwt]
secret = "changeme" secret = "changeme"
# Duration in seconds # Duration in seconds
@@ -72,6 +75,26 @@ pub struct AppConfig {
pub network: NetworkConfig, pub network: NetworkConfig,
pub database: DatabaseConfig, pub database: DatabaseConfig,
pub jwt: JwtConfig, pub jwt: JwtConfig,
#[serde(default)]
pub media: MediaConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MediaConfig {
#[serde(default = "default_media_root")]
pub root: String,
}
impl Default for MediaConfig {
fn default() -> Self {
Self {
root: default_media_root(),
}
}
}
fn default_media_root() -> String {
"media".to_string()
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
+3 -1
View File
@@ -69,7 +69,9 @@ impl App {
let services = Arc::new(Services::new(repositories.clone(), event_bus.clone())); let services = Arc::new(Services::new(repositories.clone(), event_bus.clone()));
services.permission_sync.start_listen_event().await; services.permission_sync.start_listen_event().await;
services.realtime_registry.initialize(&repositories).await?; services.realtime_registry.initialize(&repositories).await?;
services.realtime_registry.start_listening(repositories.clone(), event_bus.clone()); services
.realtime_registry
.start_listening(repositories.clone(), event_bus.clone());
let gateway = Arc::new(GatewayManager::new(services.clone())); let gateway = Arc::new(GatewayManager::new(services.clone()));
gateway.start(event_bus.clone()); gateway.start(event_bus.clone());
+42
View File
@@ -0,0 +1,42 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct EmojiQueryParams {
pub server_id: Option<Uuid>,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct CreateEmojiRequest {
pub server_id: Option<Uuid>,
pub emoji_type: String,
pub unicode_sequence: Option<String>,
pub aliases: Vec<String>,
pub mime_type: Option<String>,
pub is_animated: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct UpdateEmojiRequest {
pub aliases: Option<Vec<String>>,
pub server_id: Option<Uuid>,
pub unicode_sequence: Option<String>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct EmojiResponse {
pub id: Uuid,
pub server_id: Option<Uuid>,
pub emoji_type: String,
pub unicode_sequence: Option<String>,
pub aliases: Vec<String>,
pub asset_url: Option<String>,
pub mime_type: Option<String>,
pub file_size: Option<i64>,
pub is_animated: bool,
pub sha256: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
+5 -4
View File
@@ -1,9 +1,10 @@
pub mod auth;
pub mod attachment; pub mod attachment;
pub mod auth;
pub mod category;
pub mod channel;
pub mod core;
pub mod emoji;
pub mod message; pub mod message;
pub mod role; pub mod role;
pub mod server; pub mod server;
pub mod core;
pub mod user; pub mod user;
pub mod channel;
pub mod category;
+1 -1
View File
@@ -1,2 +1,2 @@
pub mod events;
pub mod dto; pub mod dto;
pub mod events;
+1 -1
View File
@@ -1,6 +1,6 @@
use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::Json;
use sea_orm::DbErr; use sea_orm::DbErr;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
+1 -1
View File
@@ -19,8 +19,8 @@
//! | `panics_caught` | Panics interceptés par `CatchPanicLayer` | //! | `panics_caught` | Panics interceptés par `CatchPanicLayer` |
//! | `latency_ms_total` | Latence cumulée en ms (pour moyenne glissante) | //! | `latency_ms_total` | Latence cumulée en ms (pour moyenne glissante) |
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::metrics::{Metrics, MetricsSnapshot}; use crate::metrics::{Metrics, MetricsSnapshot};
+2 -2
View File
@@ -1,14 +1,14 @@
use axum::{ use axum::{
body::{Body, HttpBody}, body::{Body, HttpBody},
extract::State, extract::State,
http::{header, Request}, http::{Request, header},
middleware::Next, middleware::Next,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use tracing::{debug, info, Instrument}; use tracing::{Instrument, debug, info};
use uuid::Uuid; use uuid::Uuid;
use super::context::{CurrentUser, RequestContext}; use super::context::{CurrentUser, RequestContext};
+2 -2
View File
@@ -5,10 +5,10 @@ pub mod context;
pub mod error; pub mod error;
pub mod metrics; pub mod metrics;
pub mod middleware; pub mod middleware;
pub mod permissions;
pub mod server; pub mod server;
pub mod validation; pub mod validation;
pub mod permissions;
pub use permissions::{RequireServerPermission, RequireChannelPermission}; pub use permissions::{RequireChannelPermission, RequireServerPermission};
pub type OxRouter = Router<AppState>; pub type OxRouter = Router<AppState>;
+1 -1
View File
@@ -7,8 +7,8 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use axum::middleware as axum_middleware;
use axum::Router; use axum::Router;
use axum::middleware as axum_middleware;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tower_http::catch_panic::CatchPanicLayer; use tower_http::catch_panic::CatchPanicLayer;
+2 -2
View File
@@ -1,11 +1,11 @@
use axum::{ use axum::{
Json,
extract::{FromRequest, Request}, extract::{FromRequest, Request},
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json,
}; };
use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::HashMap; use std::collections::HashMap;
use validator::Validate; use validator::Validate;
+1 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+1 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+34
View File
@@ -0,0 +1,34 @@
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")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub server_id: Option<Uuid>,
pub emoji_type: String,
pub unicode_sequence: Option<String>,
pub file_path: Option<String>,
pub mime_type: Option<String>,
pub file_size: Option<i64>,
pub is_animated: bool,
pub sha256: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(has_many)]
pub aliases: HasMany<super::emoji_alias::Entity>,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
is_animated: Set(false),
..Default::default()
}
}
}
+27
View File
@@ -0,0 +1,27 @@
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()
}
}
}
+1 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+3 -1
View File
@@ -7,9 +7,11 @@ pub mod category;
pub mod channel; pub mod channel;
pub mod channel_role_permission; pub mod channel_role_permission;
pub mod channel_user; pub mod channel_user;
pub mod channel_user_read_state;
pub mod channel_user_permission; pub mod channel_user_permission;
pub mod channel_user_read_state;
pub mod computed_permission; pub mod computed_permission;
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;
+2
View File
@@ -6,6 +6,8 @@ pub use super::channel::Entity as Channel;
pub use super::channel_user::Entity as ChannelUser; 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_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 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity. //! `SeaORM` Entity.
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+1 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+1 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+1 -1
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[sea_orm(model_ex_attrs(derive(Debug)))] #[sea_orm(model_ex_attrs(derive(Debug)))]
+100
View File
@@ -0,0 +1,100 @@
use crate::models::{emoji, emoji_alias};
use crate::repositories::{AnyResult, RepositoryContext};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, ExprTrait, QueryFilter, QueryOrder, Set,
};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct EmojiRepository {
pub context: Arc<RepositoryContext>,
}
impl EmojiRepository {
pub async fn get_by_id(&self, id: Uuid) -> AnyResult<Option<emoji::Model>> {
Ok(emoji::Entity::find_by_id(id).one(&self.context.db).await?)
}
pub async fn list(&self, server_id: Option<Uuid>) -> AnyResult<Vec<emoji::Model>> {
let mut query = emoji::Entity::find();
if let Some(server_id) = server_id {
query = query.filter(
emoji::Column::ServerId
.is_null()
.or(emoji::Column::ServerId.eq(server_id)),
);
} else {
query = query.filter(emoji::Column::ServerId.is_null());
}
Ok(query
.order_by_asc(emoji::Column::CreatedAt)
.all(&self.context.db)
.await?)
}
pub async fn list_exact_scope(&self, server_id: Option<Uuid>) -> AnyResult<Vec<emoji::Model>> {
let mut query = emoji::Entity::find();
query = match server_id {
Some(id) => query.filter(emoji::Column::ServerId.eq(id)),
None => query.filter(emoji::Column::ServerId.is_null()),
};
Ok(query.all(&self.context.db).await?)
}
pub async fn aliases(&self, emoji_id: Uuid) -> AnyResult<Vec<emoji_alias::Model>> {
Ok(emoji_alias::Entity::find()
.filter(emoji_alias::Column::EmojiId.eq(emoji_id))
.all(&self.context.db)
.await?)
}
pub async fn find_alias(
&self,
alias: &str,
server_id: Option<Uuid>,
) -> AnyResult<Option<emoji::Model>> {
let rows = self.list(server_id).await?;
for model in rows {
if self
.aliases(model.id)
.await?
.iter()
.any(|a| a.alias == alias)
{
return Ok(Some(model));
}
}
Ok(None)
}
pub async fn create(&self, model: emoji::ActiveModel) -> AnyResult<emoji::Model> {
Ok(model.insert(&self.context.db).await?)
}
pub async fn update(&self, model: emoji::ActiveModel) -> AnyResult<emoji::Model> {
Ok(model.update(&self.context.db).await?)
}
pub async fn delete(&self, id: Uuid) -> AnyResult<bool> {
Ok(emoji::Entity::delete_by_id(id)
.exec(&self.context.db)
.await?
.rows_affected
> 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(())
}
}
+4
View File
@@ -3,6 +3,7 @@ pub type AnyResult<T> = anyhow::Result<T>;
use crate::repositories::category::CategoryRepository; use crate::repositories::category::CategoryRepository;
use crate::repositories::channel::ChannelRepository; use crate::repositories::channel::ChannelRepository;
use crate::repositories::computed_permission::ComputedPermissionRepository; use crate::repositories::computed_permission::ComputedPermissionRepository;
use crate::repositories::emoji::EmojiRepository;
use crate::repositories::message::MessageRepository; use crate::repositories::message::MessageRepository;
use crate::repositories::read_state::ReadStateRepository; use crate::repositories::read_state::ReadStateRepository;
use crate::repositories::role::RoleRepository; use crate::repositories::role::RoleRepository;
@@ -16,6 +17,7 @@ use std::sync::Arc;
mod category; mod category;
mod channel; mod channel;
mod computed_permission; mod computed_permission;
pub mod emoji;
mod message; mod message;
mod read_state; mod read_state;
mod role; mod role;
@@ -42,6 +44,7 @@ pub struct Repositories {
pub computed_permission: ComputedPermissionRepository, pub computed_permission: ComputedPermissionRepository,
pub server_item_order: ServerItemOrderRepository, pub server_item_order: ServerItemOrderRepository,
pub server_tree: ServerTreeRepository, pub server_tree: ServerTreeRepository,
pub emoji: EmojiRepository,
} }
impl Repositories { impl Repositories {
@@ -79,6 +82,7 @@ impl Repositories {
server_tree: ServerTreeRepository { server_tree: ServerTreeRepository {
context: context.clone(), context: context.clone(),
}, },
emoji: EmojiRepository { context },
} }
} }
} }
+4 -5
View File
@@ -94,10 +94,7 @@ impl ReadStateRepository {
Ok(counts) Ok(counts)
} }
pub async fn unread_counts_by_server( pub async fn unread_counts_by_server(&self, user_id: Uuid) -> AnyResult<HashMap<Uuid, u64>> {
&self,
user_id: Uuid,
) -> AnyResult<HashMap<Uuid, u64>> {
let channels = channel::Entity::find() let channels = channel::Entity::find()
.select_only() .select_only()
.column(channel::Column::Id) .column(channel::Column::Id)
@@ -109,7 +106,9 @@ impl ReadStateRepository {
let channel_to_server: HashMap<Uuid, Uuid> = channels let channel_to_server: HashMap<Uuid, Uuid> = channels
.into_iter() .into_iter()
.filter_map(|(channel_id, server_id)| server_id.map(|server_id| (channel_id, server_id))) .filter_map(|(channel_id, server_id)| {
server_id.map(|server_id| (channel_id, server_id))
})
.collect(); .collect();
if channel_to_server.is_empty() { if channel_to_server.is_empty() {
return Ok(HashMap::new()); return Ok(HashMap::new());
+6 -3
View File
@@ -1,6 +1,5 @@
use super::{AnyResult, RepositoryContext}; use super::{AnyResult, RepositoryContext};
use crate::models::{role, server, server_role_permission, server_user, server_user_permission, use crate::models::{role, server, server_role_permission, server_user, server_user_permission};
};
use sea_orm::prelude::*; use sea_orm::prelude::*;
use sea_orm::{ActiveModelTrait, QuerySelect, Set}; use sea_orm::{ActiveModelTrait, QuerySelect, Set};
@@ -84,7 +83,11 @@ impl ServerRepository {
Ok(true) Ok(true)
} }
pub async fn get_user(&self, server_id: Uuid, user_id: Uuid) -> AnyResult<Option<server_user::Model>> { pub async fn get_user(
&self,
server_id: Uuid,
user_id: Uuid,
) -> AnyResult<Option<server_user::Model>> {
Ok(server_user::Entity::find() Ok(server_user::Entity::find()
.filter(server_user::Column::ServerId.eq(server_id)) .filter(server_user::Column::ServerId.eq(server_id))
.filter(server_user::Column::UserId.eq(user_id)) .filter(server_user::Column::UserId.eq(user_id))
+3 -1
View File
@@ -1,4 +1,6 @@
use crate::models::{category, channel, channel_user_read_state, computed_permission, message, server_item_order}; use crate::models::{
category, channel, channel_user_read_state, computed_permission, message, server_item_order,
};
use crate::permissions::ChannelPermission; use crate::permissions::ChannelPermission;
use crate::repositories::types::{CategoryWithPermissions, ChannelWithPermissions, ServerTreeData}; use crate::repositories::types::{CategoryWithPermissions, ChannelWithPermissions, ServerTreeData};
use crate::repositories::{AnyResult, RepositoryContext}; use crate::repositories::{AnyResult, RepositoryContext};
+1 -1
View File
@@ -1,4 +1,4 @@
use axum::{routing::get, Router}; use axum::{Router, routing::get};
use super::handlers; use super::handlers;
+3 -3
View File
@@ -1,13 +1,13 @@
use crate::domain::dto::auth::{LoginRequest, LoginResponse, MeResponse};
use crate::auth::token::create_jwt; use crate::auth::token::create_jwt;
use crate::core::AppState; use crate::core::AppState;
use crate::domain::dto::auth::{LoginRequest, LoginResponse, MeResponse};
use crate::http::context::CurrentUser; use crate::http::context::CurrentUser;
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
use crate::routes::user::mapper::user_model_to_user_response; use crate::routes::user::mapper::user_model_to_user_response;
use axum::extract::State;
use axum::Json; use axum::Json;
use axum_extra::extract::cookie::{Cookie, SameSite}; use axum::extract::State;
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use sea_orm::ActiveModelBehavior; use sea_orm::ActiveModelBehavior;
#[utoipa::path( #[utoipa::path(
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::http::OxRouter; use crate::http::OxRouter;
use crate::routes::auth::handlers; use crate::routes::auth::handlers;
use axum::routing::{get, post};
use axum::Router; use axum::Router;
use axum::routing::{get, post};
pub fn router() -> OxRouter { pub fn router() -> OxRouter {
Router::new() Router::new()
+18 -6
View File
@@ -1,14 +1,14 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use crate::http::context::Superuser;
use crate::http::error::HTTPError;
use crate::domain::dto::category::{ use crate::domain::dto::category::{
CategoryQueryParams, CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest, CategoryQueryParams, CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest,
}; };
use crate::http::context::Superuser;
use crate::http::error::HTTPError;
use crate::routes::category::mapper; use crate::routes::category::mapper;
use axum::{ use axum::{
Json,
extract::{Path, Query, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
Json,
}; };
use uuid::Uuid; use uuid::Uuid;
@@ -29,7 +29,11 @@ pub async fn get_all(
State(state): State<AppState>, State(state): State<AppState>,
Query(filters): Query<CategoryQueryParams>, Query(filters): Query<CategoryQueryParams>,
) -> Result<Json<Vec<CategoryResponse>>, HTTPError> { ) -> Result<Json<Vec<CategoryResponse>>, HTTPError> {
let categories = state.repositories.category.filter(filters.server_id).await?; let categories = state
.repositories
.category
.filter(filters.server_id)
.await?;
Ok(Json( Ok(Json(
categories categories
.into_iter() .into_iter()
@@ -94,7 +98,11 @@ pub async fn create(
.await? .await?
.ok_or(HTTPError::BadRequest("Server not found".to_string()))?; .ok_or(HTTPError::BadRequest("Server not found".to_string()))?;
let category = state.services.category.create_category(payload.server_id, payload.name).await?; let category = state
.services
.category
.create_category(payload.server_id, payload.name)
.await?;
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
Json(mapper::category_model_to_category_response(category)), Json(mapper::category_model_to_category_response(category)),
@@ -133,7 +141,11 @@ pub async fn update(
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
let category = state.services.category.update_category(id, payload.name).await?; let category = state
.services
.category
.update_category(id, payload.name)
.await?;
Ok(Json(mapper::category_model_to_category_response(category))) Ok(Json(mapper::category_model_to_category_response(category)))
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use axum::{routing::get, Router}; use axum::{Router, routing::get};
use super::handlers; use super::handlers;
+31 -11
View File
@@ -1,12 +1,11 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use crate::domain::dto::channel::{
ChannelPermissionsResponse, ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse,
ChannelUserPermissionResponse, CreateChannelRequest, ReadStateResponse,
SetChannelPermissionRequest, SetReadStateRequest, UpdateChannelRequest,
};
use crate::http::context::{CurrentUser, Superuser}; use crate::http::context::{CurrentUser, Superuser};
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
use crate::domain::dto::channel::{
ChannelQueryParams, ChannelResponse, ChannelPermissionsResponse, ChannelRolePermissionResponse,
ChannelUserPermissionResponse, CreateChannelRequest, ReadStateResponse,
SetChannelPermissionRequest, SetReadStateRequest,
UpdateChannelRequest,
};
use crate::routes::channel::mapper; use crate::routes::channel::mapper;
use axum::{ use axum::{
Json, Json,
@@ -55,8 +54,17 @@ pub async fn get_read_state(
State(state): State<AppState>, State(state): State<AppState>,
Path(channel_id): Path<Uuid>, Path(channel_id): Path<Uuid>,
) -> Result<Json<ReadStateResponse>, HTTPError> { ) -> Result<Json<ReadStateResponse>, HTTPError> {
state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?; state
let read_state = state.repositories.read_state.get(channel_id, user.id).await?; .repositories
.channel
.get_by_id(channel_id)
.await?
.ok_or(HTTPError::NotFound)?;
let read_state = state
.repositories
.read_state
.get(channel_id, user.id)
.await?;
let unread_count = state let unread_count = state
.repositories .repositories
.read_state .read_state
@@ -68,7 +76,9 @@ pub async fn get_read_state(
Ok(Json(ReadStateResponse { Ok(Json(ReadStateResponse {
channel_id, channel_id,
last_read_message_id: read_state.as_ref().and_then(|value| value.last_read_message_id), last_read_message_id: read_state
.as_ref()
.and_then(|value| value.last_read_message_id),
updated_at: read_state.map(|value| value.updated_at), updated_at: read_state.map(|value| value.updated_at),
unread_count, unread_count,
})) }))
@@ -89,7 +99,12 @@ pub async fn set_read_state(
Path(channel_id): Path<Uuid>, Path(channel_id): Path<Uuid>,
Json(payload): Json<SetReadStateRequest>, Json(payload): Json<SetReadStateRequest>,
) -> Result<Json<ReadStateResponse>, HTTPError> { ) -> Result<Json<ReadStateResponse>, HTTPError> {
state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?; state
.repositories
.channel
.get_by_id(channel_id)
.await?
.ok_or(HTTPError::NotFound)?;
if let Some(message_id) = payload.last_read_message_id { if let Some(message_id) = payload.last_read_message_id {
let message = state let message = state
@@ -167,7 +182,12 @@ pub async fn list_permissions(
State(state): State<AppState>, State(state): State<AppState>,
Path(channel_id): Path<Uuid>, Path(channel_id): Path<Uuid>,
) -> Result<Json<ChannelPermissionsResponse>, HTTPError> { ) -> Result<Json<ChannelPermissionsResponse>, HTTPError> {
state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?; state
.repositories
.channel
.get_by_id(channel_id)
.await?
.ok_or(HTTPError::NotFound)?;
let (users, roles) = tokio::try_join!( let (users, roles) = tokio::try_join!(
state.repositories.channel.list_user_permissions(channel_id), state.repositories.channel.list_user_permissions(channel_id),
state.repositories.channel.list_role_permissions(channel_id), state.repositories.channel.list_role_permissions(channel_id),
+8 -2
View File
@@ -77,8 +77,14 @@ pub fn channel_permissions_to_response(
roles: Vec<channel_role_permission::Model>, roles: Vec<channel_role_permission::Model>,
) -> ChannelPermissionsResponse { ) -> ChannelPermissionsResponse {
ChannelPermissionsResponse { ChannelPermissionsResponse {
users: users.into_iter().map(channel_user_permission_to_response).collect(), users: users
roles: roles.into_iter().map(channel_role_permission_to_response).collect(), .into_iter()
.map(channel_user_permission_to_response)
.collect(),
roles: roles
.into_iter()
.map(channel_role_permission_to_response)
.collect(),
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use super::handlers; use super::handlers;
use crate::core::state::AppState; use crate::core::state::AppState;
use axum::{routing::get, Router}; use axum::{Router, routing::get};
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::core::AppState; use crate::core::AppState;
use crate::domain::dto::core::JoinRequest;
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
use crate::http::validation::ValidatedJson; use crate::http::validation::ValidatedJson;
use crate::domain::dto::core::JoinRequest;
use crate::routes::core::mapper::join_request_to_user_am; use crate::routes::core::mapper::join_request_to_user_am;
use axum::extract::State; use axum::extract::State;
use axum::http::StatusCode; use axum::http::StatusCode;
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::auth::password::hash_password; use crate::auth::password::hash_password;
use crate::models::user;
use crate::domain::dto::core::JoinRequest; use crate::domain::dto::core::JoinRequest;
use crate::models::user;
use anyhow::Result as AnyResult; use anyhow::Result as AnyResult;
use sea_orm::Set; use sea_orm::Set;
use uuid::Uuid; use uuid::Uuid;
+1 -1
View File
@@ -1,6 +1,6 @@
use super::handlers; use super::handlers;
use crate::http::OxRouter; use crate::http::OxRouter;
use axum::{routing::post, Router}; use axum::{Router, routing::post};
pub fn router() -> OxRouter { pub fn router() -> OxRouter {
Router::new().route("/join", post(handlers::join)) Router::new().route("/join", post(handlers::join))
+274
View File
@@ -0,0 +1,274 @@
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::{EntityTrait, 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? {
let aliases = state.repositories.emoji.aliases(emoji.id).await?;
result.push(mapper::response(emoji, mapper::aliases(aliases)));
}
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)?;
let aliases = state.repositories.emoji.aliases(id).await?;
Ok(Json(mapper::response(emoji, mapper::aliases(aliases))))
}
#[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 aliases = Vec::new();
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 name = field.name().unwrap_or_default().to_string();
if 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 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),
"alias" => aliases.push(value),
"aliases" => aliases.extend(value.split(',').map(str::to_string)),
"is_animated" => animated = value.parse().unwrap_or(false),
_ => {}
}
}
let emoji_type = normalize_type(emoji_type.as_deref().unwrap_or("custom"))?;
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),
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, aliases).await?;
let aliases = state.repositories.emoji.aliases(created.id).await?;
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")]
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 aliases_to_validate = match &payload.aliases {
Some(aliases) => aliases.clone(),
None => state
.repositories
.emoji
.aliases(id)
.await?
.into_iter()
.map(|a| a.alias)
.collect(),
};
state
.services
.emoji
.aliases_available(&aliases_to_validate, 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);
}
let updated = state.repositories.emoji.update(active).await?;
if let Some(ref aliases) = payload.aliases {
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?;
Ok(Json(mapper::response(updated, mapper::aliases(aliases))))
}
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)
}
+25
View File
@@ -0,0 +1,25 @@
use crate::{domain::dto::emoji::EmojiResponse, models::emoji};
pub fn response(model: emoji::Model, aliases: Vec<String>) -> EmojiResponse {
EmojiResponse {
id: model.id,
server_id: model.server_id,
emoji_type: model.emoji_type,
unicode_sequence: model.unicode_sequence,
aliases,
asset_url: model
.file_path
.as_ref()
.map(|_| format!("/api/emojis/{}/asset", model.id)),
mime_type: model.mime_type,
file_size: model.file_size,
is_animated: model.is_animated,
sha256: model.sha256,
created_at: model.created_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()
}
+3
View File
@@ -0,0 +1,3 @@
pub mod handlers;
pub mod mapper;
pub mod routes;
+15
View File
@@ -0,0 +1,15 @@
use super::handlers;
use crate::core::state::AppState;
use axum::{Router, routing::get};
pub fn router() -> Router<AppState> {
Router::new()
.route("/emojis", get(handlers::get_all).post(handlers::create))
.route(
"/emojis/{id}",
get(handlers::get_by_id)
.put(handlers::update)
.delete(handlers::delete),
)
.route("/emojis/{id}/asset", get(handlers::asset))
}
+9 -4
View File
@@ -6,6 +6,7 @@ use crate::routes::category::mapper::category_model_to_category_response;
use crate::routes::channel::mapper::channel_model_to_channel_response; use crate::routes::channel::mapper::channel_model_to_channel_response;
use crate::routes::message::mapper::message_model_to_message_response_with_server_id; use crate::routes::message::mapper::message_model_to_message_response_with_server_id;
use crate::routes::server::mapper::server_model_to_server_response; use crate::routes::server::mapper::server_model_to_server_response;
use crate::services::Services;
use axum::extract::ws::Message; use axum::extract::ws::Message;
use event_bus::EventBus; use event_bus::EventBus;
use events::GatewayEvent; use events::GatewayEvent;
@@ -15,7 +16,6 @@ use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use uuid::Uuid; use uuid::Uuid;
use crate::services::Services;
pub mod events; pub mod events;
pub mod handlers; pub mod handlers;
@@ -44,7 +44,10 @@ pub struct GatewayClient {
impl GatewayManager { impl GatewayManager {
pub fn new(services: Arc<Services>) -> Self { pub fn new(services: Arc<Services>) -> Self {
Self { clients: RwLock::new(HashMap::new()), services } Self {
clients: RwLock::new(HashMap::new()),
services,
}
} }
/// Démarre les routeurs centraux des événements de messages. /// Démarre les routeurs centraux des événements de messages.
pub fn start(self: &Arc<Self>, event_bus: Arc<EventBus>) { pub fn start(self: &Arc<Self>, event_bus: Arc<EventBus>) {
@@ -95,7 +98,6 @@ impl GatewayManager {
pub(crate) fn remove_client(&self, gateway_client: &GatewayClient) { pub(crate) fn remove_client(&self, gateway_client: &GatewayClient) {
let key = gateway_client.key(); let key = gateway_client.key();
self.clients.write().remove(&key); self.clients.write().remove(&key);
} }
fn broadcast_message<T: serde::Serialize>( fn broadcast_message<T: serde::Serialize>(
@@ -113,7 +115,10 @@ impl GatewayManager {
return; return;
}; };
let users = self.services.realtime_registry.users_for_channel(channel_id); let users = self
.services
.realtime_registry
.users_for_channel(channel_id);
let clients = self.clients.read(); let clients = self.clients.read();
for (key, client) in clients.iter() { for (key, client) in clients.iter() {
if users.contains(&key.user_id) { if users.contains(&key.user_id) {
+1 -1
View File
@@ -1,7 +1,7 @@
use super::handlers; use super::handlers;
use crate::core::AppState; use crate::core::AppState;
use axum::routing::get;
use axum::Router; use axum::Router;
use axum::routing::get;
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new().route("/gateway", get(handlers::ws_handler)) Router::new().route("/gateway", get(handlers::ws_handler))
+1 -1
View File
@@ -1,6 +1,6 @@
use super::handlers; use super::handlers;
use crate::core::state::AppState; use crate::core::state::AppState;
use axum::{routing::get, Router}; use axum::{Router, routing::get};
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
+2
View File
@@ -9,6 +9,7 @@ pub mod auth;
pub mod category; pub mod category;
pub mod channel; pub mod channel;
pub mod core; pub mod core;
pub mod emoji;
pub mod gateway; pub mod gateway;
pub mod message; pub mod message;
pub mod openapi; pub mod openapi;
@@ -25,6 +26,7 @@ pub fn router() -> OxRouter {
.merge(role::routes::router()) .merge(role::routes::router())
.merge(message::routes::router()) .merge(message::routes::router())
.merge(user::routes::router()) .merge(user::routes::router())
.merge(emoji::routes::router())
.layer(axum_middleware::from_fn(middleware::require_auth)); .layer(axum_middleware::from_fn(middleware::require_auth));
// Routes publiques (ou gérant leur propre auth) // Routes publiques (ou gérant leur propre auth)
+10
View File
@@ -42,6 +42,11 @@ use utoipa::{Modify, OpenApi};
message::handlers::update, message::handlers::update,
message::handlers::delete, message::handlers::delete,
core::handlers::join, core::handlers::join,
emoji::handlers::get_all,
emoji::handlers::get_by_id,
emoji::handlers::create,
emoji::handlers::update,
emoji::handlers::delete,
), ),
components( components(
schemas( schemas(
@@ -70,6 +75,10 @@ use utoipa::{Modify, OpenApi};
crate::domain::dto::message::UpdateMessageRequest, crate::domain::dto::message::UpdateMessageRequest,
crate::domain::dto::core::JoinRequest, crate::domain::dto::core::JoinRequest,
ChannelType, ChannelType,
crate::domain::dto::emoji::EmojiResponse,
crate::domain::dto::emoji::CreateEmojiRequest,
crate::domain::dto::emoji::UpdateEmojiRequest,
crate::domain::dto::emoji::EmojiQueryParams,
) )
), ),
modifiers(&SecurityAddon, &ApiPrefixAddon), modifiers(&SecurityAddon, &ApiPrefixAddon),
@@ -82,6 +91,7 @@ use utoipa::{Modify, OpenApi};
(name = "roles", description = "Gestion des rolees"), (name = "roles", description = "Gestion des rolees"),
(name = "Messages", description = "Gestion des messages"), (name = "Messages", description = "Gestion des messages"),
(name = "Core", description = "Endpoints de base (enregistrement, etc.)"), (name = "Core", description = "Endpoints de base (enregistrement, etc.)"),
(name = "Emojis", description = "Gestion des emojis Unicode et personnalisés"),
) )
)] )]
pub struct ApiDoc; pub struct ApiDoc;
+149 -25
View File
@@ -1,12 +1,18 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use crate::domain::dto::role::{CreateRoleRequest, RoleQueryParams, RoleResponse, UpdateRoleRequest}; use crate::domain::dto::role::{
CreateRoleRequest, RoleQueryParams, RoleResponse, UpdateRoleRequest,
};
use crate::domain::dto::user::UserResponse; use crate::domain::dto::user::UserResponse;
use crate::http::context::CurrentUser; use crate::http::context::CurrentUser;
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
use crate::permissions::ServerPermission; use crate::permissions::ServerPermission;
use crate::routes::role::mapper; use crate::routes::role::mapper;
use crate::routes::user::mapper as user_mapper; use crate::routes::user::mapper as user_mapper;
use axum::{Json, extract::{Path, Query, State}, http::StatusCode}; use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use uuid::Uuid; use uuid::Uuid;
async fn require_permission( async fn require_permission(
@@ -44,7 +50,12 @@ pub async fn get_all(
None => state.repositories.role.get_all().await?, None => state.repositories.role.get_all().await?,
}; };
Ok(Json(roles.into_iter().map(mapper::role_model_to_role_response).collect())) Ok(Json(
roles
.into_iter()
.map(mapper::role_model_to_role_response)
.collect(),
))
} }
#[utoipa::path(get, path = "/roles/{id}", params(("id" = Uuid, Path)), responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles")] #[utoipa::path(get, path = "/roles/{id}", params(("id" = Uuid, Path)), responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles")]
@@ -52,7 +63,12 @@ pub async fn get_by_id(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Json<RoleResponse>, HTTPError> { ) -> Result<Json<RoleResponse>, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?; let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
Ok(Json(mapper::role_model_to_role_response(role))) Ok(Json(mapper::role_model_to_role_response(role)))
} }
@@ -62,12 +78,29 @@ pub async fn create(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<CreateRoleRequest>, Json(payload): Json<CreateRoleRequest>,
) -> Result<(StatusCode, Json<RoleResponse>), HTTPError> { ) -> Result<(StatusCode, Json<RoleResponse>), HTTPError> {
state.repositories.server.get_by_id(payload.server_id).await? state
.repositories
.server
.get_by_id(payload.server_id)
.await?
.ok_or(HTTPError::BadRequest("Server not found".to_string()))?; .ok_or(HTTPError::BadRequest("Server not found".to_string()))?;
require_permission(&state, &user, payload.server_id, ServerPermission::MANAGE_ROLES).await?; require_permission(
&state,
&user,
payload.server_id,
ServerPermission::MANAGE_ROLES,
)
.await?;
let role = state.services.role.create_role(mapper::create_request_to_am(payload)).await?; let role = state
Ok((StatusCode::CREATED, Json(mapper::role_model_to_role_response(role)))) .services
.role
.create_role(mapper::create_request_to_am(payload))
.await?;
Ok((
StatusCode::CREATED,
Json(mapper::role_model_to_role_response(role)),
))
} }
#[utoipa::path(put, path = "/roles/{id}", request_body = UpdateRoleRequest, responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles", security(("bearerAuth" = [])))] #[utoipa::path(put, path = "/roles/{id}", request_body = UpdateRoleRequest, responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles", security(("bearerAuth" = [])))]
@@ -77,10 +110,29 @@ pub async fn update(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(payload): Json<UpdateRoleRequest>, Json(payload): Json<UpdateRoleRequest>,
) -> Result<Json<RoleResponse>, HTTPError> { ) -> Result<Json<RoleResponse>, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?; let role = state
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_ROLES).await?; .repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_ROLES,
)
.await?;
let role = state.services.role.update_role(mapper::update_request_to_am(role.id, role.server_id, payload)).await?; let role = state
.services
.role
.update_role(mapper::update_request_to_am(
role.id,
role.server_id,
payload,
))
.await?;
Ok(Json(mapper::role_model_to_role_response(role))) Ok(Json(mapper::role_model_to_role_response(role)))
} }
@@ -90,10 +142,25 @@ pub async fn delete(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?; let role = state
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_ROLES).await?; .repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_ROLES,
)
.await?;
if state.services.role.delete_role(id).await? { Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) } if state.services.role.delete_role(id).await? {
Ok(StatusCode::NO_CONTENT)
} else {
Err(HTTPError::NotFound)
}
} }
pub async fn get_members( pub async fn get_members(
@@ -101,10 +168,26 @@ pub async fn get_members(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Json<Vec<UserResponse>>, HTTPError> { ) -> Result<Json<Vec<UserResponse>>, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?; let role = state
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?; .repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_MEMBERS,
)
.await?;
let members = state.repositories.role.get_members(id).await?; let members = state.repositories.role.get_members(id).await?;
Ok(Json(members.into_iter().map(user_mapper::user_model_to_user_response).collect())) Ok(Json(
members
.into_iter()
.map(user_mapper::user_model_to_user_response)
.collect(),
))
} }
pub async fn add_member( pub async fn add_member(
@@ -112,11 +195,32 @@ pub async fn add_member(
State(state): State<AppState>, State(state): State<AppState>,
Path((id, user_id)): Path<(Uuid, Uuid)>, Path((id, user_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?; let role = state
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?; .repositories
state.repositories.server.get_user(role.server_id, user_id).await? .role
.ok_or(HTTPError::BadRequest("User is not a member of this server".to_string()))?; .get_by_id(id)
state.services.role.add_member(id, user_id, role.server_id).await?; .await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_MEMBERS,
)
.await?;
state
.repositories
.server
.get_user(role.server_id, user_id)
.await?
.ok_or(HTTPError::BadRequest(
"User is not a member of this server".to_string(),
))?;
state
.services
.role
.add_member(id, user_id, role.server_id)
.await?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@@ -125,7 +229,27 @@ pub async fn remove_member(
State(state): State<AppState>, State(state): State<AppState>,
Path((id, user_id)): Path<(Uuid, Uuid)>, Path((id, user_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?; let role = state
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?; .repositories
if state.services.role.remove_member(id, user_id, role.server_id).await? { Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) } .role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_MEMBERS,
)
.await?;
if state
.services
.role
.remove_member(id, user_id, role.server_id)
.await?
{
Ok(StatusCode::NO_CONTENT)
} else {
Err(HTTPError::NotFound)
}
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::models::role;
use crate::domain::dto::role::{CreateRoleRequest, RoleResponse, UpdateRoleRequest}; use crate::domain::dto::role::{CreateRoleRequest, RoleResponse, UpdateRoleRequest};
use crate::models::role;
use sea_orm::Set; use sea_orm::Set;
use uuid::Uuid; use uuid::Uuid;
+8 -2
View File
@@ -1,5 +1,8 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use axum::{routing::{get, put}, Router}; use axum::{
Router,
routing::{get, put},
};
use super::handlers; use super::handlers;
@@ -13,5 +16,8 @@ pub fn router() -> Router<AppState> {
.delete(handlers::delete), .delete(handlers::delete),
) )
.route("/roles/{id}/members", get(handlers::get_members)) .route("/roles/{id}/members", get(handlers::get_members))
.route("/roles/{id}/members/{user_id}", put(handlers::add_member).delete(handlers::remove_member)) .route(
"/roles/{id}/members/{user_id}",
put(handlers::add_member).delete(handlers::remove_member),
)
} }
+30 -6
View File
@@ -32,7 +32,11 @@ async fn require_server_permission(
.map(|value| ServerPermission::from_bits_truncate(value.permissions as u64)) .map(|value| ServerPermission::from_bits_truncate(value.permissions as u64))
.unwrap_or_default(); .unwrap_or_default();
if granted.contains(permission) { Ok(()) } else { Err(HTTPError::Forbidden) } if granted.contains(permission) {
Ok(())
} else {
Err(HTTPError::Forbidden)
}
} }
/// Liste tous les serveurs /// Liste tous les serveurs
@@ -241,9 +245,23 @@ pub async fn list_user_permissions(
State(state): State<AppState>, State(state): State<AppState>,
Path(server_id): Path<Uuid>, Path(server_id): Path<Uuid>,
) -> Result<Json<Vec<ServerUserPermissionResponse>>, HTTPError> { ) -> Result<Json<Vec<ServerUserPermissionResponse>>, HTTPError> {
state.repositories.server.get_by_id(server_id).await?.ok_or(HTTPError::NotFound)?; state
let permissions = state.repositories.server.get_user_permissions(server_id).await?; .repositories
Ok(Json(permissions.into_iter().map(mapper::server_user_permission_to_response).collect())) .server
.get_by_id(server_id)
.await?
.ok_or(HTTPError::NotFound)?;
let permissions = state
.repositories
.server
.get_user_permissions(server_id)
.await?;
Ok(Json(
permissions
.into_iter()
.map(mapper::server_user_permission_to_response)
.collect(),
))
} }
/// Définit ou remplace les permissions directes d'un utilisateur sur un serveur. /// Définit ou remplace les permissions directes d'un utilisateur sur un serveur.
@@ -268,8 +286,14 @@ pub async fn set_user_permission(
Json(payload): Json<SetServerPermissionRequest>, Json(payload): Json<SetServerPermissionRequest>,
) -> Result<Json<ServerUserPermissionResponse>, HTTPError> { ) -> Result<Json<ServerUserPermissionResponse>, HTTPError> {
require_server_permission(&state, &user, server_id, ServerPermission::MANAGE_MEMBERS).await?; require_server_permission(&state, &user, server_id, ServerPermission::MANAGE_MEMBERS).await?;
state.repositories.server.get_user(server_id, user_id).await? state
.ok_or(HTTPError::BadRequest("User is not a member of this server".to_string()))?; .repositories
.server
.get_user(server_id, user_id)
.await?
.ok_or(HTTPError::BadRequest(
"User is not a member of this server".to_string(),
))?;
state state
.repositories .repositories
.server .server
+4 -1
View File
@@ -130,7 +130,10 @@ pub fn build_server_tree(
.into_iter() .into_iter()
.map(|c| { .map(|c| {
let chan_perm_bits = c.permissions.map(|p| p.bits()).unwrap_or(0); let chan_perm_bits = c.permissions.map(|p| p.bits()).unwrap_or(0);
let mut response = channel_model_to_channel_response_with_permission(c.channel, Some(chan_perm_bits)); let mut response = channel_model_to_channel_response_with_permission(
c.channel,
Some(chan_perm_bits),
);
response.unread_count = Some(*unread_counts.get(&response.id).unwrap_or(&0)); response.unread_count = Some(*unread_counts.get(&response.id).unwrap_or(&0));
response response
}) })
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::auth::password::hash_password; use crate::auth::password::hash_password;
use crate::models::user;
use crate::domain::dto::user::{CreateUserRequest, UpdateUserRequest, UserResponse}; use crate::domain::dto::user::{CreateUserRequest, UpdateUserRequest, UserResponse};
use crate::models::user;
use anyhow::Result as AnyResult; use anyhow::Result as AnyResult;
use sea_orm::{NotSet, Set}; use sea_orm::{NotSet, Set};
use uuid::Uuid; use uuid::Uuid;
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use axum::{routing::get, Router}; use axum::{Router, routing::get};
use super::handlers; use super::handlers;
+1 -3
View File
@@ -4,9 +4,7 @@ use crate::models::{channel, role};
use crate::permissions::PermissionSet; use crate::permissions::PermissionSet;
use crate::services::ServicesContext; use crate::services::ServicesContext;
use crate::services::permission::PermissionService; use crate::services::permission::PermissionService;
use sea_orm::{ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait,
};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
+150
View File
@@ -0,0 +1,150 @@
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;
}
}
}
+5
View File
@@ -1,6 +1,7 @@
use crate::repositories::Repositories; use crate::repositories::Repositories;
use crate::services::category::CategoryService; use crate::services::category::CategoryService;
use crate::services::channel::ChannelService; use crate::services::channel::ChannelService;
use crate::services::emoji::EmojiService;
use crate::services::message::MessageService; use crate::services::message::MessageService;
use crate::services::permission::PermissionService; use crate::services::permission::PermissionService;
use crate::services::permission_sync::PermissionSyncService; use crate::services::permission_sync::PermissionSyncService;
@@ -13,6 +14,7 @@ use std::sync::{Arc, OnceLock};
pub mod category; pub mod category;
pub mod channel; pub mod channel;
pub mod emoji;
pub mod message; pub mod message;
mod permission; mod permission;
pub mod permission_sync; pub mod permission_sync;
@@ -41,6 +43,7 @@ pub struct Services {
pub user: Arc<UserService>, pub user: Arc<UserService>,
pub role: Arc<RoleService>, pub role: Arc<RoleService>,
pub permission: Arc<PermissionService>, pub permission: Arc<PermissionService>,
pub emoji: Arc<EmojiService>,
} }
impl Services { impl Services {
@@ -60,6 +63,7 @@ impl Services {
let user = Arc::new(UserService::new(service_context.clone())); let user = Arc::new(UserService::new(service_context.clone()));
let role = Arc::new(RoleService::new(service_context.clone())); let role = Arc::new(RoleService::new(service_context.clone()));
let permission = Arc::new(PermissionService::new(service_context.clone())); let permission = Arc::new(PermissionService::new(service_context.clone()));
let emoji = Arc::new(EmojiService::new(service_context.clone()));
let services = Self { let services = Self {
realtime_registry, realtime_registry,
@@ -72,6 +76,7 @@ impl Services {
user, user,
role, role,
permission, permission,
emoji,
}; };
let _ = service_context.services.set(services.clone()); let _ = service_context.services.set(services.clone());
services services
+80 -24
View File
@@ -27,8 +27,14 @@ impl RealtimeRegistry {
{ {
continue; continue;
} }
channel_users.entry(permission.resource_id).or_default().insert(permission.user_id); channel_users
user_channels.entry(permission.user_id).or_default().insert(permission.resource_id); .entry(permission.resource_id)
.or_default()
.insert(permission.user_id);
user_channels
.entry(permission.user_id)
.or_default()
.insert(permission.resource_id);
} }
*self.channel_users.write() = channel_users; *self.channel_users.write() = channel_users;
@@ -37,20 +43,32 @@ impl RealtimeRegistry {
} }
pub fn users_for_channel(&self, channel_id: Uuid) -> HashSet<Uuid> { pub fn users_for_channel(&self, channel_id: Uuid) -> HashSet<Uuid> {
self.channel_users.read().get(&channel_id).cloned().unwrap_or_default() self.channel_users
.read()
.get(&channel_id)
.cloned()
.unwrap_or_default()
} }
pub fn set_user_channels(&self, user_id: Uuid, channels: impl IntoIterator<Item = Uuid>) { pub fn set_user_channels(&self, user_id: Uuid, channels: impl IntoIterator<Item = Uuid>) {
let channels: HashSet<_> = channels.into_iter().collect(); let channels: HashSet<_> = channels.into_iter().collect();
let old = self.user_channels.write().insert(user_id, channels.clone()).unwrap_or_default(); let old = self
.user_channels
.write()
.insert(user_id, channels.clone())
.unwrap_or_default();
let mut by_channel = self.channel_users.write(); let mut by_channel = self.channel_users.write();
for channel_id in old.difference(&channels) { for channel_id in old.difference(&channels) {
if let Some(users) = by_channel.get_mut(channel_id) { if let Some(users) = by_channel.get_mut(channel_id) {
users.remove(&user_id); users.remove(&user_id);
if users.is_empty() { by_channel.remove(channel_id); } if users.is_empty() {
by_channel.remove(channel_id);
}
} }
} }
for channel_id in channels { by_channel.entry(channel_id).or_default().insert(user_id); } for channel_id in channels {
by_channel.entry(channel_id).or_default().insert(user_id);
}
} }
pub fn remove_user(&self, user_id: Uuid) { pub fn remove_user(&self, user_id: Uuid) {
@@ -59,7 +77,9 @@ impl RealtimeRegistry {
for channel_id in channels { for channel_id in channels {
if let Some(users) = by_channel.get_mut(&channel_id) { if let Some(users) = by_channel.get_mut(&channel_id) {
users.remove(&user_id); users.remove(&user_id);
if users.is_empty() { by_channel.remove(&channel_id); } if users.is_empty() {
by_channel.remove(&channel_id);
}
} }
} }
} }
@@ -71,33 +91,69 @@ impl RealtimeRegistry {
for user_id in users { for user_id in users {
if let Some(channels) = by_user.get_mut(&user_id) { if let Some(channels) = by_user.get_mut(&user_id) {
channels.remove(&channel_id); channels.remove(&channel_id);
if channels.is_empty() { by_user.remove(&user_id); } if channels.is_empty() {
by_user.remove(&user_id);
}
} }
} }
} }
} }
pub fn start_listening(self: &Arc<Self>, repositories: Arc<Repositories>, event_bus: Arc<EventBus>) { pub fn start_listening(
self: &Arc<Self>,
repositories: Arc<Repositories>,
event_bus: Arc<EventBus>,
) {
let registry = Arc::clone(self); let registry = Arc::clone(self);
event_bus.on_async_with("channel_user_permission_updated", repositories.clone(), move |repositories, (_channel_id, user_id, _permissions): (Uuid, Uuid, u64)| { event_bus.on_async_with(
let registry = Arc::clone(&registry); "channel_user_permission_updated",
async move { repositories.clone(),
match repositories.computed_permission.get_all().await { move |repositories, (_channel_id, user_id, _permissions): (Uuid, Uuid, u64)| {
Ok(all) => registry.set_user_channels(user_id, all.into_iter().filter(|p| p.user_id == user_id && p.scope_type == PermissionScopeType::Channel && ChannelPermission::from_bits_retain(p.permissions as u64).contains(ChannelPermission::READ_CHANNEL)).map(|p| p.resource_id)), let registry = Arc::clone(&registry);
Err(error) => tracing::error!(%user_id, ?error, "Unable to refresh realtime registry"), async move {
match repositories.computed_permission.get_all().await {
Ok(all) => registry.set_user_channels(
user_id,
all.into_iter()
.filter(|p| {
p.user_id == user_id
&& p.scope_type == PermissionScopeType::Channel
&& ChannelPermission::from_bits_retain(p.permissions as u64)
.contains(ChannelPermission::READ_CHANNEL)
})
.map(|p| p.resource_id),
),
Err(error) => {
tracing::error!(%user_id, ?error, "Unable to refresh realtime registry")
}
}
} }
} },
}); );
let registry = Arc::clone(self); let registry = Arc::clone(self);
let repositories = repositories.clone(); let repositories = repositories.clone();
event_bus.on_async_with("server_user_permission_updated", repositories, move |repositories, (_server_id, user_id): (Uuid, Uuid)| { event_bus.on_async_with(
let registry = Arc::clone(&registry); "server_user_permission_updated",
async move { repositories,
if let Ok(all) = repositories.computed_permission.get_all().await { move |repositories, (_server_id, user_id): (Uuid, Uuid)| {
registry.set_user_channels(user_id, all.into_iter().filter(|p| p.user_id == user_id && p.scope_type == PermissionScopeType::Channel && ChannelPermission::from_bits_retain(p.permissions as u64).contains(ChannelPermission::READ_CHANNEL)).map(|p| p.resource_id)); let registry = Arc::clone(&registry);
async move {
if let Ok(all) = repositories.computed_permission.get_all().await {
registry.set_user_channels(
user_id,
all.into_iter()
.filter(|p| {
p.user_id == user_id
&& p.scope_type == PermissionScopeType::Channel
&& ChannelPermission::from_bits_retain(p.permissions as u64)
.contains(ChannelPermission::READ_CHANNEL)
})
.map(|p| p.resource_id),
);
}
} }
} },
}); );
} }
} }
+26 -8
View File
@@ -1,5 +1,5 @@
use crate::services::ServicesContext;
use crate::models::role; use crate::models::role;
use crate::services::ServicesContext;
use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait}; use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@@ -56,9 +56,7 @@ impl RoleService {
let txn = db.begin().await?; let txn = db.begin().await?;
let res = role::Entity::delete_by_id(id) let res = role::Entity::delete_by_id(id).exec(&txn).await?;
.exec(&txn)
.await?;
let deleted = res.rows_affected > 0; let deleted = res.rows_affected > 0;
@@ -71,8 +69,18 @@ impl RoleService {
Ok(deleted) Ok(deleted)
} }
pub async fn add_member(&self, role_id: Uuid, user_id: Uuid, server_id: Uuid) -> Result<bool, anyhow::Error> { pub async fn add_member(
let added = self.service_context.repositories.role.add_member(role_id, user_id).await?; &self,
role_id: Uuid,
user_id: Uuid,
server_id: Uuid,
) -> Result<bool, anyhow::Error> {
let added = self
.service_context
.repositories
.role
.add_member(role_id, user_id)
.await?;
if added { if added {
self.service_context self.service_context
.event_bus .event_bus
@@ -81,8 +89,18 @@ impl RoleService {
Ok(added) Ok(added)
} }
pub async fn remove_member(&self, role_id: Uuid, user_id: Uuid, server_id: Uuid) -> Result<bool, anyhow::Error> { pub async fn remove_member(
let removed = self.service_context.repositories.role.remove_member(role_id, user_id).await?; &self,
role_id: Uuid,
user_id: Uuid,
server_id: Uuid,
) -> Result<bool, anyhow::Error> {
let removed = self
.service_context
.repositories
.role
.remove_member(role_id, user_id)
.await?;
if removed { if removed {
self.service_context self.service_context
.event_bus .event_bus
+6 -5
View File
@@ -1,7 +1,10 @@
use crate::models::{role, server};
use crate::repositories::Repositories; use crate::repositories::Repositories;
use crate::services::ServicesContext; use crate::services::ServicesContext;
use crate::models::{role, server}; use sea_orm::{
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, QueryOrder, TransactionTrait, Set}; ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
TransactionTrait,
};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@@ -82,9 +85,7 @@ impl ServerService {
let txn = db.begin().await?; let txn = db.begin().await?;
let res = server::Entity::delete_by_id(id) let res = server::Entity::delete_by_id(id).exec(&txn).await?;
.exec(&txn)
.await?;
let deleted = res.rows_affected > 0; let deleted = res.rows_affected > 0;
+11 -10
View File
@@ -1,7 +1,9 @@
use crate::services::ServicesContext;
use crate::models::{role, user};
use crate::auth::password; use crate::auth::password;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, TransactionTrait, Set}; use crate::models::{role, user};
use crate::services::ServicesContext;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set, TransactionTrait,
};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@@ -65,10 +67,11 @@ impl UserService {
let mut active = user_model.into_active_model(); let mut active = user_model.into_active_model();
let password_to_hash = password_str.clone(); let password_to_hash = password_str.clone();
let hashed = tokio::task::spawn_blocking(move || password::hash_password(&password_to_hash)) let hashed =
.await tokio::task::spawn_blocking(move || password::hash_password(&password_to_hash))
.map_err(|e| anyhow::anyhow!("Join error: {}", e))? .await
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?; .map_err(|e| anyhow::anyhow!("Join error: {}", e))?
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
active.password = Set(hashed); active.password = Set(hashed);
@@ -87,9 +90,7 @@ impl UserService {
let txn = db.begin().await?; let txn = db.begin().await?;
let res = user::Entity::delete_by_id(id) let res = user::Entity::delete_by_id(id).exec(&txn).await?;
.exec(&txn)
.await?;
let deleted = res.rows_affected > 0; let deleted = res.rows_affected > 0;
+1 -1
View File
@@ -23,8 +23,8 @@
//! Chaque métrique est également disponible en taux moyen par seconde via //! Chaque métrique est également disponible en taux moyen par seconde via
//! [`UdpMetricsSnapshot::rates_since`]. //! [`UdpMetricsSnapshot::rates_since`].
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::metrics::{Metrics, MetricsSnapshot}; use crate::metrics::{Metrics, MetricsSnapshot};