Init
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
102
src/network/http/web/hub.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -12,6 +12,7 @@ mod category;
|
||||
mod channel;
|
||||
mod message;
|
||||
mod user;
|
||||
pub mod types;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryContext {
|
||||
|
||||
@@ -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
20
src/repositories/types.rs
Normal 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>,
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user