Init
This commit is contained in:
+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