This commit is contained in:
2025-12-13 02:15:28 +01:00
commit dbec2e9a74
58 changed files with 6177 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
config.toml
oxspeak.db
target/
# artefact ...
oxspeak - Copie.db:Zone.Identifier

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="oxspeak.db" uuid="6f1740ef-4686-4ce9-b224-e6b0fd312908">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:file:\\wsl$\Debian\home\Nell\linux_dev\unix_oxspeak_server_v2\oxspeak.db?nolock=1</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/unix_oxspeak_server_v2.iml" filepath="$PROJECT_DIR$/.idea/unix_oxspeak_server_v2.iml" />
</modules>
</component>
</project>

12
.idea/unix_oxspeak_server_v2.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/migration/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3626
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

63
Cargo.toml Normal file
View File

@@ -0,0 +1,63 @@
[package]
name = "ox_speak_server"
version = "0.1.0"
edition = "2024"
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "ox_speak_server_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[workspace]
members = [".", "migration"]
[profile.release]
#debug = true
# poid minimal, rapidité baissé
#strip = true # Retire les symboles
#lto = true # Link Time Optimization
#codegen-units = 1 # Meilleure optimisation
#opt-level = "z" # Optimise pour la taille
# poid élevé, vitesse maximal
strip = true # Retire les symboles
lto = "thin" # LTO "léger", bon compromis vitesse/taille
codegen-units = 16 # Parallélise la compilation (défaut), plus rapide à compiler
opt-level = 3 # Optimisation maximale pour la vitesse
#panic = "abort" # Réduit la taille ET améliore légèrement les perfs
[dependencies]
# Async
tokio = {version = "1.48", features = ["full"]}
# HTTP
#actix-web = "4.12"
#poem = "3.1"
#poem-openapi = { version="5.1", features = ["swagger-ui", "url", "chrono"]}
axum = { version = "0.8", features = ["macros", "ws"] }
#utoipa = "5.4"
#utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
#tower = "0.5"
#tower-http = { version = "0.6", features = ["trace", "cors", "timeout"] }
# UDP
socket2 = "0.6"
# db
sea-orm = { version = "2.0.0-rc", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
migration = { path = "migration" }
# logs
log = "0.4.28"
env_logger = "0.11.8"
# utils
chrono = "0.4"
parking_lot = "0.12"
serde = { version = "1.0", features = ["default", "derive"] }
serde_json = { version = "1.0.145", features = ["default"]}
toml = "0.9"
validator = { version = "0.20", features = ["derive"] }
uuid = {version = "1", features = ["v4", "v7", "fast-rng", "serde"]}

41
config.example.toml Normal file
View File

@@ -0,0 +1,41 @@
# OxSpeak Server Configuration
# Copy this file to config.toml and modify the values according to your needs
[server]
# Server listening address + port (TCP+UDP)
bind_addr = "0.0.0.0:7000"
# Mode: "debug" or "release"
mode = "release"
[database]
# Database type: "sqlite", "postgres" or "mysql"
type = "sqlite"
# SQLite configuration (default)
# Path to the database file
path = "./oxspeak.db"
# PostgreSQL configuration (uncomment and configure if type = "postgres")
# host = "localhost"
# port = 5432
# user = "postgres"
# password = "your_password"
# dbname = "oxspeak"
# sslmode = "disable"
# MySQL configuration (uncomment and configure if type = "mysql")
# host = "localhost"
# port = 3306
# user = "root"
# password = "your_password"
# dbname = "oxspeak"
[jwt]
# Secret key for JWT token generation (CHANGE THIS VALUE!)
secret = "your_very_secure_jwt_secret_to_change"
# Token validity duration in hours
expiration = 24
[udp]
# UDP server port (voice)
port = 9000

22
migration/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "2.0.0-rc"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
]

41
migration/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

12
migration/src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View File

@@ -0,0 +1,555 @@
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Create table `server`
manager.create_table(
Table::create()
.table(Alias::new("server"))
.if_not_exists()
.col(
ColumnDef::new("id")
.uuid()
.primary_key()
.not_null()
)
.col(
ColumnDef::new("name")
.string()
.not_null()
)
.col(
ColumnDef::new("password")
.string()
.null()
)
.col(
ColumnDef::new(Alias::new("created_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.to_owned(),
).await?;
// Create table `category`
manager.create_table(
Table::create()
.table(Alias::new("category"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
)
.col(
ColumnDef::new(Alias::new("server_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("name"))
.string()
.not_null()
)
.col(
ColumnDef::new(Alias::new("created_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
// L'index sera créé après via manager.create_index
.foreign_key(
ForeignKey::create()
.name("fk_category_server")
.from(Alias::new("category"), Alias::new("server_id"))
.to(Alias::new("server"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned(),
).await?;
// Create table `channel`
manager.create_table(
Table::create()
.table(Alias::new("channel"))
.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("category_id"))
.uuid()
.null()
)
.col(
ColumnDef::new(Alias::new("position"))
.integer()
.not_null()
.default(0)
)
.col(
ColumnDef::new(Alias::new("channel_type"))
.integer()
.not_null()
)
.col(
ColumnDef::new(Alias::new("name"))
.string()
.null()
)
.col(
ColumnDef::new(Alias::new("created_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
// Indexes créés après
.foreign_key(
ForeignKey::create()
.name("fk_channel_server")
.from(Alias::new("channel"), Alias::new("server_id"))
.to(Alias::new("server"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.foreign_key(
ForeignKey::create()
.name("fk_channel_category")
.from(Alias::new("channel"), Alias::new("category_id"))
.to(Alias::new("category"), Alias::new("id"))
.on_delete(ForeignKeyAction::SetNull)
)
.to_owned()
).await?;
// Create table `user`
manager.create_table(
Table::create()
.table(Alias::new("user"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
)
.col(
ColumnDef::new(Alias::new("username"))
.string()
.not_null()
)
.col(
ColumnDef::new(Alias::new("pub_key"))
.text()
.not_null()
.unique_key()
)
.col(
ColumnDef::new(Alias::new("created_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.to_owned()
).await?;
// Create table `message`
manager.create_table(
Table::create()
.table(Alias::new("message"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
)
.col(
ColumnDef::new(Alias::new("channel_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("user_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("content"))
.text()
.not_null()
)
.col(
ColumnDef::new(Alias::new("created_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.col(
ColumnDef::new(Alias::new("edited_at"))
.date_time()
.null()
)
.col(
ColumnDef::new(Alias::new("reply_to_id"))
.uuid()
.null()
)
// Indexes créés après
.foreign_key(
ForeignKey::create()
.name("fk_message_channel")
.from(Alias::new("message"), Alias::new("channel_id"))
.to(Alias::new("channel"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.foreign_key(
ForeignKey::create()
.name("fk_message_user")
.from(Alias::new("message"), Alias::new("user_id"))
.to(Alias::new("user"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.foreign_key(
ForeignKey::create()
.name("fk_message_reply_to")
.from(Alias::new("message"), Alias::new("reply_to_id"))
.to(Alias::new("message"), Alias::new("id"))
.on_delete(ForeignKeyAction::SetNull)
)
.to_owned()
).await?;
// Create table `attachment`
manager.create_table(
Table::create()
.table(Alias::new("attachment"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
)
.col(
ColumnDef::new(Alias::new("message_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("filename"))
.string()
.not_null()
)
.col(
ColumnDef::new(Alias::new("file_size"))
.big_integer()
.not_null()
)
.col(
ColumnDef::new(Alias::new("mime_type"))
.string()
.not_null()
)
.col(
ColumnDef::new(Alias::new("created_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
// Index créé après
.foreign_key(
ForeignKey::create()
.name("fk_attachment_message")
.from(Alias::new("attachment"), Alias::new("message_id"))
.to(Alias::new("message"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.to_owned()
).await?;
// Create M2M table `server_user`
manager.create_table(
Table::create()
.table(Alias::new("server_user"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
)
.col(
ColumnDef::new(Alias::new("server_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("user_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("username"))
.string()
.null()
)
.col(
ColumnDef::new(Alias::new("joined_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.date_time()
.not_null()
)
// Indexes créés après
.foreign_key(
ForeignKey::create()
.name("fk_server_user_server")
.from(Alias::new("server_user"), Alias::new("server_id"))
.to(Alias::new("server"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.foreign_key(
ForeignKey::create()
.name("fk_server_user_user")
.from(Alias::new("server_user"), Alias::new("user_id"))
.to(Alias::new("user"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.to_owned()
).await?;
// Create M2M table `channel_user`
manager.create_table(
Table::create()
.table(Alias::new("channel_user"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
)
.col(
ColumnDef::new(Alias::new("channel_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("user_id"))
.uuid()
.not_null()
)
.col(
ColumnDef::new(Alias::new("role"))
.string()
.not_null()
.default("member".to_owned())
)
.col(
ColumnDef::new(Alias::new("joined_at"))
.date_time()
.not_null()
.default(Expr::current_timestamp())
)
// Indexes créés après
.foreign_key(
ForeignKey::create()
.name("fk_channel_user_channel")
.from(Alias::new("channel_user"), Alias::new("channel_id"))
.to(Alias::new("channel"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.foreign_key(
ForeignKey::create()
.name("fk_channel_user_user")
.from(Alias::new("channel_user"), Alias::new("user_id"))
.to(Alias::new("user"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
)
.to_owned()
).await?;
// --------------------------------------------------------------------
// Création des INDEX après les tables
// --------------------------------------------------------------------
// category(server_id)
manager.create_index(
Index::create()
.name("idx_category_server_id")
.table(Alias::new("category"))
.col(Alias::new("server_id"))
.to_owned()
).await?;
// channel(server_id)
manager.create_index(
Index::create()
.name("idx_channel_server_id")
.table(Alias::new("channel"))
.col(Alias::new("server_id"))
.to_owned()
).await?;
// channel(category_id)
manager.create_index(
Index::create()
.name("idx_channel_category_id")
.table(Alias::new("channel"))
.col(Alias::new("category_id"))
.to_owned()
).await?;
// message(channel_id)
manager.create_index(
Index::create()
.name("idx_message_channel_id")
.table(Alias::new("message"))
.col(Alias::new("channel_id"))
.to_owned()
).await?;
// message(user_id)
manager.create_index(
Index::create()
.name("idx_message_user_id")
.table(Alias::new("message"))
.col(Alias::new("user_id"))
.to_owned()
).await?;
// message(reply_to_id)
manager.create_index(
Index::create()
.name("idx_message_reply_to_id")
.table(Alias::new("message"))
.col(Alias::new("reply_to_id"))
.to_owned()
).await?;
// attachment(message_id)
manager.create_index(
Index::create()
.name("idx_attachment_message_id")
.table(Alias::new("attachment"))
.col(Alias::new("message_id"))
.to_owned()
).await?;
// server_user(server_id)
manager.create_index(
Index::create()
.name("idx_server_user_server_id")
.table(Alias::new("server_user"))
.col(Alias::new("server_id"))
.to_owned()
).await?;
// server_user(user_id)
manager.create_index(
Index::create()
.name("idx_server_user_user_id")
.table(Alias::new("server_user"))
.col(Alias::new("user_id"))
.to_owned()
).await?;
// unique (server_id, user_id)
manager.create_index(
Index::create()
.name("uk_server_user_server_user")
.table(Alias::new("server_user"))
.col(Alias::new("server_id"))
.col(Alias::new("user_id"))
.unique()
.to_owned()
).await?;
// channel_user(channel_id)
manager.create_index(
Index::create()
.name("idx_channel_user_channel_id")
.table(Alias::new("channel_user"))
.col(Alias::new("channel_id"))
.to_owned()
).await?;
// channel_user(user_id)
manager.create_index(
Index::create()
.name("idx_channel_user_user_id")
.table(Alias::new("channel_user"))
.col(Alias::new("user_id"))
.to_owned()
).await?;
// unique (channel_id, user_id)
manager.create_index(
Index::create()
.name("uk_channel_user_channel_user")
.table(Alias::new("channel_user"))
.col(Alias::new("channel_id"))
.col(Alias::new("user_id"))
.unique()
.to_owned()
).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Alias::new("channel_user")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("server_user")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("attachment")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("message")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("channel")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("category")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("user")).to_owned()).await?;
manager.drop_table(Table::drop().table(Alias::new("server")).to_owned()).await?;
Ok(())
}
}

6
migration/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

59
src/app/app.rs Normal file
View File

@@ -0,0 +1,59 @@
use crate::app::AppState;
use crate::config::Config;
use crate::database::Database;
use crate::network::http::HTTPServer;
use crate::network::udp::UDPServer;
pub struct App {
config: Config,
db: Database,
udp_server: UDPServer,
http_server: HTTPServer
}
impl App {
pub async fn init(config: Config) -> Self {
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database");
let state = AppState::new(db.clone());
// let state = AppState::new();
let udp_server = UDPServer::new(config.bind_addr());
let http_server = HTTPServer::new(config.bind_addr(), state);
Self {
config,
db,
udp_server,
http_server
}
}
pub async fn run(&self) {
println!("Application démarrée. Appuyez sur Ctrl+C pour arrêter.");
// Le select arbitre la course
tokio::select! {
// Branche 1 : Le serveur tourne. S'il crash ou finit (peu probable), on sort.
_ = self.udp_server.run() => {
println!("Le serveur UDP s'est arrêté de lui-même (erreur ?).");
}
_ = self.http_server.run() => {
println!("Le serveur HTTP s'est arrêté de lui-même (erreur ?).");
}
// Branche 2 : On écoute le signal d'arrêt.
_ = tokio::signal::ctrl_c() => {
println!("Signal d'arrêt reçu !");
}
}
println!("Nettoyage et fermeture de l'application.");
}
}

5
src/app/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
mod app;
mod state;
pub use app::App;
pub use state::AppState;

23
src/app/state.rs Normal file
View File

@@ -0,0 +1,23 @@
use crate::database::Database;
#[derive(Clone)]
pub struct AppState {
pub db: Database
}
impl AppState {
pub fn new(db: Database) -> Self {
Self { db }
}
}
// #[derive(Clone)]
// pub struct AppState {
//
// }
//
// impl AppState {
// pub fn new() -> Self {
// Self { }
// }
// }

160
src/config/config.rs Normal file
View File

@@ -0,0 +1,160 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub jwt: JwtConfig,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServerConfig {
pub bind_addr: String,
pub mode: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DatabaseConfig {
#[serde(rename = "type")]
pub db_type: String,
// SQLite
pub path: Option<String>,
// PostgreSQL / MySQL
pub host: Option<String>,
pub port: Option<u16>,
pub user: Option<String>,
pub password: Option<String>,
pub dbname: Option<String>,
// PostgreSQL specific
pub sslmode: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JwtConfig {
pub secret: String,
pub expiration: u64,
}
impl Config {
/// Loads configuration from a TOML file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let content = fs::read_to_string(path)
.map_err(|e| ConfigError::IoError(e.to_string()))?;
let config: Config = toml::from_str(&content)
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
config.validate()?;
Ok(config)
}
/// Validates the configuration
fn validate(&self) -> Result<(), ConfigError> {
// Validate server mode
if self.server.mode != "debug" && self.server.mode != "release" {
return Err(ConfigError::ValidationError(
format!("Mode invalide: '{}'. Doit être 'debug' ou 'release'", self.server.mode)
));
}
// Validate database type
match self.database.db_type.as_str() {
"sqlite" => {
if self.database.path.is_none() {
return Err(ConfigError::ValidationError(
"SQLite database path is required".to_string()
));
}
}
"postgres" | "mysql" => {
if self.database.host.is_none()
|| self.database.port.is_none()
|| self.database.user.is_none()
|| self.database.password.is_none()
|| self.database.dbname.is_none() {
return Err(ConfigError::ValidationError(
format!("Incomplete configuration for {}", self.database.db_type)
));
}
}
_ => {
return Err(ConfigError::ValidationError(
format!("Invalid database type: '{}'. Must be 'sqlite', 'postgres' or 'mysql'",
self.database.db_type)
));
}
}
// Validate JWT secret
if self.jwt.secret == "your_very_secure_jwt_secret_to_change" {
eprintln!("⚠️ WARNING: You are using the default JWT secret! Change it in production!");
}
if self.jwt.secret.len() < 32 {
eprintln!("⚠️ WARNING: JWT secret is too short (minimum 32 characters recommended)");
}
Ok(())
}
/// Returns the database connection string
pub fn database_url(&self) -> String {
match self.database.db_type.as_str() {
"sqlite" => {
format!("sqlite://{}?mode=rwc", self.database.path.as_ref().unwrap())
}
"postgres" => {
format!(
"postgresql://{}:{}@{}:{}/{}?sslmode={}",
self.database.user.as_ref().unwrap(),
self.database.password.as_ref().unwrap(),
self.database.host.as_ref().unwrap(),
self.database.port.unwrap(),
self.database.dbname.as_ref().unwrap(),
self.database.sslmode.as_ref().unwrap_or(&"disable".to_string())
)
}
"mysql" => {
format!(
"mysql://{}:{}@{}:{}/{}",
self.database.user.as_ref().unwrap(),
self.database.password.as_ref().unwrap(),
self.database.host.as_ref().unwrap(),
self.database.port.unwrap(),
self.database.dbname.as_ref().unwrap()
)
}
_ => panic!("Unsupported database type")
}
}
/// Returns the SocketAddr to bind to
pub fn bind_addr(&self) -> std::net::SocketAddr {
self.server.bind_addr.parse().unwrap()
}
}
#[derive(Debug)]
pub enum ConfigError {
IoError(String),
ParseError(String),
ValidationError(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ConfigError::IoError(msg) => write!(f, "IO Error: {}", msg),
ConfigError::ParseError(msg) => write!(f, "Parsing Error: {}", msg),
ConfigError::ValidationError(msg) => write!(f, "Validation Error: {}", msg),
}
}
}
impl std::error::Error for ConfigError {}

3
src/config/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod config;
pub use config::Config;

34
src/database/database.rs Normal file
View File

@@ -0,0 +1,34 @@
use std::time::Duration;
use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection, DbErr};
use migration::{Migrator, MigratorTrait};
#[derive(Clone)]
pub struct Database {
pub connection: DatabaseConnection,
}
impl Database {
pub async fn init(dsn: &str) -> Result<Self, DbErr> {
let mut opt = ConnectOptions::new(dsn);
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.acquire_timeout(Duration::from_secs(8))
.sqlx_logging(true)
.sqlx_logging_level(log::LevelFilter::Debug);
let connection = SeaDatabase::connect(opt).await?;
// On lance les migrations ici.
// Si ça échoue, le programme s'arrête proprement à l'init.
Migrator::up(&connection, None).await?;
Ok(Self { connection })
}
// Tu peux ajouter ici tes méthodes helpers si tu veux encapsuler SeaORM
// ex: pub async fn find_user(...)
pub fn get_connection(&self) -> &DatabaseConnection {
&self.connection
}
}

3
src/database/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod database;
pub use database::Database;

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod config;
pub mod app;
pub mod network;
pub mod utils;
pub mod database;
pub mod models;
pub mod serializers;
pub mod repositories;

17
src/main.rs Normal file
View File

@@ -0,0 +1,17 @@
use ox_speak_server_lib::app::App;
use ox_speak_server_lib::config;
#[tokio::main]
async fn main() {
env_logger::init();
let config = match config::Config::from_file("config.toml") {
Ok(config) => config,
Err(e) => panic!("Error loading configuration: {}", e)
};
println!("Configuration loaded: {:?}", config);
let app = App::init(config).await;
app.run().await;
}

45
src/models/attachment.rs Normal file
View File

@@ -0,0 +1,45 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "attachment")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub message_id: Uuid,
pub filename: String,
pub file_size: i32,
pub mime_type: String,
pub created_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::message::Entity",
from = "Column::MessageId",
to = "super::message::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Message,
}
impl Related<super::message::Entity> for Entity {
fn to() -> RelationDef {
Relation::Message.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

52
src/models/category.rs Normal file
View File

@@ -0,0 +1,52 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "category")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub server_id: Uuid,
pub name: String,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::channel::Entity")]
Channel,
#[sea_orm(
belongs_to = "super::server::Entity",
from = "Column::ServerId",
to = "super::server::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Server,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::server::Entity> for Entity {
fn to() -> RelationDef {
Relation::Server.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

77
src/models/channel.rs Normal file
View File

@@ -0,0 +1,77 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "channel")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: i32,
pub name: Option<String>,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::category::Entity",
from = "Column::CategoryId",
to = "super::category::Column::Id",
on_update = "NoAction",
on_delete = "SetNull"
)]
Category,
#[sea_orm(has_many = "super::channel_user::Entity")]
ChannelUser,
#[sea_orm(has_many = "super::message::Entity")]
Message,
#[sea_orm(
belongs_to = "super::server::Entity",
from = "Column::ServerId",
to = "super::server::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Server,
}
impl Related<super::category::Entity> for Entity {
fn to() -> RelationDef {
Relation::Category.def()
}
}
impl Related<super::channel_user::Entity> for Entity {
fn to() -> RelationDef {
Relation::ChannelUser.def()
}
}
impl Related<super::message::Entity> for Entity {
fn to() -> RelationDef {
Relation::Message.def()
}
}
impl Related<super::server::Entity> for Entity {
fn to() -> RelationDef {
Relation::Server.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

View File

@@ -0,0 +1,58 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "channel_user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub channel_id: Uuid,
pub user_id: Uuid,
pub role: String,
pub joined_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Channel,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

77
src/models/message.rs Normal file
View File

@@ -0,0 +1,77 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "message")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub channel_id: Uuid,
pub user_id: Uuid,
#[sea_orm(column_type = "Text")]
pub content: String,
pub created_at: DateTime,
pub edited_at: Option<DateTime>,
pub reply_to_id: Option<Uuid>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::attachment::Entity")]
Attachment,
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Channel,
#[sea_orm(
belongs_to = "Entity",
from = "Column::ReplyToId",
to = "Column::Id",
on_update = "NoAction",
on_delete = "SetNull"
)]
SelfRef,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::attachment::Entity> for Entity {
fn to() -> RelationDef {
Relation::Attachment.def()
}
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

12
src/models/mod.rs Normal file
View File

@@ -0,0 +1,12 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub mod prelude;
pub mod attachment;
pub mod category;
pub mod channel;
pub mod channel_user;
pub mod message;
pub mod server;
pub mod server_user;
pub mod user;

10
src/models/prelude.rs Normal file
View File

@@ -0,0 +1,10 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub use super::attachment::Entity as Attachment;
pub use super::category::Entity as Category;
pub use super::channel::Entity as Channel;
pub use super::channel_user::Entity as ChannelUser;
pub use super::message::Entity as Message;
pub use super::server::Entity as Server;
pub use super::server_user::Entity as ServerUser;
pub use super::user::Entity as User;

54
src/models/server.rs Normal file
View File

@@ -0,0 +1,54 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "server")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub name: String,
pub password: Option<String>,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::category::Entity")]
Category,
#[sea_orm(has_many = "super::channel::Entity")]
Channel,
#[sea_orm(has_many = "super::server_user::Entity")]
ServerUser,
}
impl Related<super::category::Entity> for Entity {
fn to() -> RelationDef {
Relation::Category.def()
}
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::server_user::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerUser.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

59
src/models/server_user.rs Normal file
View File

@@ -0,0 +1,59 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "server_user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub server_id: Uuid,
pub user_id: Uuid,
pub username: Option<String>,
pub joined_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::server::Entity",
from = "Column::ServerId",
to = "super::server::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Server,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::server::Entity> for Entity {
fn to() -> RelationDef {
Relation::Server.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

55
src/models/user.rs Normal file
View File

@@ -0,0 +1,55 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub username: String,
#[sea_orm(column_type = "Text", unique)]
pub pub_key: String,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::channel_user::Entity")]
ChannelUser,
#[sea_orm(has_many = "super::message::Entity")]
Message,
#[sea_orm(has_many = "super::server_user::Entity")]
ServerUser,
}
impl Related<super::channel_user::Entity> for Entity {
fn to() -> RelationDef {
Relation::ChannelUser.def()
}
}
impl Related<super::message::Entity> for Entity {
fn to() -> RelationDef {
Relation::Message.def()
}
}
impl Related<super::server_user::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerUser.def()
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}

View File

@@ -0,0 +1,20 @@
use std::time::Instant;
use uuid::Uuid;
use crate::app::AppState;
#[derive(Clone, Debug)]
pub struct RequestContext {
pub request_id: Uuid,
pub started_at: Instant,
pub method: axum::http::Method,
pub uri: axum::http::Uri,
pub user: Option<CurrentUser>,
}
#[derive(Clone, Debug)]
pub struct CurrentUser {
pub id: Uuid,
pub username: String,
}

49
src/network/http/error.rs Normal file
View File

@@ -0,0 +1,49 @@
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;
use sea_orm::DbErr;
#[derive(Debug)]
pub enum HTTPError {
Database(DbErr),
NotFound,
BadRequest(String),
InternalServerError(String),
}
// Conversion automatique depuis DbErr (erreurs SeaORM)
impl From<DbErr> for HTTPError {
fn from(err: DbErr) -> Self {
HTTPError::Database(err)
}
}
// Conversion depuis ParseError (pour UUID, etc.)
impl From<uuid::Error> for HTTPError {
fn from(err: uuid::Error) -> Self {
HTTPError::BadRequest(format!("Invalid UUID: {}", err))
}
}
// Implémentation pour Axum : transformer AppError en réponse HTTP
impl IntoResponse for HTTPError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
HTTPError::Database(err) => {
eprintln!("Database error: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, "Database error")
}
HTTPError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
HTTPError::BadRequest(msg) => {
return (StatusCode::BAD_REQUEST, Json(json!({ "error": msg }))).into_response();
}
HTTPError::InternalServerError(msg) => {
eprintln!("Internal error: {}", msg);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": msg }))).into_response();
}
};
(status, Json(json!({ "error": error_message }))).into_response()
}
}

View File

@@ -0,0 +1,56 @@
use axum::{
extract::State,
http::Request,
middleware::Next,
response::Response,
};
use std::time::Instant;
use uuid::Uuid;
use crate::app::AppState;
use crate::network::http::context::{CurrentUser, RequestContext};
pub async fn context_middleware(
State(app_state): State<AppState>,
mut req: Request<axum::body::Body>,
next: Next,
) -> Response {
let request_id = Uuid::new_v4();
let started_at = Instant::now();
// Infos "type Django request"
let method = req.method().clone();
let uri = req.uri().clone();
// Exemple: récupérer un user depuis un token (pseudo-code)
// Ici je laisse volontairement une logique minimaliste/placeholder.
// Le but: montrer où tu branches ta vraie auth.
let user: Option<CurrentUser> = {
let _maybe_auth = req
.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok());
// TODO: vérifier token -> user_id -> charger en DB avec app_state
// Some(CurrentUser { id: ..., username: ... })
None
};
// Injecte le contexte dans la requête
req.extensions_mut().insert(RequestContext {
request_id,
started_at,
method: method.clone(),
uri: uri.clone(),
user,
});
println!(">>> Incoming [{}] {} {}", request_id, method, uri);
// Passe la requête au reste de la stack
let resp = next.run(req).await;
println!("<<< Response [{}]: {}", request_id, resp.status());
resp
}

15
src/network/http/mod.rs Normal file
View File

@@ -0,0 +1,15 @@
use axum::Router;
use crate::app::AppState;
mod server;
mod router;
mod middleware;
mod web;
mod error;
mod context;
pub use server::HTTPServer;
pub use error::HTTPError;
pub use context::RequestContext;
pub type AppRouter = Router<AppState>;

View File

@@ -0,0 +1,12 @@
use std::sync::Arc;
use axum::{middleware, Router};
use crate::app::AppState;
use crate::network::http::middleware::context_middleware;
use crate::network::http::{web, AppRouter};
pub fn setup_route(app_state: AppState) -> Router {
Router::new()
.merge(web::setup_route())
.layer(middleware::from_fn_with_state(app_state.clone(), context_middleware))
.with_state(app_state)
}

View File

@@ -0,0 +1,24 @@
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use crate::app::AppState;
use crate::network::http::router::setup_route;
#[derive(Clone)]
pub struct HTTPServer {
bind_addr: SocketAddr,
app_state: AppState
}
impl HTTPServer {
pub fn new(bind_addr: SocketAddr, app_state: AppState) -> Self {
Self {bind_addr, app_state}
}
pub async fn run(&self) -> std::io::Result<()> {
let route = setup_route(self.app_state.clone());
let listener = TcpListener::bind(&self.bind_addr).await?;
axum::serve(listener, route).await
}
}

View File

@@ -0,0 +1,87 @@
use axum::{Extension, Json};
use axum::extract::{Path, State};
use axum::http::{Extensions, StatusCode};
use axum::routing::{delete, get, post, put};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use uuid::Uuid;
use crate::app::AppState;
use crate::models::category;
use crate::network::http::{AppRouter, HTTPError};
use crate::network::http::RequestContext;
use crate::serializers::CategorySerializer;
pub fn setup_route() -> AppRouter {
AppRouter::new()
.route("/categories/", get(category_list))
.route("/categories/{id}/", get(category_detail))
.route("/categories/", post(category_create))
.route("/categories/{id}/", put(category_update))
.route("/categories/{id}/", delete(category_delete))
}
pub async fn category_list(
State(app_state): State<AppState>,
Extension(ctx): Extension<RequestContext>
) -> Result<Json<Vec<CategorySerializer>>, HTTPError> {
let categories = category::Entity::find()
.all(app_state.db.get_connection())
.await?;
Ok(Json(categories.into_iter().map(CategorySerializer::from).collect()))
}
pub async fn category_detail(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<Json<CategorySerializer>, HTTPError> {
let category = category::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
Ok(Json(CategorySerializer::from(category)))
}
pub async fn category_create(
State(app_state): State<AppState>,
Json(serializer): Json<CategorySerializer>
) -> Result<Json<CategorySerializer>, HTTPError> {
let active = serializer.into_active_model();
let category: category::Model = active.insert(app_state.db.get_connection()).await?;
Ok(Json(CategorySerializer::from(category)))
}
pub async fn category_update(
State(app_state): State<AppState>,
Path(id): Path<Uuid>,
Json(serializer): Json<CategorySerializer>,
) -> Result<Json<CategorySerializer>, HTTPError> {
let category = category::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
let active = category.into_active_model();
let category: category::Model = serializer.apply_to_active_model(active)
.update(app_state.db.get_connection())
.await?;
Ok(Json(CategorySerializer::from(category)))
}
pub async fn category_delete(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<StatusCode, HTTPError> {
let result = category::Entity::delete_by_id(id)
.exec(app_state.db.get_connection())
.await?;
if result.rows_affected == 0 {
Err(HTTPError::NotFound)
} else {
Ok(StatusCode::NO_CONTENT)
}
}

View File

@@ -0,0 +1,85 @@
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get, post, put};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use uuid::Uuid;
use crate::app::AppState;
use crate::models::channel;
use crate::network::http::{AppRouter, HTTPError};
use crate::serializers::ChannelSerializer;
pub fn setup_route() -> AppRouter {
AppRouter::new()
.route("/channels/", get(channel_list))
.route("/channels/{id}/", get(channel_detail))
.route("/channels/", post(channel_create))
.route("/channels/{id}/", put(channel_update))
.route("/channels/{id}/", delete(channel_delete))
}
pub async fn channel_list(
State(app_state): State<AppState>
) -> Result<Json<Vec<ChannelSerializer>>, HTTPError> {
let channels = channel::Entity::find()
.all(app_state.db.get_connection())
.await?;
Ok(Json(channels.into_iter().map(ChannelSerializer::from).collect()))
}
pub async fn channel_detail(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<Json<ChannelSerializer>, HTTPError> {
let channel = channel::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
Ok(Json(ChannelSerializer::from(channel)))
}
pub async fn channel_create(
State(app_state): State<AppState>,
Json(serializer): Json<ChannelSerializer>
) -> Result<Json<ChannelSerializer>, HTTPError> {
let active = serializer.into_active_model();
let channel: channel::Model = active.insert(app_state.db.get_connection()).await?;
Ok(Json(ChannelSerializer::from(channel)))
}
pub async fn channel_update(
State(app_state): State<AppState>,
Path(id): Path<Uuid>,
Json(serializer): Json<ChannelSerializer>,
) -> Result<Json<ChannelSerializer>, HTTPError> {
let channel = channel::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
let active = channel.into_active_model();
let channel: channel::Model = serializer.apply_to_active_model(active)
.update(app_state.db.get_connection())
.await?;
Ok(Json(ChannelSerializer::from(channel)))
}
pub async fn channel_delete(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<StatusCode, HTTPError> {
let result = channel::Entity::delete_by_id(id)
.exec(app_state.db.get_connection())
.await?;
if result.rows_affected > 0 {
Ok(StatusCode::NO_CONTENT)
} else {
Err(HTTPError::NotFound)
}
}

View File

@@ -0,0 +1,84 @@
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get, post, put};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use uuid::Uuid;
use crate::app::AppState;
use crate::models::message;
use crate::network::http::{AppRouter, HTTPError};
use crate::serializers::MessageSerializer;
pub fn setup_route() -> AppRouter {
AppRouter::new()
.route("/messages/", get(message_list))
.route("/messages/{id}/", get(message_detail))
.route("/messages/", post(message_create))
.route("/messages/{id}/", put(message_update))
.route("/messages/{id}/", delete(message_delete))
}
pub async fn message_list(
State(app_state): State<AppState>
) -> Result<Json<Vec<MessageSerializer>>, HTTPError> {
let messages = message::Entity::find()
.all(app_state.db.get_connection())
.await?;
Ok(Json(messages.into_iter().map(MessageSerializer::from).collect()))
}
pub async fn message_detail(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<Json<MessageSerializer>, HTTPError> {
let message = message::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
Ok(Json(MessageSerializer::from(message)))
}
pub async fn message_create(
State(app_state): State<AppState>,
Json(serializer): Json<MessageSerializer>
) -> Result<Json<MessageSerializer>, HTTPError> {
let active = serializer.into_active_model();
let message: message::Model = active.insert(app_state.db.get_connection()).await?;
Ok(Json(MessageSerializer::from(message)))
}
pub async fn message_update(
State(app_state): State<AppState>,
Path(id): Path<Uuid>,
Json(serializer): Json<MessageSerializer>,
) -> Result<Json<MessageSerializer>, HTTPError> {
let message = message::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
let active = message.into_active_model();
let message: message::Model = serializer.apply_to_active_model(active)
.update(app_state.db.get_connection())
.await?;
Ok(Json(MessageSerializer::from(message)))
}
pub async fn message_delete(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<StatusCode, HTTPError> {
let result = message::Entity::delete_by_id(id)
.exec(app_state.db.get_connection())
.await?;
if result.rows_affected == 0 {
Err(HTTPError::NotFound)
} else {
Ok(StatusCode::NO_CONTENT)
}
}

View File

@@ -0,0 +1,15 @@
use crate::network::http::AppRouter;
mod category;
mod channel;
mod message;
mod server;
pub fn setup_route() -> AppRouter {
AppRouter::new()
.nest("/category", category::setup_route())
.nest("/channel", channel::setup_route())
.nest("/message", message::setup_route())
.nest("/server", server::setup_route())
}

View File

@@ -0,0 +1,85 @@
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get, post, put};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use uuid::Uuid;
use crate::app::AppState;
use crate::models::server;
use crate::network::http::{AppRouter, HTTPError};
use crate::serializers::ServerSerializer;
pub fn setup_route() -> AppRouter {
AppRouter::new()
.route("/servers/", get(server_list))
.route("/servers/{id}/", get(server_detail))
.route("/servers/{id}/", post(server_create))
.route("/servers/{id}/", put(server_update))
.route("/servers/{id}/", delete(server_delete))
}
pub async fn server_list(
State(app_state): State<AppState>
) -> Result<Json<Vec<ServerSerializer>>, HTTPError> {
let servers = server::Entity::find()
.all(app_state.db.get_connection())
.await?;
Ok(Json(servers.into_iter().map(ServerSerializer::from).collect()))
}
pub async fn server_detail(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<Json<ServerSerializer>, HTTPError> {
let server = server::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
Ok(Json(ServerSerializer::from(server)))
}
pub async fn server_create(
State(app_state): State<AppState>,
Json(serializer): Json<ServerSerializer>
) -> Result<Json<ServerSerializer>, HTTPError> {
let active = serializer.into_active_model();
let server: server::Model = active.insert(app_state.db.get_connection()).await?;
Ok(Json(ServerSerializer::from(server)))
}
pub async fn server_update(
State(app_state): State<AppState>,
Path(id): Path<Uuid>,
Json(serializer): Json<ServerSerializer>,
) -> Result<Json<ServerSerializer>, HTTPError> {
let server = server::Entity::find_by_id(id)
.one(app_state.db.get_connection())
.await?
.ok_or(HTTPError::NotFound)?;
let active = server.into_active_model();
let server: server::Model = serializer.apply_to_active_model(active)
.update(app_state.db.get_connection())
.await?;
Ok(Json(ServerSerializer::from(server)))
}
pub async fn server_delete(
State(app_state): State<AppState>,
Path(id): Path<Uuid>
) -> Result<StatusCode, HTTPError> {
let result = server::Entity::delete_by_id(id)
.exec(app_state.db.get_connection())
.await?;
if result.rows_affected == 0 {
Err(HTTPError::NotFound)
} else {
Ok(StatusCode::NO_CONTENT)
}
}

View File

View File

@@ -0,0 +1,11 @@
use axum::Router;
use crate::app::AppState;
use crate::network::http::AppRouter;
mod api;
pub fn setup_route() -> AppRouter {
AppRouter::new()
.nest("/api", api::setup_route())
}

2
src/network/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod udp;
pub mod http;

3
src/network/udp/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod server;
pub use server::UDPServer;

114
src/network/udp/server.rs Normal file
View File

@@ -0,0 +1,114 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::thread::available_parallelism;
use std::io;
use tokio::net::UdpSocket;
use parking_lot::RwLock;
use tokio::task;
#[derive(Clone)]
pub struct UDPServer {
table_router: Arc<RwLock<HashMap<String, SocketAddr>>>,
bind_addr: SocketAddr
}
impl UDPServer {
pub fn new(bind_addr: SocketAddr) -> Self {
Self {
table_router: Arc::new(RwLock::new(HashMap::new())),
bind_addr
}
}
pub async fn run(&self) -> io::Result<()> {
#[cfg(unix)]
{
self.run_unix().await
}
#[cfg(windows)]
{
self.run_windows().await
}
#[cfg(not(any(unix, windows)))]
{
Err(io::Error::new(io::ErrorKind::Other, "Unsupported platform"))
}
}
#[cfg(unix)]
async fn run_unix(&self) -> io::Result<()> {
use socket2::{Domain, Protocol, Socket, Type};
let mut workers = Vec::new();
for id in available_parallelism() {
let bind_addr = self.bind_addr.clone();
let domain = match bind_addr {
SocketAddr::V4(_) => Domain::IPV4,
SocketAddr::V6(_) => Domain::IPV6,
};
let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
sock.set_reuse_address(true)?;
sock.set_reuse_port(true)?;
sock.bind(&bind_addr.into())?;
let std_sock = std::net::UdpSocket::from(sock);
std_sock.set_nonblocking(true)?;
let udp = UdpSocket::from_std(std_sock)?;
let buffer_size = 1500;
let worker = task::spawn(async move {
if let Err(e) = Self::worker_loop(udp, buffer_size).await {
eprintln!("Worker loop error: {}", e);
}
});
workers.push(worker);
}
for worker in workers {
let _ = worker.await;
}
println!("All UDP workers stopped.");
Ok(())
}
#[cfg(windows)]
async fn run_windows(&self) -> io::Result<()> {
let udp = UdpSocket::bind(self.bind_addr).await?;
let udp = Arc::new(udp);
let mut workers = Vec::with_capacity(self.workers);
for id in 0..self.workers {
let sock = udp.clone();
let buf_size = 1500;
let worker = task::spawn(async move {
if let Err(e) = Self::worker_loop(udp, buffer_size) {
eprintln!("Worker loop error: {}", e);
}
});
workers.push(worker);
}
for worker in workers {
let _ = worker.await;
}
Ok(())
}
async fn worker_loop(socket: UdpSocket, buffer_size: usize) -> io::Result<()>{
let mut buffer = vec![0u8; buffer_size];
loop {
let (size, peer) = socket.recv_from(&mut buffer).await?;
Self::handle_packet(&socket, &buffer[..size]).await;
}
}
async fn handle_packet(socket: &UdpSocket, packet: &[u8]){
}
}

26
src/repositories/mod.rs Normal file
View File

@@ -0,0 +1,26 @@
use std::sync::Arc;
use sea_orm::DatabaseConnection;
use crate::repositories::server::ServerRepository;
mod server;
#[derive(Clone)]
pub struct RepositoryContext {
db: DatabaseConnection,
// pub events: EventBus, // si tu veux publier des events “post-save” plus tard
}
#[derive(Clone)]
pub struct Repositories {
pub server: ServerRepository,
}
impl Repositories {
pub fn new(db: DatabaseConnection) -> Self {
let context = Arc::new(RepositoryContext { db });
Self {
server: ServerRepository {context: context.clone()},
}
}
}

View File

@@ -0,0 +1,33 @@
use std::sync::Arc;
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
use crate::models::server;
use crate::repositories::RepositoryContext;
#[derive(Clone)]
pub struct ServerRepository {
pub context: Arc<RepositoryContext>
}
impl ServerRepository {
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<server::Model>, DbErr> {
server::Entity::find_by_id(id).one(&self.context.db).await
}
pub async fn update(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
let model = active.update(&self.context.db).await?;
// plus tard: self.context.events.publish(...)
Ok(model)
}
pub async fn create(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
let model = active.insert(&self.context.db).await?;
// plus tard: emit post-save
Ok(model)
}
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
server::Entity::delete_by_id(id).exec(&self.context.db).await?;
// plus tard: emit post-delete
Ok(())
}
}

View File

@@ -0,0 +1,48 @@
use serde::{Serialize, Deserialize};
use sea_orm::ActiveValue::Set;
use uuid::Uuid;
use crate::models::category;
#[derive(Serialize, Deserialize)]
pub struct CategorySerializer {
#[serde(skip_deserializing)]
pub id: Option<Uuid>,
pub server_id: Uuid,
pub name: String,
#[serde(skip_deserializing)]
pub created_at: Option<String>,
#[serde(skip_deserializing)]
pub updated_at: Option<String>,
}
impl From<category::Model> for CategorySerializer {
fn from(model: category::Model) -> Self {
Self {
id: Some(model.id),
server_id: model.server_id,
name: model.name,
created_at: Some(model.created_at.to_string()),
updated_at: Some(model.updated_at.to_string()),
}
}
}
impl CategorySerializer {
pub fn into_active_model(self) -> category::ActiveModel {
category::ActiveModel {
server_id: Set(self.server_id),
name: Set(self.name),
..Default::default()
}
}
pub fn apply_to_active_model(self, mut active_model: category::ActiveModel) -> category::ActiveModel {
active_model.server_id = Set(self.server_id);
active_model.name = Set(self.name);
active_model
}
}

View File

@@ -0,0 +1,57 @@
use serde::{Serialize, Deserialize};
use sea_orm::ActiveValue::Set;
use uuid::Uuid;
use crate::models::channel;
#[derive(Serialize, Deserialize)]
pub struct ChannelSerializer {
#[serde(skip_deserializing)]
pub id: Option<Uuid>,
pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>,
pub name: Option<String>,
pub position: Option<i32>,
pub channel_type: i32,
#[serde(skip_deserializing)]
pub created_at: Option<String>,
#[serde(skip_deserializing)]
pub updated_at: Option<String>,
}
impl From<channel::Model> for ChannelSerializer {
fn from(model: channel::Model) -> Self {
Self {
id: Some(model.id),
server_id: model.server_id,
category_id: model.category_id,
name: model.name,
position: Some(model.position),
channel_type: model.channel_type,
created_at: Some(model.created_at.to_string()),
updated_at: Some(model.updated_at.to_string()),
}
}
}
impl ChannelSerializer {
pub fn into_active_model(self) -> channel::ActiveModel {
channel::ActiveModel {
server_id: Set(self.server_id),
category_id: Set(self.category_id),
name: Set(self.name),
position: Set(self.position.unwrap_or(0)),
channel_type: Set(self.channel_type),
..Default::default()
}
}
pub fn apply_to_active_model(self, mut active_model: channel::ActiveModel) -> channel::ActiveModel {
active_model.server_id = Set(self.server_id);
active_model.category_id = Set(self.category_id);
active_model.name = Set(self.name);
active_model.position = Set(self.position.unwrap_or(0));
active_model.channel_type = Set(self.channel_type);
active_model
}
}

View File

@@ -0,0 +1,49 @@
use serde::{Serialize, Deserialize};
use sea_orm::ActiveValue::Set;
use uuid::Uuid;
use crate::models::message;
#[derive(Serialize, Deserialize)]
pub struct MessageSerializer {
#[serde(skip_deserializing)]
pub id: Option<Uuid>,
pub channel_id: Option<Uuid>,
pub author_id: Option<Uuid>,
pub content: String,
#[serde(skip_deserializing)]
pub created_at: Option<String>,
#[serde(skip_deserializing)]
pub updated_at: Option<String>,
}
impl From<message::Model> for MessageSerializer {
fn from(model: message::Model) -> Self {
Self {
id: Some(model.id),
channel_id: Some(model.channel_id),
author_id: Some(model.user_id),
content: model.content,
created_at: Some(model.created_at.to_string()),
updated_at: Some(model.edited_at.unwrap_or(model.created_at).to_string()),
}
}
}
impl MessageSerializer {
pub fn into_active_model(self) -> message::ActiveModel {
message::ActiveModel {
channel_id: Set(self.channel_id.unwrap()),
user_id: Set(self.author_id.unwrap()),
content: Set(self.content),
..Default::default()
}
}
pub fn apply_to_active_model(self, mut active_model: message::ActiveModel) -> message::ActiveModel {
active_model.channel_id = Set(self.channel_id.unwrap());
active_model.user_id = Set(self.author_id.unwrap());
active_model.content = Set(self.content);
active_model
}
}

9
src/serializers/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
mod server;
mod category;
mod channel;
mod message;
pub use server::*;
pub use category::*;
pub use channel::*;
pub use message::*;

54
src/serializers/server.rs Normal file
View File

@@ -0,0 +1,54 @@
use serde::{Serialize, Deserialize};
use sea_orm::ActiveValue::Set;
use uuid::Uuid;
use crate::models::server;
#[derive(Serialize, Deserialize)]
pub struct ServerSerializer {
#[serde(skip_deserializing)]
pub id: Option<Uuid>,
pub name: String,
#[serde(skip_serializing)]
pub password: Option<String>,
#[serde(skip_deserializing)]
pub created_at: Option<String>,
#[serde(skip_deserializing)]
pub updated_at: Option<String>,
}
// On part du Model (données « propres » venant de la BDD),
// pas de l'ActiveModel (qui contient des ActiveValue<T>).
impl From<server::Model> for ServerSerializer {
fn from(model: server::Model) -> Self {
Self {
id: Some(model.id),
name: model.name,
password: model.password,
created_at: Some(model.created_at.to_string()),
updated_at: Some(model.updated_at.to_string()),
}
}
}
impl ServerSerializer {
/// équivalent de `create()` dun serializer DRF
pub fn into_active_model(self) -> server::ActiveModel {
server::ActiveModel {
name: Set(self.name),
// champ Option<String> -> tu peux le passer directement à Set(...)
password: Set(self.password),
..Default::default()
}
}
/// équivalent de `update(instance, validated_data)` en DRF
pub fn apply_to_active_model(self, mut active_model: server::ActiveModel) -> server::ActiveModel {
active_model.name = Set(self.name);
active_model.password = Set(self.password);
active_model
}
}

1
src/utils/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod toolbox;

9
src/utils/toolbox.rs Normal file
View File

@@ -0,0 +1,9 @@
pub fn number_of_cpus() -> usize {
match std::thread::available_parallelism() {
Ok(n) => n.get(),
Err(_) => {
eprintln!("Warning: Could not determine number of CPUs, defaulting to 1");
1
}
}
}