This commit is contained in:
2026-01-04 12:38:41 +01:00
parent 96765342d1
commit 1a4b706702
14 changed files with 230 additions and 31 deletions

13
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -17,7 +17,11 @@
<label>
Type :
<input type="number" name="channel_type" v-model="form_inputs.channel_type">
<!-- <input type="number" name="channel_type" v-model="form_inputs.channel_type">-->
<select v-model="form_inputs.channel_type">
<option value="text">Text</option>
<option value="voice">Voice</option>
</select>
</label>
<button type="submit" @click="create_channel">Create</button>
@@ -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())

View File

@@ -2,6 +2,10 @@
<div>
<div>
<h3>server : {{server.name}} ({{server.id}}) <button @click="remove">Remove</button></h3>
<div>
<p>tree :</p>
<pre>{{JSON.stringify(tree, null, 2)}}</pre>
</div>
</div>
<hr>
@@ -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()
})
</script>

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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<AppState>;

View File

@@ -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<AppState>,
Path(id): Path<Uuid>
) -> Result<Json<ServerTreeSerializer>, HTTPError> {
let layout = state.repositories.server.get_tree(id).await?;
Ok(Json(ServerTreeSerializer::from(layout)))
}

102
src/network/http/web/hub.rs Normal file
View File

@@ -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<AppState>
) -> 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::<Message>(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<Message>,
}
/// Gestionnaire global des connexions WebSocket
#[derive(Clone)]
pub struct WSHub {
pub clients: Arc<RwLock<HashMap<Uuid, Vec<ConnectedClient>>>>,
}
impl WSHub {
pub fn new() -> Self {
Self {
clients: Arc::new(RwLock::new(HashMap::new()))
}
}
}

View File

@@ -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()

View File

@@ -12,6 +12,7 @@ mod category;
mod channel;
mod message;
mod user;
pub mod types;
#[derive(Clone)]
pub struct RepositoryContext {

View File

@@ -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::Model>),
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<ServerExplorerItem>,
}
// Helpers
impl ServerRepository {
pub async fn get_channels_tree(&self, server_id: Uuid) -> Result<ServerLayout, DbErr> {
pub async fn get_tree(&self, server_id: Uuid) -> Result<ServerTree, DbErr> {
// 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 })
}
}

20
src/repositories/types.rs Normal file
View File

@@ -0,0 +1,20 @@
use crate::models::{category, channel};
pub enum ServerExplorerItem {
Category(category::Model, Vec<channel::Model>),
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<ServerExplorerItem>,
}

View File

@@ -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
}
}
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TreeItemSerializer {
Category {
#[serde(flatten)]
category: CategorySerializer,
channels: Vec<ChannelSerializer>
},
Channel(ChannelSerializer)
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct ServerTreeSerializer {
pub items: Vec<TreeItemSerializer>,
}
impl From<ServerTree> 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(),
}
}
}