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(),
+ }
+ }
+}