diff --git a/Cargo.lock b/Cargo.lock index 31cc624..bbcebc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -934,6 +934,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -954,6 +965,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1650,6 +1662,7 @@ dependencies = [ "axum", "chrono", "env_logger", + "futures-util", "log", "migration", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 4e3d929..349d3dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,3 +66,4 @@ serde_repr = "0.1" toml = "0.9" validator = { version = "0.20", features = ["derive"] } uuid = {version = "1", features = ["v4", "v7", "fast-rng", "serde"]} +futures-util = "0.3" diff --git a/frontend/src/components/channel/channel_create.vue b/frontend/src/components/channel/channel_create.vue index 229f501..aa600db 100644 --- a/frontend/src/components/channel/channel_create.vue +++ b/frontend/src/components/channel/channel_create.vue @@ -17,7 +17,11 @@ @@ -33,18 +37,22 @@ let form_inputs = ref({ name: '', server_id: null, category_id: null, - channel_type: 0, + channel_type: 'text', // Changé de 0 à 'text' position: 0 }) // defined methods async function create_channel(){ + // Petit nettoyage pour éviter d'envoyer "" au lieu de null pour les UUIDs + const payload = { ...form_inputs.value }; + if (!payload.server_id) payload.server_id = null; + if (!payload.category_id) payload.category_id = null; const response = await fetch("/api/channel/channels/", { method: 'POST', headers: { "Content-Type": "application/json", "Accept": "application/json", }, - body: JSON.stringify(form_inputs.value) + body: JSON.stringify(payload) }) if (response.ok) { emit('created', await response.json()) diff --git a/frontend/src/components/server/server_detail.vue b/frontend/src/components/server/server_detail.vue index 3dd5df3..514bdc9 100644 --- a/frontend/src/components/server/server_detail.vue +++ b/frontend/src/components/server/server_detail.vue @@ -2,6 +2,10 @@

server : {{server.name}} ({{server.id}})

+
+

tree :

+
{{JSON.stringify(tree, null, 2)}}
+

@@ -19,6 +23,7 @@ const {server} = defineProps({ required: true } }) +const tree = ref(null) const emit = defineEmits(['remove']) // defined methods @@ -33,8 +38,18 @@ async function remove(){ } } -onMounted(() => { +async function fetch_tree(){ + const response = await fetch(`/api/server/servers/${server.id}/tree/`) + if (response.ok) { + tree.value = await response.json() + console.log(tree) + } else { + console.error("Failed to fetch server tree:", response.statusText) + } +} +onMounted(() => { + fetch_tree() }) diff --git a/src/app/app.rs b/src/app/app.rs index cde9691..180c7e6 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -2,7 +2,7 @@ use crate::app::AppState; use crate::config::Config; use crate::database::Database; use crate::event_bus::EventBus; -use crate::network::http::HTTPServer; +use crate::network::http::{HTTPServer, WSHub}; use crate::network::udp::UDPServer; use crate::repositories::Repositories; @@ -25,7 +25,12 @@ impl App { let db = Database::init(&config.database_url()).await.expect("Failed to initialize database"); let repositories = Repositories::new(db.get_connection(), event_bus.clone()); - let state = AppState{db: db.clone(), event_bus: event_bus.clone(), repositories: repositories.clone()}; + let state = AppState{ + db: db.clone(), + event_bus: event_bus.clone(), + repositories: repositories.clone(), + ws_hub: WSHub::new() + }; let udp_server = UDPServer::new(config.bind_addr()); let http_server = HTTPServer::new(config.bind_addr(), state); diff --git a/src/app/state.rs b/src/app/state.rs index 2cf19e2..f1a5253 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,12 +1,14 @@ use crate::database::Database; use crate::event_bus::EventBus; +use crate::network::http::WSHub; use crate::repositories::Repositories; #[derive(Clone)] pub struct AppState { pub db: Database, pub event_bus: EventBus, - pub repositories: Repositories + pub repositories: Repositories, + pub ws_hub: WSHub, } impl AppState { diff --git a/src/network/http/mod.rs b/src/network/http/mod.rs index 9adb632..91b8d7a 100644 --- a/src/network/http/mod.rs +++ b/src/network/http/mod.rs @@ -11,5 +11,6 @@ mod context; pub use server::HTTPServer; pub use error::HTTPError; pub use context::RequestContext; +pub use web::WSHub; pub type AppRouter = Router; diff --git a/src/network/http/web/api/server.rs b/src/network/http/web/api/server.rs index 247c8f7..ee75d79 100644 --- a/src/network/http/web/api/server.rs +++ b/src/network/http/web/api/server.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::app::AppState; use crate::models::server; use crate::network::http::{AppRouter, HTTPError}; -use crate::serializers::ServerSerializer; +use crate::serializers::{ServerSerializer, ServerTreeSerializer}; pub fn setup_route() -> AppRouter { AppRouter::new() @@ -17,6 +17,7 @@ pub fn setup_route() -> AppRouter { .route("/servers/{id}/", put(server_update)) .route("/servers/{id}/", delete(server_delete)) .route("/servers/{id}/password/", get(server_password)) + .route("/servers/{id}/tree/", get(tree)) } pub async fn server_list( @@ -86,4 +87,13 @@ pub async fn server_password( .ok_or(HTTPError::NotFound)?; Ok(Json(server.password)) +} + +pub async fn tree( + State(state): State, + Path(id): Path +) -> Result, HTTPError> { + let layout = state.repositories.server.get_tree(id).await?; + + Ok(Json(ServerTreeSerializer::from(layout))) } \ No newline at end of file diff --git a/src/network/http/web/hub.rs b/src/network/http/web/hub.rs new file mode 100644 index 0000000..b39c0dd --- /dev/null +++ b/src/network/http/web/hub.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; +use std::sync::Arc; +use axum::extract::{State, WebSocketUpgrade}; +use axum::extract::ws::{Message, WebSocket}; +use axum::response::IntoResponse; +use futures_util::{SinkExt, StreamExt}; +use parking_lot::RwLock; +use tokio::sync::mpsc; +use uuid::Uuid; +use crate::app::AppState; + +async fn ws_handler( + ws: WebSocketUpgrade, + State(app_state): State +) -> impl IntoResponse { + // todo : récupérer le vrai id de l'utilisateur + let user_id = Uuid::new_v4(); + ws.on_upgrade(move |socket| handle_socket(socket, app_state, user_id)) +} + +async fn handle_socket(socket: WebSocket, app_state: AppState, user_id: Uuid) { + // .split() sépare la lecture de l'écriture + let (mut ws_sender, mut ws_receiver) = socket.split(); + let (tx, mut rx) = mpsc::channel::(100); + + // ID unique pour CETTE connexion spécifique (un utilisateur peut en avoir plusieurs) + let connection_id = Uuid::new_v4(); + + // 1. Enregistrement du client (Gestion du Vec) + let client = ConnectedClient { + user_id, + connection_id, + sender: tx, + }; + + { + let mut clients = app_state.ws_hub.clients.write(); + clients.entry(user_id).or_insert_with(Vec::new).push(client); + } + + // 2. Tâche d'envoi (Hub -> Browser) + let mut send_task = tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + if ws_sender.send(msg).await.is_err() { + break; + } + } + }); + + // 3. Tâche de réception (Browser -> Server) + let mut recv_task = tokio::spawn(async move { + while let Some(result) = ws_receiver.next().await { + if let Ok(msg) = result { + if let Message::Close(_) = msg { break; } + // Ici vous pourriez dispatcher les messages vers l'event_bus + } else { + break; + } + } + }); + + // 4. Attente de fin + tokio::select! { + _ = (&mut send_task) => recv_task.abort(), + _ = (&mut recv_task) => send_task.abort(), + }; + + // 5. Nettoyage propre : on retire seulement CETTE connexion + let mut clients = app_state.ws_hub.clients.write(); + if let Some(user_conns) = clients.get_mut(&user_id) { + user_conns.retain(|c| c.connection_id != connection_id); + // Optionnel : si le Vec est vide, on peut supprimer l'entrée pour libérer de la mémoire + if user_conns.is_empty() { + clients.remove(&user_id); + } + } + + println!("Connexion {} de l'utilisateur {} fermée.", connection_id, user_id); +} + +/// Représente une connexion active +pub struct ConnectedClient { + pub user_id: Uuid, + pub connection_id: Uuid, + pub sender: mpsc::Sender, +} + + + +/// Gestionnaire global des connexions WebSocket +#[derive(Clone)] +pub struct WSHub { + pub clients: Arc>>>, +} + +impl WSHub { + pub fn new() -> Self { + Self { + clients: Arc::new(RwLock::new(HashMap::new())) + } + } +} \ No newline at end of file diff --git a/src/network/http/web/mod.rs b/src/network/http/web/mod.rs index 4359625..1795a61 100644 --- a/src/network/http/web/mod.rs +++ b/src/network/http/web/mod.rs @@ -3,7 +3,9 @@ use crate::app::AppState; use crate::network::http::AppRouter; mod api; +mod hub; +pub use hub::WSHub; pub fn setup_route() -> AppRouter { AppRouter::new() diff --git a/src/repositories/mod.rs b/src/repositories/mod.rs index 8dea626..aa6cf21 100644 --- a/src/repositories/mod.rs +++ b/src/repositories/mod.rs @@ -12,6 +12,7 @@ mod category; mod channel; mod message; mod user; +pub mod types; #[derive(Clone)] pub struct RepositoryContext { diff --git a/src/repositories/server.rs b/src/repositories/server.rs index 23dcdab..ef1d2c8 100644 --- a/src/repositories/server.rs +++ b/src/repositories/server.rs @@ -2,7 +2,8 @@ use std::sync::Arc; use sea_orm::{DbErr, EntityTrait, ActiveModelTrait, QueryFilter, ColumnTrait, QueryOrder}; use uuid::Uuid; use crate::models::{category, channel, server}; -use crate::repositories::RepositoryContext; +use super::RepositoryContext; +use super::types::{ServerExplorerItem, ServerTree}; #[derive(Clone)] pub struct ServerRepository { @@ -37,28 +38,9 @@ impl ServerRepository { } } -pub enum ServerExplorerItem { - Category(category::Model, Vec), - Channel(channel::Model), -} - -// Pour pouvoir trier facilement -impl ServerExplorerItem { - fn position(&self) -> i32 { - match self { - ServerExplorerItem::Category(cat, _) => cat.position, - ServerExplorerItem::Channel(chan) => chan.position, - } - } -} - -pub struct ServerLayout { - pub items: Vec, -} - // Helpers impl ServerRepository { - pub async fn get_channels_tree(&self, server_id: Uuid) -> Result { + pub async fn get_tree(&self, server_id: Uuid) -> Result { // 1. Récupération des catégories avec leurs channels let categories_with_channels = category::Entity::find() .filter(category::Column::ServerId.eq(server_id)) @@ -108,6 +90,6 @@ impl ServerRepository { } }); - Ok(ServerLayout { items }) + Ok(ServerTree { items }) } } \ No newline at end of file diff --git a/src/repositories/types.rs b/src/repositories/types.rs new file mode 100644 index 0000000..c976bc7 --- /dev/null +++ b/src/repositories/types.rs @@ -0,0 +1,20 @@ +use crate::models::{category, channel}; + +pub enum ServerExplorerItem { + Category(category::Model, Vec), + Channel(channel::Model), +} + +// Pour pouvoir trier facilement +impl ServerExplorerItem { + pub fn position(&self) -> i32 { + match self { + ServerExplorerItem::Category(cat, _) => cat.position, + ServerExplorerItem::Channel(chan) => chan.position, + } + } +} + +pub struct ServerTree { + pub items: Vec, +} \ No newline at end of file diff --git a/src/serializers/server.rs b/src/serializers/server.rs index 357baf1..cae8d2e 100644 --- a/src/serializers/server.rs +++ b/src/serializers/server.rs @@ -2,6 +2,8 @@ use serde::{Serialize, Deserialize}; use sea_orm::ActiveValue::Set; use uuid::Uuid; use crate::models::server; +use crate::repositories::types::{ServerExplorerItem, ServerTree}; +use super::{CategorySerializer, ChannelSerializer}; #[derive(Serialize, Deserialize)] pub struct ServerSerializer { @@ -51,4 +53,39 @@ impl ServerSerializer { active_model.password = Set(self.password); active_model } -} \ No newline at end of file +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum TreeItemSerializer { + Category { + #[serde(flatten)] + category: CategorySerializer, + channels: Vec + }, + Channel(ChannelSerializer) +} + +#[derive(Serialize)] +#[serde(transparent)] +pub struct ServerTreeSerializer { + pub items: Vec, +} + +impl From for ServerTreeSerializer { + fn from(layout: ServerTree) -> Self { + Self { + items: layout.items.into_iter().map(|item| match item { + ServerExplorerItem::Category(cat, chans) => { + TreeItemSerializer::Category { + category: CategorySerializer::from(cat), + channels: chans.into_iter().map(ChannelSerializer::from).collect(), + } + }, + ServerExplorerItem::Channel(chan) => { + TreeItemSerializer::Channel(ChannelSerializer::from(chan)) + } + }).collect(), + } + } +}