Init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/.idea
|
||||
Generated
+4041
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "oxspeak_server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "oxspeak_server_lib"
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "migration", "event_bus"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.52.1", features = ["full"] }
|
||||
config = "0.15.22"
|
||||
sea-orm = { version = "2.0.0-rc.38", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
|
||||
migration = { path = "migration" }
|
||||
event-bus = { path = "event_bus" }
|
||||
parking_lot = "0.12.5"
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.149"
|
||||
toml = "1.1.2"
|
||||
uuid = { version = "1.23.1", features = ["v4", "v7", "fast-rng", "serde"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] }
|
||||
thiserror = "2"
|
||||
utoipa = { version = "5", features = ["uuid"] }
|
||||
log = "0.4"
|
||||
glob = "0.3"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
[network]
|
||||
# IP address to bind to
|
||||
host = "0.0.0.0"
|
||||
# hostv6 = "::"
|
||||
# TCP and UDP port can be the same
|
||||
# HTTP port
|
||||
tcp_port = 8080
|
||||
# Voice/Video port
|
||||
udp_port = 8080
|
||||
|
||||
[database]
|
||||
# DSN for database
|
||||
# SQLite
|
||||
url = "sqlite://oxspeak.db"
|
||||
# PostgreSQL
|
||||
# url = "postgresql://user:passwd@localhost:5432/db_name"
|
||||
# MySQL
|
||||
# url = "mysql://user:passwd@localhost:3306/db_name"
|
||||
|
||||
[jwt]
|
||||
secret = "changeme"
|
||||
# Duration in seconds
|
||||
duration = 86400 # 1 day
|
||||
refresh_duration = 1296000 # 15 days
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "event-bus"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,396 @@
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use glob::Pattern;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Type brut d'un événement : pointeur atomique vers n'importe quelle valeur.
|
||||
pub type AnyEvent = Arc<dyn Any + Send + Sync>;
|
||||
|
||||
/// Capacité par défaut du buffer de chaque canal broadcast.
|
||||
const DEFAULT_CAPACITY: usize = 64;
|
||||
|
||||
/// Le bus d'événements central.
|
||||
///
|
||||
/// Partagez-le via `Arc<EventBus>` entre les modules.
|
||||
/// Chaque topic possède son propre canal broadcast : seuls les abonnés du bon
|
||||
/// topic sont réveillés lors d'un `emit` (wake-up ciblé).
|
||||
///
|
||||
/// # Exemple minimal — callback sync
|
||||
/// ```rust,no_run
|
||||
/// use std::sync::Arc;
|
||||
/// use oxspeak_server_lib::event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct User { name: String }
|
||||
///
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let bus = Arc::new(EventBus::new());
|
||||
///
|
||||
/// bus.on::<User>("user-connected", |user| {
|
||||
/// println!("Connecté : {:?}", user);
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple — callback async
|
||||
/// ```rust,no_run
|
||||
/// use std::sync::Arc;
|
||||
/// use oxspeak_server_lib::event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct User { name: String }
|
||||
///
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let bus = Arc::new(EventBus::new());
|
||||
///
|
||||
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
|
||||
/// println!("(async) Connecté : {:?}", user);
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-connected", User { name: "Bob".into() });
|
||||
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple — pattern glob avec topic
|
||||
/// ```rust,no_run
|
||||
/// use std::sync::Arc;
|
||||
/// use oxspeak_server_lib::event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct User { name: String }
|
||||
///
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let bus = Arc::new(EventBus::new());
|
||||
///
|
||||
/// bus.on_pattern::<User, _>("user-*", |topic, user| {
|
||||
/// match topic.as_str() {
|
||||
/// "user-created" => println!("Créé : {:?}", user),
|
||||
/// "user-deleted" => println!("Supprimé : {:?}", user),
|
||||
/// other => println!("{}: {:?}", other, user),
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-created", User { name: "Alice".into() });
|
||||
/// bus.emit("user-deleted", User { name: "Bob".into() });
|
||||
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct EventBus {
|
||||
/// Canaux par topic exact.
|
||||
channels: RwLock<HashMap<String, broadcast::Sender<AnyEvent>>>,
|
||||
/// Canaux pour les souscriptions par pattern glob.
|
||||
patterns: RwLock<Vec<(Pattern, broadcast::Sender<(String, AnyEvent)>)>>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
/// Crée un bus avec la capacité par défaut (64 messages par canal).
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
channels: RwLock::new(HashMap::new()),
|
||||
patterns: RwLock::new(Vec::new()),
|
||||
capacity: DEFAULT_CAPACITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un bus avec une capacité de buffer personnalisée.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
channels: RwLock::new(HashMap::new()),
|
||||
patterns: RwLock::new(Vec::new()),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Interne
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_or_create_sender(&self, topic: &str) -> broadcast::Sender<AnyEvent> {
|
||||
{
|
||||
let channels = self.channels.read();
|
||||
if let Some(tx) = channels.get(topic) {
|
||||
return tx.clone();
|
||||
}
|
||||
}
|
||||
let mut channels = self.channels.write();
|
||||
channels
|
||||
.entry(topic.to_string())
|
||||
.or_insert_with(|| {
|
||||
let (tx, _) = broadcast::channel(self.capacity);
|
||||
tx
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Émission
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Émet un événement sur un topic.
|
||||
///
|
||||
/// - Pousse l'event dans le canal du topic exact (si des abonnés existent).
|
||||
/// - Pousse l'event dans tous les canaux de pattern qui matchent le topic.
|
||||
/// - Si personne n'écoute, l'événement est ignoré silencieusement.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct User;
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.emit("user-connected", User);
|
||||
/// bus.emit("user-deleted", uuid::Uuid::new_v4());
|
||||
/// ```
|
||||
pub fn emit<T: Any + Send + Sync + 'static>(&self, topic: &str, event: T) {
|
||||
let event: AnyEvent = Arc::new(event);
|
||||
|
||||
// Abonnés au topic exact
|
||||
if let Some(tx) = self.channels.read().get(topic) {
|
||||
let _ = tx.send(Arc::clone(&event));
|
||||
}
|
||||
|
||||
// Abonnés aux patterns glob correspondants
|
||||
for (pattern, tx) in self.patterns.read().iter() {
|
||||
if pattern.matches(topic) {
|
||||
let _ = tx.send((topic.to_string(), Arc::clone(&event)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Abonnement — callbacks (API principale)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// S'abonne à un topic et appelle `handler` à chaque événement de type `T`.
|
||||
///
|
||||
/// Le handler est exécuté dans une tâche Tokio dédiée (fire-and-forget).
|
||||
/// Les événements d'un autre type sont ignorés silencieusement.
|
||||
/// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.on::<User>("user-connected", |user| {
|
||||
/// println!("Connecté : {:?}", user);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on<T, F>(&self, topic: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(T) + Send + Sync + 'static,
|
||||
{
|
||||
let mut rx = self.get_or_create_sender(topic).subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
handler(typed.clone());
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// S'abonne à un topic et appelle un handler **async** à chaque événement de type `T`.
|
||||
///
|
||||
/// Parfait pour effectuer des opérations async dans le handler
|
||||
/// (requête DB, appel HTTP, broadcast WebSocket…).
|
||||
/// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
|
||||
/// println!("(async) Connecté : {:?}", user);
|
||||
/// // Ici tu peux faire du async : requête DB, HTTP, etc.
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on_async<T, F, Fut>(&self, topic: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(T) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let mut rx = self.get_or_create_sender(topic).subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
handler(typed.clone()).await;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// S'abonne à tous les topics correspondant à un pattern glob.
|
||||
///
|
||||
/// Le handler reçoit `(topic, valeur)` — le nom du topic est inclus pour
|
||||
/// distinguer `user-created` de `user-deleted` par exemple.
|
||||
///
|
||||
/// **Aucun pré-enregistrement nécessaire** : les topics futurs sont couverts.
|
||||
/// Supporte la syntaxe glob : `*` (toute séquence), `?` (un caractère),
|
||||
/// `[abc]` (classe de caractères).
|
||||
///
|
||||
/// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.on_pattern::<User, _>("user-*", |topic, user| {
|
||||
/// match topic.as_str() {
|
||||
/// "user-created" => println!("Créé : {:?}", user),
|
||||
/// "user-deleted" => println!("Supprimé : {:?}", user),
|
||||
/// other => println!("{}: {:?}", other, user),
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-created", User { name: "Alice".into() });
|
||||
/// bus.emit("user-deleted", User { name: "Bob".into() });
|
||||
/// ```
|
||||
pub fn on_pattern<T, F>(&self, pattern: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(String, T) + Send + Sync + 'static,
|
||||
{
|
||||
let glob = Pattern::new(pattern).expect("pattern glob invalide");
|
||||
let (tx, mut rx) = broadcast::channel(self.capacity);
|
||||
self.patterns.write().push((glob, tx));
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok((topic, evt)) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
handler(topic, typed.clone());
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// S'abonne à tous les topics correspondant à un pattern glob, avec un handler **async**.
|
||||
///
|
||||
/// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.on_pattern_async::<User, _, _>("user-*", |topic, user| async move {
|
||||
/// match topic.as_str() {
|
||||
/// "user-created" => println!("(async) Créé : {:?}", user),
|
||||
/// "user-deleted" => println!("(async) Supprimé : {:?}", user),
|
||||
/// other => println!("(async) {}: {:?}", other, user),
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-created", User { name: "Alice".into() });
|
||||
/// ```
|
||||
pub fn on_pattern_async<T, F, Fut>(&self, pattern: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(String, T) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let glob = Pattern::new(pattern).expect("pattern glob invalide");
|
||||
let (tx, mut rx) = broadcast::channel(self.capacity);
|
||||
self.patterns.write().push((glob, tx));
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok((topic, evt)) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
handler(topic, typed.clone()).await;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Abonnement — accès bas niveau (cas avancés)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Retourne un receiver brut ([`AnyEvent`]) pour gérer toi-même la boucle.
|
||||
///
|
||||
/// Utile avec la macro [`match_event!`][crate::match_event] pour gérer
|
||||
/// plusieurs types différents sur un même topic.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # use oxspeak_server_lib::match_event;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let mut rx = bus.on_raw("user-connected");
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
///
|
||||
/// if let Ok(evt) = rx.recv().await {
|
||||
/// match_event!(evt,
|
||||
/// User => |u| println!("User: {:?}", u),
|
||||
/// UdpMetric => |m| println!("Metric: {:?}", m),
|
||||
/// );
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn on_raw(&self, topic: &str) -> broadcast::Receiver<AnyEvent> {
|
||||
self.get_or_create_sender(topic).subscribe()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Utilitaires
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Retourne la liste des topics actuellement enregistrés.
|
||||
pub fn topics(&self) -> Vec<String> {
|
||||
self.channels.read().keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventBus {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
//! # Event Bus
|
||||
//!
|
||||
//! Un bus d'événements asynchrone permettant de faire transiter des messages typés
|
||||
//! entre plusieurs modules, sans couplage direct.
|
||||
//!
|
||||
//! ## Caractéristiques
|
||||
//!
|
||||
//! - **Association clé → événement** : chaque topic (`&str`) est indépendant
|
||||
//! - **Sans restriction de type** : n'importe quel `T: Any + Send + Sync + Clone`
|
||||
//! - **Wake-up ciblé** : seuls les abonnés du bon topic sont réveillés
|
||||
//! - **API callback** : style JavaScript — `bus.on("topic", |payload| { ... })`
|
||||
//! - **Pattern glob** : `on_pattern("user-*", |topic, payload| { ... })`
|
||||
//! - **Handlers async** : `on_async` et `on_pattern_async`
|
||||
//!
|
||||
//! ## Exemple — callback sync
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::sync::Arc;
|
||||
//! use oxspeak_server_lib::event_bus::EventBus;
|
||||
//!
|
||||
//! #[derive(Clone, Debug)]
|
||||
//! struct User { name: String }
|
||||
//!
|
||||
//! # tokio_test::block_on(async {
|
||||
//! let bus = Arc::new(EventBus::new());
|
||||
//!
|
||||
//! bus.on::<User>("user-connected", |user| {
|
||||
//! println!("Connecté : {:?}", user);
|
||||
//! });
|
||||
//!
|
||||
//! bus.emit("user-connected", User { name: "Alice".into() });
|
||||
//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
//! # });
|
||||
//! ```
|
||||
//!
|
||||
//! ## Exemple — callback async
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::sync::Arc;
|
||||
//! use oxspeak_server_lib::event_bus::EventBus;
|
||||
//!
|
||||
//! #[derive(Clone, Debug)]
|
||||
//! struct User { name: String }
|
||||
//!
|
||||
//! # tokio_test::block_on(async {
|
||||
//! let bus = Arc::new(EventBus::new());
|
||||
//!
|
||||
//! bus.on_async::<User, _, _>("user-connected", |user| async move {
|
||||
//! println!("(async) Connecté : {:?}", user);
|
||||
//! });
|
||||
//!
|
||||
//! bus.emit("user-connected", User { name: "Bob".into() });
|
||||
//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
//! # });
|
||||
//! ```
|
||||
//!
|
||||
//! ## Exemple — pattern glob (topic inclus dans le callback)
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::sync::Arc;
|
||||
//! use oxspeak_server_lib::event_bus::EventBus;
|
||||
//!
|
||||
//! #[derive(Clone, Debug)]
|
||||
//! struct User { name: String }
|
||||
//!
|
||||
//! # tokio_test::block_on(async {
|
||||
//! let bus = Arc::new(EventBus::new());
|
||||
//!
|
||||
//! bus.on_pattern::<User, _>("user-*", |topic, user| {
|
||||
//! match topic.as_str() {
|
||||
//! "user-created" => println!("Créé : {:?}", user),
|
||||
//! "user-deleted" => println!("Supprimé : {:?}", user),
|
||||
//! other => println!("{}: {:?}", other, user),
|
||||
//! }
|
||||
//! });
|
||||
//!
|
||||
//! bus.emit("user-created", User { name: "Alice".into() });
|
||||
//! bus.emit("user-deleted", User { name: "Bob".into() });
|
||||
//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
//! # });
|
||||
//! ```
|
||||
//!
|
||||
//! ## Exemple — multi-types avec `match_event!` (cas avancé)
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::sync::Arc;
|
||||
//! use oxspeak_server_lib::event_bus::EventBus;
|
||||
//! use oxspeak_server_lib::match_event;
|
||||
//!
|
||||
//! #[derive(Clone, Debug)] struct User { name: String }
|
||||
//! #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
|
||||
//!
|
||||
//! # tokio_test::block_on(async {
|
||||
//! let bus = Arc::new(EventBus::new());
|
||||
//! let mut rx = bus.on_raw("mixed-topic");
|
||||
//!
|
||||
//! bus.emit("mixed-topic", User { name: "Alice".into() });
|
||||
//!
|
||||
//! if let Ok(evt) = rx.recv().await {
|
||||
//! match_event!(evt,
|
||||
//! User => |u| println!("User: {:?}", u),
|
||||
//! UdpMetric => |m| println!("Metric: {:?}", m),
|
||||
//! );
|
||||
//! }
|
||||
//! # });
|
||||
//! ```
|
||||
|
||||
mod bus;
|
||||
|
||||
// Réexports publics
|
||||
pub use bus::{AnyEvent, EventBus};
|
||||
|
||||
/// Downcaste un [`AnyEvent`] vers un ou plusieurs types concrets et exécute
|
||||
/// la closure correspondante si le type correspond.
|
||||
///
|
||||
/// Les branches non correspondantes sont ignorées silencieusement.
|
||||
///
|
||||
/// # Syntaxe
|
||||
/// ```text
|
||||
/// match_event!(evt, Type1 => |val| { ... }, Type2 => |val| { ... })
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # use oxspeak_server_lib::match_event;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let mut rx = bus.on_raw("user-connected");
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
///
|
||||
/// if let Ok(evt) = rx.recv().await {
|
||||
/// match_event!(evt,
|
||||
/// User => |u| println!("Utilisateur : {:?}", u),
|
||||
/// UdpMetric => |m| println!("Metric : {:?}", m),
|
||||
/// );
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! match_event {
|
||||
($evt:expr, $($type:ty => $handler:expr),+ $(,)?) => {
|
||||
$(
|
||||
if let Some(val) = ($evt).downcast_ref::<$type>() {
|
||||
($handler)(val.clone());
|
||||
} else
|
||||
)+
|
||||
{
|
||||
// Aucun type ne correspond → on ignore silencieusement
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct User {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct UdpMetric {
|
||||
value: f32,
|
||||
}
|
||||
|
||||
// ── on (callback sync) ────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_callback_sync() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let received = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&received);
|
||||
|
||||
bus.on::<User, _>("user-connected", move |user| {
|
||||
if user.name == "Alice" {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Alice".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(received.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_targeted_wakeup() {
|
||||
// Émettre sur "user-connected" ne doit pas réveiller "udp-metrics-updated"
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let metric_called = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&metric_called);
|
||||
|
||||
bus.on::<UdpMetric, _>("udp-metrics-updated", move |_| {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Carol".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(!metric_called.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_type_mismatch_ignored() {
|
||||
// Émettre un UdpMetric sur un topic écouté en User → handler pas appelé
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let called = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&called);
|
||||
|
||||
bus.on::<User, _>("mixed-topic", move |_| {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
bus.emit("mixed-topic", UdpMetric { value: 1.0 });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(!called.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_multiple_subscribers_same_topic() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let count = Arc::new(AtomicU32::new(0));
|
||||
|
||||
for _ in 0..3 {
|
||||
let c = Arc::clone(&count);
|
||||
bus.on::<User, _>("user-connected", move |_| {
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Grace".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert_eq!(count.load(Ordering::SeqCst), 3);
|
||||
}
|
||||
|
||||
// ── on_async ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_async_callback() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let received = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&received);
|
||||
|
||||
bus.on_async::<User, _, _>("user-connected", move |user| {
|
||||
let f = Arc::clone(&flag);
|
||||
async move {
|
||||
if user.name == "Async" {
|
||||
f.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Async".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(received.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ── on_pattern ────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_pattern_callback_receives_topic_and_payload() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let created = Arc::new(AtomicBool::new(false));
|
||||
let deleted = Arc::new(AtomicBool::new(false));
|
||||
let c = Arc::clone(&created);
|
||||
let d = Arc::clone(&deleted);
|
||||
|
||||
bus.on_pattern::<User, _>("user-*", move |topic, user| match topic.as_str() {
|
||||
"user-created" if user.name == "Dave" => c.store(true, Ordering::SeqCst),
|
||||
"user-deleted" if user.name == "Eve" => d.store(true, Ordering::SeqCst),
|
||||
_ => {}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-created",
|
||||
User {
|
||||
name: "Dave".into(),
|
||||
},
|
||||
);
|
||||
bus.emit("user-deleted", User { name: "Eve".into() });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
|
||||
|
||||
assert!(created.load(Ordering::SeqCst));
|
||||
assert!(deleted.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_pattern_does_not_match_other_topics() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let called = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&called);
|
||||
|
||||
bus.on_pattern::<User, _>("user-*", move |_, _| {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"server-created",
|
||||
User {
|
||||
name: "Ghost".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(!called.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_pattern_no_pre_registration_needed() {
|
||||
// Le pattern est enregistré avant que le topic n'existe
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let received = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&received);
|
||||
|
||||
bus.on_pattern::<User, _>("user-*", move |topic, user| {
|
||||
if topic == "user-new-topic" && user.name == "Frank" {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-new-topic",
|
||||
User {
|
||||
name: "Frank".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(received.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ── on_pattern_async ──────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_pattern_async_callback() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let received = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&received);
|
||||
|
||||
bus.on_pattern_async::<User, _, _>("user-*", move |topic, user| {
|
||||
let f = Arc::clone(&flag);
|
||||
async move {
|
||||
if topic == "user-created" && user.name == "Hank" {
|
||||
f.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-created",
|
||||
User {
|
||||
name: "Hank".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(received.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ── on_raw + match_event! ─────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_raw_and_match_event_macro() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let mut rx = bus.on_raw("user-connected");
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Frank".into(),
|
||||
},
|
||||
);
|
||||
|
||||
let evt = rx.recv().await.unwrap();
|
||||
let mut received_name = String::new();
|
||||
match_event!(evt,
|
||||
User => |u: User| { received_name = u.name.clone(); },
|
||||
UdpMetric => |_m: UdpMetric| { panic!("mauvais type"); }
|
||||
);
|
||||
assert_eq!(received_name, "Frank");
|
||||
}
|
||||
|
||||
// ── Utilitaires ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_topics_list() {
|
||||
let bus = EventBus::new();
|
||||
// on_raw enregistre le canal (get_or_create)
|
||||
let _rx1 = bus.on_raw("user-connected");
|
||||
let _rx2 = bus.on_raw("udp-metrics-updated");
|
||||
|
||||
let mut topics = bus.topics();
|
||||
topics.sort();
|
||||
assert_eq!(topics, vec!["udp-metrics-updated", "user-connected"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_emit_multiple_types_same_bus() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let user_ok = Arc::new(AtomicBool::new(false));
|
||||
let metric_ok = Arc::new(AtomicBool::new(false));
|
||||
let u = Arc::clone(&user_ok);
|
||||
let m = Arc::clone(&metric_ok);
|
||||
|
||||
bus.on::<User, _>("user-connected", move |user| {
|
||||
if user.name == "Bob" {
|
||||
u.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
bus.on::<UdpMetric, _>("udp-metrics-updated", move |metric| {
|
||||
if (metric.value - 3.14).abs() < 0.001 {
|
||||
m.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit("user-connected", User { name: "Bob".into() });
|
||||
bus.emit("udp-metrics-updated", UdpMetric { value: 3.14 });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(user_ok.load(Ordering::SeqCst));
|
||||
assert!(metric_ok.load(Ordering::SeqCst));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
/// Type brut d'un événement : pointeur atomique vers n'importe quelle valeur.
|
||||
pub type AnyEvent = Arc<dyn Any + Send + Sync>;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Erreurs
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Erreurs possibles lors d'un `recv().await`.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RecvError {
|
||||
#[error("le canal est fermé")]
|
||||
Closed,
|
||||
#[error("messages perdus (lag) : {0} ignorés")]
|
||||
Lagged(u64),
|
||||
}
|
||||
|
||||
/// Erreurs possibles lors d'un `try_recv()`.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TryRecvError {
|
||||
#[error("aucun message disponible")]
|
||||
Empty,
|
||||
#[error("le canal est fermé")]
|
||||
Closed,
|
||||
#[error("messages perdus (lag) : {0} ignorés")]
|
||||
Lagged(u64),
|
||||
}
|
||||
|
||||
impl From<broadcast::error::RecvError> for RecvError {
|
||||
fn from(e: broadcast::error::RecvError) -> Self {
|
||||
match e {
|
||||
broadcast::error::RecvError::Closed => RecvError::Closed,
|
||||
broadcast::error::RecvError::Lagged(n) => RecvError::Lagged(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<broadcast::error::TryRecvError> for TryRecvError {
|
||||
fn from(e: broadcast::error::TryRecvError) -> Self {
|
||||
match e {
|
||||
broadcast::error::TryRecvError::Empty => TryRecvError::Empty,
|
||||
broadcast::error::TryRecvError::Closed => TryRecvError::Closed,
|
||||
broadcast::error::TryRecvError::Lagged(n) => TryRecvError::Lagged(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TypedReceiver<T>
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Receiver typé pour un topic précis.
|
||||
///
|
||||
/// `recv().await` retourne directement un `T` — le downcast est automatique.
|
||||
/// Les événements d'un type différent sont ignorés silencieusement.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let mut rx = bus.on::<User>("user-connected");
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
/// let user = rx.recv().await.unwrap();
|
||||
/// println!("{:?}", user);
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct TypedReceiver<T> {
|
||||
pub(crate) inner: broadcast::Receiver<AnyEvent>,
|
||||
pub(crate) _marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> TypedReceiver<T>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
{
|
||||
pub(crate) fn new(inner: broadcast::Receiver<AnyEvent>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attend le prochain événement de type `T` sur ce topic.
|
||||
/// Les événements d'un autre type sont ignorés silencieusement.
|
||||
pub async fn recv(&mut self) -> Result<T, RecvError> {
|
||||
loop {
|
||||
match self.inner.recv().await {
|
||||
Ok(evt) => {
|
||||
if let Some(val) = evt.downcast_ref::<T>() {
|
||||
return Ok(val.clone());
|
||||
}
|
||||
// Mauvais type → on ignore et on attend le suivant
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Version non-bloquante.
|
||||
pub fn try_recv(&mut self) -> Result<T, TryRecvError> {
|
||||
loop {
|
||||
match self.inner.try_recv() {
|
||||
Ok(evt) => {
|
||||
if let Some(val) = evt.downcast_ref::<T>() {
|
||||
return Ok(val.clone());
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::TryRecvError::Empty) => return Err(TryRecvError::Empty),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accès au receiver brut sous-jacent.
|
||||
pub fn into_inner(self) -> broadcast::Receiver<AnyEvent> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// PatternReceiver<T>
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Receiver unique pour un pattern wildcard (ex: `"user-*"`).
|
||||
///
|
||||
/// Un seul receiver reçoit les événements de **tous** les topics correspondants,
|
||||
/// passés et futurs. `recv().await` retourne `(nom_du_topic, valeur)`.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let mut rx = bus.on_pattern::<User>("user-*");
|
||||
///
|
||||
/// bus.emit("user-created", User { name: "Alice".into() });
|
||||
/// bus.emit("user-deleted", User { name: "Bob".into() });
|
||||
///
|
||||
/// let (topic, user) = rx.recv().await.unwrap();
|
||||
/// println!("{}: {:?}", topic, user);
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct PatternReceiver<T> {
|
||||
pub(crate) inner: broadcast::Receiver<(String, AnyEvent)>,
|
||||
pub(crate) _marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> PatternReceiver<T>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
{
|
||||
pub(crate) fn new(inner: broadcast::Receiver<(String, AnyEvent)>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attend le prochain événement de type `T` sur n'importe quel topic du pattern.
|
||||
/// Retourne `(nom_du_topic, valeur)`.
|
||||
/// Les événements d'un type différent sont ignorés silencieusement.
|
||||
pub async fn recv(&mut self) -> Result<(String, T), RecvError> {
|
||||
loop {
|
||||
match self.inner.recv().await {
|
||||
Ok((topic, evt)) => {
|
||||
if let Some(val) = evt.downcast_ref::<T>() {
|
||||
return Ok((topic, val.clone()));
|
||||
}
|
||||
// Mauvais type → on ignore et on attend le suivant
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Version non-bloquante.
|
||||
pub fn try_recv(&mut self) -> Result<(String, T), TryRecvError> {
|
||||
loop {
|
||||
match self.inner.try_recv() {
|
||||
Ok((topic, evt)) => {
|
||||
if let Some(val) = evt.downcast_ref::<T>() {
|
||||
return Ok((topic, val.clone()));
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::TryRecvError::Empty) => return Err(TryRecvError::Empty),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/// Vérifie si `topic` correspond au `pattern` avec support des wildcards.
|
||||
///
|
||||
/// - `*` correspond à n'importe quelle séquence de caractères (y compris vide)
|
||||
/// - `?` correspond à exactement un caractère quelconque
|
||||
///
|
||||
/// # Exemples
|
||||
/// ```
|
||||
/// use oxspeak_server_lib::event_bus::wildcard_match;
|
||||
///
|
||||
/// assert!(wildcard_match("user-*", "user-connected"));
|
||||
/// assert!(wildcard_match("user-*", "user-created"));
|
||||
/// assert!(!wildcard_match("user-*", "udp-metrics-updated"));
|
||||
/// assert!(wildcard_match("*-updated", "udp-metrics-updated"));
|
||||
/// assert!(wildcard_match("user-?", "user-a"));
|
||||
/// assert!(!wildcard_match("user-?", "user-ab"));
|
||||
/// ```
|
||||
pub fn wildcard_match(pattern: &str, topic: &str) -> bool {
|
||||
let p: Vec<char> = pattern.chars().collect();
|
||||
let t: Vec<char> = topic.chars().collect();
|
||||
wildcard_match_inner(&p, &t)
|
||||
}
|
||||
|
||||
fn wildcard_match_inner(pattern: &[char], topic: &[char]) -> bool {
|
||||
match (pattern.first(), topic.first()) {
|
||||
(None, None) => true,
|
||||
(Some(&'*'), _) => {
|
||||
// '*' peut correspondre à 0 ou plusieurs caractères
|
||||
wildcard_match_inner(&pattern[1..], topic)
|
||||
|| (!topic.is_empty() && wildcard_match_inner(pattern, &topic[1..]))
|
||||
}
|
||||
(Some(&'?'), Some(_)) => wildcard_match_inner(&pattern[1..], &topic[1..]),
|
||||
(Some(p), Some(t)) if p == t => wildcard_match_inner(&pattern[1..], &topic[1..]),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_wildcard_exact() {
|
||||
assert!(wildcard_match("user-connected", "user-connected"));
|
||||
assert!(!wildcard_match("user-connected", "user-created"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard_star() {
|
||||
assert!(wildcard_match("user-*", "user-connected"));
|
||||
assert!(wildcard_match("user-*", "user-created"));
|
||||
assert!(wildcard_match("user-*", "user-deleted"));
|
||||
assert!(!wildcard_match("user-*", "udp-metrics-updated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard_question_mark() {
|
||||
assert!(wildcard_match("user-?", "user-a"));
|
||||
assert!(!wildcard_match("user-?", "user-ab"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard_star_anywhere() {
|
||||
assert!(wildcard_match("*-updated", "udp-metrics-updated"));
|
||||
assert!(wildcard_match("*metrics*", "udp-metrics-updated"));
|
||||
}
|
||||
}
|
||||
@@ -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.38"
|
||||
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
|
||||
]
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[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"))
|
||||
.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()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("default_permissions"))
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.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("position"))
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.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()),
|
||||
)
|
||||
// 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"))
|
||||
.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()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("default_permissions"))
|
||||
.big_integer()
|
||||
.null(),
|
||||
)
|
||||
// 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("password")).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("pub_key"))
|
||||
.text()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.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()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("is_superuser"))
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.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"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("updated_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.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"))
|
||||
.timestamp_with_time_zone()
|
||||
.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"))
|
||||
.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()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("is_admin"))
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("is_owner"))
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("permissions"))
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
// 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("permissions"))
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("joined_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.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?;
|
||||
|
||||
// Create table `group`
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Alias::new("group"))
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new("id").uuid().primary_key().not_null())
|
||||
.col(ColumnDef::new("server_id").uuid().not_null())
|
||||
.col(ColumnDef::new("name").string().not_null())
|
||||
.col(
|
||||
ColumnDef::new("permissions")
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new("is_default")
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new("created_at")
|
||||
.timestamp_with_time_zone()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_group_server")
|
||||
.from(Alias::new("group"), Alias::new("server_id"))
|
||||
.to(Alias::new("server"), Alias::new("id"))
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create table `group_member`
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Alias::new("group_member"))
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new("group_id").uuid().not_null())
|
||||
.col(ColumnDef::new("user_id").uuid().not_null())
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.col(Alias::new("group_id"))
|
||||
.col(Alias::new("user_id")),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_group_member_group")
|
||||
.from(Alias::new("group_member"), Alias::new("group_id"))
|
||||
.to(Alias::new("group"), Alias::new("id"))
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_group_member_user")
|
||||
.from(Alias::new("group_member"), Alias::new("user_id"))
|
||||
.to(Alias::new("user"), Alias::new("id"))
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index: idx_group_server_id
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_group_server_id")
|
||||
.table(Alias::new("group"))
|
||||
.col(Alias::new("server_id"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index: idx_group_member_user_id
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_group_member_user_id")
|
||||
.table(Alias::new("group_member"))
|
||||
.col(Alias::new("user_id"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Alias::new("group_member")).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Alias::new("group")).to_owned())
|
||||
.await?;
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
use config::{Config, ConfigError, File, FileFormat};
|
||||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::path::Path;
|
||||
use std::{fmt, fs};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppConfigError {
|
||||
Io(std::io::Error),
|
||||
Config(ConfigError),
|
||||
}
|
||||
|
||||
impl fmt::Display for AppConfigError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(err) => write!(f, "failed to access config file: {err}"),
|
||||
Self::Config(err) => write!(f, "failed to load config: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AppConfigError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(err) => Some(err),
|
||||
Self::Config(err) => Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AppConfigError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Self::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for AppConfigError {
|
||||
fn from(err: ConfigError) -> Self {
|
||||
Self::Config(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_CONFIG_TOML: &str = r#"[network]
|
||||
# IP address to bind to
|
||||
host = "0.0.0.0"
|
||||
# hostv6 = "::"
|
||||
# TCP and UDP port can be the same
|
||||
# HTTP port
|
||||
tcp_port = 8080
|
||||
# Voice/Video port
|
||||
udp_port = 8080
|
||||
|
||||
[database]
|
||||
# DSN for database
|
||||
# SQLite
|
||||
url = "sqlite://oxspeak.db"
|
||||
# PostgreSQL
|
||||
# url = "postgresql://user:passwd@localhost:5432/db_name"
|
||||
# MySQL
|
||||
# url = "mysql://user:passwd@localhost:3306/db_name"
|
||||
|
||||
[jwt]
|
||||
secret = "changeme"
|
||||
# Duration in seconds
|
||||
duration = 86400 # 1 day
|
||||
refresh_duration = 1296000 # 15 days
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub network: NetworkConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub jwt: JwtConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct NetworkConfig {
|
||||
pub host: Ipv4Addr,
|
||||
pub hostv6: Option<Ipv6Addr>,
|
||||
pub tcp_port: u16,
|
||||
pub udp_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct DatabaseConfig {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl fmt::Debug for DatabaseConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("DatabaseConfig")
|
||||
.field("url", &"<hidden>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct JwtConfig {
|
||||
pub secret: String,
|
||||
pub duration: u64,
|
||||
pub refresh_duration: u64,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn file_exists() -> bool {
|
||||
Path::new("config.toml").exists()
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self, AppConfigError> {
|
||||
let settings = Config::builder()
|
||||
.add_source(File::new("config.toml", FileFormat::Toml))
|
||||
.build()?;
|
||||
|
||||
Ok(settings.try_deserialize()?)
|
||||
}
|
||||
|
||||
pub fn gen_config() -> Result<(), AppConfigError> {
|
||||
if !Self::file_exists() {
|
||||
fs::write("config.toml", DEFAULT_CONFIG_TOML)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_or_generate() -> Result<Self, AppConfigError> {
|
||||
if !Self::file_exists() {
|
||||
Self::gen_config()?;
|
||||
tracing::info!(config_path = "config.toml", "Generated");
|
||||
}
|
||||
|
||||
Self::load()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod database;
|
||||
|
||||
pub use database::Database;
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod models;
|
||||
pub mod udp;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use oxspeak_server_lib::config::AppConfig;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Enable tracing
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
std::env::var("RUST_LOG")
|
||||
.unwrap_or_else(|_| "info,sqlx=debug,sea_orm=debug,sea_orm_migration=info".into()),
|
||||
)
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_thread_ids(false)
|
||||
.with_thread_names(false)
|
||||
.init();
|
||||
|
||||
// Load or generate config
|
||||
if !AppConfig::file_exists() {
|
||||
AppConfig::gen_config()?;
|
||||
tracing::info!(config_path = "config.toml", "Generated");
|
||||
tracing::info!("Config generated, please edit config.toml");
|
||||
return Ok(())
|
||||
}
|
||||
let config = AppConfig::load()?;
|
||||
tracing::info!(?config, "Loaded config");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -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: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! `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 position: i32,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! `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;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
|
||||
)]
|
||||
#[sea_orm(rs_type = "i32", db_type = "Integer")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChannelType {
|
||||
#[sea_orm(num_value = 0)]
|
||||
Text,
|
||||
#[sea_orm(num_value = 1)]
|
||||
Voice,
|
||||
#[sea_orm(num_value = 3)]
|
||||
DM,
|
||||
}
|
||||
|
||||
#[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: ChannelType,
|
||||
pub name: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub default_permissions: Option<u64>,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "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 permissions: u64,
|
||||
pub joined_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! `SeaORM` Entity.
|
||||
|
||||
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 = "group")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
pub permissions: u64,
|
||||
pub is_default: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[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(has_many = "super::group_member::Entity")]
|
||||
GroupMember,
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Server.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::group_member::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::GroupMember.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! `SeaORM` Entity.
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "group_member")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub group_id: Uuid,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::group::Entity",
|
||||
from = "Column::GroupId",
|
||||
to = "super::group::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Group,
|
||||
#[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::group::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Group.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -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: DateTimeUtc,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
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::now_v7()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! `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 group;
|
||||
pub mod group_member;
|
||||
pub mod message;
|
||||
pub mod server;
|
||||
pub mod server_user;
|
||||
pub mod user;
|
||||
@@ -0,0 +1,12 @@
|
||||
//! `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::group::Entity as Group;
|
||||
pub use super::group_member::Entity as GroupMember;
|
||||
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;
|
||||
@@ -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 = "server")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub default_permissions: u64,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! `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: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub is_admin: bool,
|
||||
pub is_owner: bool,
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//! `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,
|
||||
pub password: String,
|
||||
#[sea_orm(column_type = "Text", unique)]
|
||||
pub pub_key: String,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub is_superuser: bool,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Métrologie du serveur UDP.
|
||||
//!
|
||||
//! Ce module expose :
|
||||
//! - [`UdpMetrics`] : compteurs atomiques lock-free (pas de contention dans la
|
||||
//! boucle de routage).
|
||||
//! - [`UdpMetricsSnapshot`] : lecture cohérente de tous les compteurs à un
|
||||
//! instant T, utilisable pour calculer des deltas.
|
||||
//! - [`UdpRates`] : taux moyens par seconde calculés entre deux snapshots.
|
||||
//! - [`spawn_reporter`] : tâche tokio de reporting périodique via `tracing`.
|
||||
//!
|
||||
//! # Métriques collectées
|
||||
//!
|
||||
//! | Compteur | Description |
|
||||
//! |--------------------|-----------------------------------------------|
|
||||
//! | `packets_received` | Datagrammes reçus |
|
||||
//! | `bytes_received` | Octets reçus (payload uniquement) |
|
||||
//! | `packets_sent` | Datagrammes retransmis vers des abonnés |
|
||||
//! | `bytes_sent` | Octets retransmis |
|
||||
//! | `packets_dropped` | Paquets ignorés (canal sans abonnés) |
|
||||
//! | `send_errors` | Échecs `send_to` |
|
||||
//! | `recv_errors` | Échecs `recv_from` (avant erreur fatale) |
|
||||
//!
|
||||
//! Chaque métrique est également disponible en taux moyen par seconde via
|
||||
//! [`UdpMetricsSnapshot::rates_since`].
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// ── Compteurs ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compteurs atomiques du serveur UDP.
|
||||
///
|
||||
/// Partagé via [`Arc`] entre la boucle de routage et le reporter périodique.
|
||||
/// Tous les accès utilisent [`Ordering::Relaxed`] : on accepte que les lectures
|
||||
/// voient des valeurs légèrement décalées entre compteurs (suffisant pour de la
|
||||
/// métrologie), ce qui évite tout overhead de synchronisation.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UdpMetrics {
|
||||
/// Nombre total de datagrammes reçus.
|
||||
pub packets_received: AtomicU64,
|
||||
/// Volume total d'octets reçus (payload des datagrammes).
|
||||
pub bytes_received: AtomicU64,
|
||||
/// Nombre total de datagrammes retransmis (somme sur tous les abonnés).
|
||||
pub packets_sent: AtomicU64,
|
||||
/// Volume total d'octets retransmis.
|
||||
pub bytes_sent: AtomicU64,
|
||||
/// Paquets ignorés car le canal ne possède aucun abonné.
|
||||
pub packets_dropped: AtomicU64,
|
||||
/// Nombre d'erreurs `send_to` (non fatales).
|
||||
pub send_errors: AtomicU64,
|
||||
/// Nombre d'erreurs `recv_from` enregistrées avant arrêt du serveur.
|
||||
pub recv_errors: AtomicU64,
|
||||
}
|
||||
|
||||
impl UdpMetrics {
|
||||
/// Crée un jeu de métriques vide enroulé dans un [`Arc`].
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self::default())
|
||||
}
|
||||
|
||||
/// Enregistre la réception d'un datagramme de `bytes` octets.
|
||||
#[inline]
|
||||
pub fn inc_received(&self, bytes: u64) {
|
||||
self.packets_received.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_received.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Enregistre l'émission d'un datagramme de `bytes` octets vers un client.
|
||||
#[inline]
|
||||
pub fn inc_sent(&self, bytes: u64) {
|
||||
self.packets_sent.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_sent.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Enregistre un paquet ignoré (canal sans abonnés).
|
||||
#[inline]
|
||||
pub fn inc_dropped(&self) {
|
||||
self.packets_dropped.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Enregistre un échec `send_to` non fatal.
|
||||
#[inline]
|
||||
pub fn inc_send_error(&self) {
|
||||
self.send_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Enregistre un échec `recv_from`.
|
||||
#[inline]
|
||||
pub fn inc_recv_error(&self) {
|
||||
self.recv_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Prend un instantané cohérent de tous les compteurs.
|
||||
pub fn snapshot(&self) -> UdpMetricsSnapshot {
|
||||
UdpMetricsSnapshot {
|
||||
packets_received: self.packets_received.load(Ordering::Relaxed),
|
||||
bytes_received: self.bytes_received.load(Ordering::Relaxed),
|
||||
packets_sent: self.packets_sent.load(Ordering::Relaxed),
|
||||
bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
|
||||
packets_dropped: self.packets_dropped.load(Ordering::Relaxed),
|
||||
send_errors: self.send_errors.load(Ordering::Relaxed),
|
||||
recv_errors: self.recv_errors.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Snapshot ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Lecture cohérente de l'ensemble des compteurs à un instant T.
|
||||
///
|
||||
/// Permet de calculer des deltas et des taux entre deux points dans le temps
|
||||
/// sans bloquer la boucle de routage.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct UdpMetricsSnapshot {
|
||||
pub packets_received: u64,
|
||||
pub bytes_received: u64,
|
||||
pub packets_sent: u64,
|
||||
pub bytes_sent: u64,
|
||||
pub packets_dropped: u64,
|
||||
pub send_errors: u64,
|
||||
pub recv_errors: u64,
|
||||
}
|
||||
|
||||
impl UdpMetricsSnapshot {
|
||||
/// Calcule les taux moyens par seconde depuis un snapshot précédent.
|
||||
///
|
||||
/// `elapsed` est la durée réelle écoulée entre les deux snapshots.
|
||||
pub fn rates_since(&self, previous: &Self, elapsed: Duration) -> UdpRates {
|
||||
// On évite la division par zéro si l'interval est infinitésimal.
|
||||
let secs = elapsed.as_secs_f64().max(f64::EPSILON);
|
||||
|
||||
UdpRates {
|
||||
packets_received_per_sec: self
|
||||
.packets_received
|
||||
.saturating_sub(previous.packets_received)
|
||||
as f64
|
||||
/ secs,
|
||||
bytes_received_per_sec: self.bytes_received.saturating_sub(previous.bytes_received)
|
||||
as f64
|
||||
/ secs,
|
||||
packets_sent_per_sec: self.packets_sent.saturating_sub(previous.packets_sent) as f64
|
||||
/ secs,
|
||||
bytes_sent_per_sec: self.bytes_sent.saturating_sub(previous.bytes_sent) as f64 / secs,
|
||||
packets_dropped_per_sec: self
|
||||
.packets_dropped
|
||||
.saturating_sub(previous.packets_dropped)
|
||||
as f64
|
||||
/ secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Taux ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Taux moyens par seconde calculés entre deux [`UdpMetricsSnapshot`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct UdpRates {
|
||||
/// Paquets reçus par seconde.
|
||||
pub packets_received_per_sec: f64,
|
||||
/// Octets reçus par seconde.
|
||||
pub bytes_received_per_sec: f64,
|
||||
/// Paquets envoyés par seconde.
|
||||
pub packets_sent_per_sec: f64,
|
||||
/// Octets envoyés par seconde.
|
||||
pub bytes_sent_per_sec: f64,
|
||||
/// Paquets ignorés par seconde.
|
||||
pub packets_dropped_per_sec: f64,
|
||||
}
|
||||
|
||||
// ── Reporter périodique ───────────────────────────────────────────────────────
|
||||
|
||||
/// Lance une tâche Tokio qui logue les métriques toutes les `interval`.
|
||||
///
|
||||
/// Chaque rapport inclut les compteurs cumulatifs **et** les taux moyens sur
|
||||
/// la fenêtre écoulée depuis le rapport précédent.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```no_run
|
||||
/// use std::time::Duration;
|
||||
/// use std::sync::Arc;
|
||||
/// use oxspeak_server_lib::udp::metrics::{UdpMetrics, spawn_reporter};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let metrics = UdpMetrics::new();
|
||||
/// spawn_reporter(Arc::clone(&metrics), Duration::from_secs(5));
|
||||
/// }
|
||||
/// ```
|
||||
pub fn spawn_reporter(metrics: Arc<UdpMetrics>, interval: Duration) {
|
||||
tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(interval);
|
||||
// Le premier tick est immédiat ; on le consomme pour démarrer à t=0.
|
||||
ticker.tick().await;
|
||||
|
||||
let mut prev_snapshot = metrics.snapshot();
|
||||
let mut prev_instant = Instant::now();
|
||||
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
|
||||
let now = Instant::now();
|
||||
let current = metrics.snapshot();
|
||||
let elapsed = now.duration_since(prev_instant);
|
||||
let rates = current.rates_since(&prev_snapshot, elapsed);
|
||||
|
||||
tracing::info!(
|
||||
// ── Cumulatifs ──
|
||||
pkts_rx = current.packets_received,
|
||||
bytes_rx = current.bytes_received,
|
||||
pkts_tx = current.packets_sent,
|
||||
bytes_tx = current.bytes_sent,
|
||||
pkts_dropped = current.packets_dropped,
|
||||
send_errors = current.send_errors,
|
||||
recv_errors = current.recv_errors,
|
||||
// ── Taux / s ──
|
||||
pkts_rx_s = format!("{:.1}", rates.packets_received_per_sec),
|
||||
bytes_rx_s = format!("{:.0}", rates.bytes_received_per_sec),
|
||||
pkts_tx_s = format!("{:.1}", rates.packets_sent_per_sec),
|
||||
bytes_tx_s = format!("{:.0}", rates.bytes_sent_per_sec),
|
||||
pkts_dropped_s = format!("{:.1}", rates.packets_dropped_per_sec),
|
||||
interval_ms = elapsed.as_millis(),
|
||||
"UDP metrics"
|
||||
);
|
||||
|
||||
prev_snapshot = current;
|
||||
prev_instant = now;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod metrics;
|
||||
pub mod router;
|
||||
pub mod server;
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Identifiant d'un canal de routage (ex: room ID, channel name…).
|
||||
pub type ChannelId = String;
|
||||
|
||||
/// Table de routage UDP.
|
||||
///
|
||||
/// Associe un identifiant de canal à la liste des clients (adresses IP/port)
|
||||
/// actuellement abonnés à ce canal. Le serveur UDP utilise cette table pour
|
||||
/// décider où retransmettre les paquets entrants.
|
||||
///
|
||||
/// # Note future
|
||||
/// Ce type est intentionnellement simple pour démarrer. La prochaine étape
|
||||
/// sera d'y associer une logique de dispatch (ex: forwarding sélectif,
|
||||
/// authentification du client, etc.).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RoutingTable {
|
||||
channels: HashMap<ChannelId, Vec<SocketAddr>>,
|
||||
}
|
||||
|
||||
impl RoutingTable {
|
||||
/// Crée une table de routage vide.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Inscrit un client dans un canal.
|
||||
///
|
||||
/// Si le canal n'existe pas encore, il est créé automatiquement.
|
||||
/// Si le client est déjà inscrit dans ce canal, l'appel est sans effet.
|
||||
pub fn join(&mut self, channel: impl Into<ChannelId>, client: SocketAddr) {
|
||||
let clients = self.channels.entry(channel.into()).or_default();
|
||||
if !clients.contains(&client) {
|
||||
clients.push(client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retire un client d'un canal.
|
||||
///
|
||||
/// Si le canal devient vide après le retrait, il est supprimé de la table.
|
||||
pub fn leave(&mut self, channel: &str, client: &SocketAddr) {
|
||||
if let Some(clients) = self.channels.get_mut(channel) {
|
||||
clients.retain(|c| c != client);
|
||||
if clients.is_empty() {
|
||||
self.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne la liste des clients abonnés à un canal, ou `None` si le
|
||||
/// canal n'existe pas.
|
||||
pub fn subscribers(&self, channel: &str) -> Option<&[SocketAddr]> {
|
||||
self.channels.get(channel).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Retourne tous les canaux connus et leurs abonnés.
|
||||
pub fn channels(&self) -> &HashMap<ChannelId, Vec<SocketAddr>> {
|
||||
&self.channels
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::metrics::UdpMetrics;
|
||||
use super::router::RoutingTable;
|
||||
use crate::config::NetworkConfig;
|
||||
|
||||
/// Taille du buffer de lecture pour chaque datagramme UDP entrant.
|
||||
///
|
||||
/// La RFC 768 limite les datagrammes UDP à 65 507 octets (payload max avec
|
||||
/// en-têtes IP+UDP). Pour de la voix/vidéo compressée, les paquets réels
|
||||
/// seront bien plus petits, mais on alloue le maximum une seule fois pour
|
||||
/// éviter toute troncature silencieuse.
|
||||
const UDP_READ_BUFFER_SIZE: usize = 65_507;
|
||||
|
||||
/// Erreurs pouvant survenir pendant l'opération du serveur UDP.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum UdpServerError {
|
||||
#[error("failed to bind UDP socket to {addr}: {source}")]
|
||||
Bind {
|
||||
addr: SocketAddr,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Serveur UDP asynchrone agissant comme routeur de paquets.
|
||||
///
|
||||
/// Reçoit des datagrammes entrants et les route vers les clients inscrits
|
||||
/// dans les canaux correspondants via une [`RoutingTable`].
|
||||
///
|
||||
/// La configuration réseau est fournie par [`NetworkConfig`] (issue de
|
||||
/// [`AppConfig`][crate::config::AppConfig]), qui centralise la lecture du
|
||||
/// fichier TOML.
|
||||
///
|
||||
/// Les métriques sont collectées dans un [`UdpMetrics`] partageable via
|
||||
/// [`Arc`] — passez-le à [`metrics::spawn_reporter`][super::metrics::spawn_reporter]
|
||||
/// pour un reporting périodique automatique.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```no_run
|
||||
/// use std::time::Duration;
|
||||
/// use oxspeak_server_lib::config::{AppConfig, NetworkConfig};
|
||||
/// use oxspeak_server_lib::udp::server::UdpServer;
|
||||
/// use oxspeak_server_lib::udp::metrics::{UdpMetrics, spawn_reporter};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let config = AppConfig::load().unwrap();
|
||||
/// let metrics = UdpMetrics::new();
|
||||
/// spawn_reporter(metrics.clone(), Duration::from_secs(10));
|
||||
/// let (server, _shutdown_tx) = UdpServer::new(config.network, metrics);
|
||||
/// server.run().await.unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct UdpServer {
|
||||
bind_addr: SocketAddr,
|
||||
routing_table: RoutingTable,
|
||||
metrics: Arc<UdpMetrics>,
|
||||
shutdown_rx: broadcast::Receiver<()>,
|
||||
}
|
||||
|
||||
impl UdpServer {
|
||||
/// Crée un nouveau [`UdpServer`] depuis la configuration réseau globale.
|
||||
///
|
||||
/// `metrics` est un [`Arc`] vers le jeu de métriques à alimenter.
|
||||
/// Conservez-en un clone si vous souhaitez l'observer depuis l'extérieur
|
||||
/// (reporter, API de santé, etc.).
|
||||
///
|
||||
/// Retourne le serveur et un [`broadcast::Sender`] pour déclencher le
|
||||
/// shutdown gracieux.
|
||||
pub fn new(network: NetworkConfig, metrics: Arc<UdpMetrics>) -> (Self, broadcast::Sender<()>) {
|
||||
let bind_addr = SocketAddr::new(network.host.into(), network.udp_port);
|
||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
|
||||
(
|
||||
Self {
|
||||
bind_addr,
|
||||
routing_table: RoutingTable::new(),
|
||||
metrics,
|
||||
shutdown_rx,
|
||||
},
|
||||
shutdown_tx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Expose la table de routage de façon mutable pour y inscrire des
|
||||
/// clients avant ou pendant l'exécution (via partage d'état ou messages).
|
||||
pub fn routing_table_mut(&mut self) -> &mut RoutingTable {
|
||||
&mut self.routing_table
|
||||
}
|
||||
|
||||
/// Retourne une référence aux métriques du serveur.
|
||||
pub fn metrics(&self) -> &Arc<UdpMetrics> {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
/// Bind le socket et démarre la boucle de routage.
|
||||
///
|
||||
/// Pour chaque datagramme reçu, le paquet est retransmis inline (sans
|
||||
/// spawn de tâche) vers tous les clients abonnés au canal identifié.
|
||||
/// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une
|
||||
/// erreur I/O fatale survient.
|
||||
pub async fn run(mut self) -> Result<(), UdpServerError> {
|
||||
let socket =
|
||||
UdpSocket::bind(self.bind_addr)
|
||||
.await
|
||||
.map_err(|source| UdpServerError::Bind {
|
||||
addr: self.bind_addr,
|
||||
source,
|
||||
})?;
|
||||
|
||||
tracing::info!(addr = %self.bind_addr, "UDP server listening");
|
||||
|
||||
let mut buf = vec![0u8; UDP_READ_BUFFER_SIZE];
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = socket.recv_from(&mut buf) => {
|
||||
match result {
|
||||
Ok((len, peer)) => {
|
||||
self.metrics.inc_received(len as u64);
|
||||
self.route_packet(&socket, &buf[..len], peer).await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.metrics.inc_recv_error();
|
||||
tracing::error!(%err, "recv_from failed");
|
||||
return Err(UdpServerError::Io(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = self.shutdown_rx.recv() => {
|
||||
tracing::info!("UDP server shutting down");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Route un paquet entrant vers les abonnés du canal correspondant.
|
||||
///
|
||||
/// # Logique actuelle (placeholder)
|
||||
/// En l'absence de protocole applicatif défini, tous les paquets reçus
|
||||
/// sont logués. Branche ici la logique d'identification du canal
|
||||
/// (ex: lire un header de paquet pour extraire le `channel_id`).
|
||||
async fn route_packet(&self, socket: &UdpSocket, data: &[u8], sender: SocketAddr) {
|
||||
tracing::debug!(%sender, bytes = data.len(), "datagram received");
|
||||
|
||||
// TODO: extraire le channel_id depuis le header du paquet applicatif.
|
||||
// Pour l'instant on utilise un canal de démonstration statique.
|
||||
let channel_id = "default";
|
||||
|
||||
match self.routing_table.subscribers(channel_id) {
|
||||
Some(clients) => {
|
||||
for &client in clients {
|
||||
// Ne pas renvoyer au sender lui-même.
|
||||
if client == sender {
|
||||
continue;
|
||||
}
|
||||
match socket.send_to(data, client).await {
|
||||
Ok(_) => {
|
||||
self.metrics.inc_sent(data.len() as u64);
|
||||
}
|
||||
Err(err) => {
|
||||
self.metrics.inc_send_error();
|
||||
tracing::warn!(%client, %err, "failed to forward packet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.metrics.inc_dropped();
|
||||
tracing::debug!(%sender, channel = channel_id, "no subscribers, packet dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user