Init
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -934,6 +934,17 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"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]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -954,6 +965,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1650,6 +1662,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"migration",
|
"migration",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
|||||||
@@ -66,3 +66,4 @@ serde_repr = "0.1"
|
|||||||
toml = "0.9"
|
toml = "0.9"
|
||||||
validator = { version = "0.20", features = ["derive"] }
|
validator = { version = "0.20", features = ["derive"] }
|
||||||
uuid = {version = "1", features = ["v4", "v7", "fast-rng", "serde"]}
|
uuid = {version = "1", features = ["v4", "v7", "fast-rng", "serde"]}
|
||||||
|
futures-util = "0.3"
|
||||||
|
|||||||
@@ -17,7 +17,11 @@
|
|||||||
|
|
||||||
<label>
|
<label>
|
||||||
Type :
|
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>
|
</label>
|
||||||
|
|
||||||
<button type="submit" @click="create_channel">Create</button>
|
<button type="submit" @click="create_channel">Create</button>
|
||||||
@@ -33,18 +37,22 @@ let form_inputs = ref({
|
|||||||
name: '',
|
name: '',
|
||||||
server_id: null,
|
server_id: null,
|
||||||
category_id: null,
|
category_id: null,
|
||||||
channel_type: 0,
|
channel_type: 'text', // Changé de 0 à 'text'
|
||||||
position: 0
|
position: 0
|
||||||
})
|
})
|
||||||
// defined methods
|
// defined methods
|
||||||
async function create_channel(){
|
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/", {
|
const response = await fetch("/api/channel/channels/", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(form_inputs.value)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
emit('created', await response.json())
|
emit('created', await response.json())
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h3>server : {{server.name}} ({{server.id}}) <button @click="remove">Remove</button></h3>
|
<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>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@@ -19,6 +23,7 @@ const {server} = defineProps({
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const tree = ref(null)
|
||||||
const emit = defineEmits(['remove'])
|
const emit = defineEmits(['remove'])
|
||||||
|
|
||||||
// defined methods
|
// 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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::app::AppState;
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::event_bus::EventBus;
|
use crate::event_bus::EventBus;
|
||||||
use crate::network::http::HTTPServer;
|
use crate::network::http::{HTTPServer, WSHub};
|
||||||
use crate::network::udp::UDPServer;
|
use crate::network::udp::UDPServer;
|
||||||
use crate::repositories::Repositories;
|
use crate::repositories::Repositories;
|
||||||
|
|
||||||
@@ -25,7 +25,12 @@ impl App {
|
|||||||
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database");
|
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database");
|
||||||
let repositories = Repositories::new(db.get_connection(), event_bus.clone());
|
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 udp_server = UDPServer::new(config.bind_addr());
|
||||||
let http_server = HTTPServer::new(config.bind_addr(), state);
|
let http_server = HTTPServer::new(config.bind_addr(), state);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::event_bus::EventBus;
|
use crate::event_bus::EventBus;
|
||||||
|
use crate::network::http::WSHub;
|
||||||
use crate::repositories::Repositories;
|
use crate::repositories::Repositories;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: Database,
|
pub db: Database,
|
||||||
pub event_bus: EventBus,
|
pub event_bus: EventBus,
|
||||||
pub repositories: Repositories
|
pub repositories: Repositories,
|
||||||
|
pub ws_hub: WSHub,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ mod context;
|
|||||||
pub use server::HTTPServer;
|
pub use server::HTTPServer;
|
||||||
pub use error::HTTPError;
|
pub use error::HTTPError;
|
||||||
pub use context::RequestContext;
|
pub use context::RequestContext;
|
||||||
|
pub use web::WSHub;
|
||||||
|
|
||||||
pub type AppRouter = Router<AppState>;
|
pub type AppRouter = Router<AppState>;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use uuid::Uuid;
|
|||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
use crate::models::server;
|
use crate::models::server;
|
||||||
use crate::network::http::{AppRouter, HTTPError};
|
use crate::network::http::{AppRouter, HTTPError};
|
||||||
use crate::serializers::ServerSerializer;
|
use crate::serializers::{ServerSerializer, ServerTreeSerializer};
|
||||||
|
|
||||||
pub fn setup_route() -> AppRouter {
|
pub fn setup_route() -> AppRouter {
|
||||||
AppRouter::new()
|
AppRouter::new()
|
||||||
@@ -17,6 +17,7 @@ pub fn setup_route() -> AppRouter {
|
|||||||
.route("/servers/{id}/", put(server_update))
|
.route("/servers/{id}/", put(server_update))
|
||||||
.route("/servers/{id}/", delete(server_delete))
|
.route("/servers/{id}/", delete(server_delete))
|
||||||
.route("/servers/{id}/password/", get(server_password))
|
.route("/servers/{id}/password/", get(server_password))
|
||||||
|
.route("/servers/{id}/tree/", get(tree))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn server_list(
|
pub async fn server_list(
|
||||||
@@ -87,3 +88,12 @@ pub async fn server_password(
|
|||||||
|
|
||||||
Ok(Json(server.password))
|
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;
|
use crate::network::http::AppRouter;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod hub;
|
||||||
|
|
||||||
|
pub use hub::WSHub;
|
||||||
|
|
||||||
pub fn setup_route() -> AppRouter {
|
pub fn setup_route() -> AppRouter {
|
||||||
AppRouter::new()
|
AppRouter::new()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod category;
|
|||||||
mod channel;
|
mod channel;
|
||||||
mod message;
|
mod message;
|
||||||
mod user;
|
mod user;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepositoryContext {
|
pub struct RepositoryContext {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use std::sync::Arc;
|
|||||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait, QueryFilter, ColumnTrait, QueryOrder};
|
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait, QueryFilter, ColumnTrait, QueryOrder};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::models::{category, channel, server};
|
use crate::models::{category, channel, server};
|
||||||
use crate::repositories::RepositoryContext;
|
use super::RepositoryContext;
|
||||||
|
use super::types::{ServerExplorerItem, ServerTree};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerRepository {
|
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
|
// Helpers
|
||||||
impl ServerRepository {
|
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
|
// 1. Récupération des catégories avec leurs channels
|
||||||
let categories_with_channels = category::Entity::find()
|
let categories_with_channels = category::Entity::find()
|
||||||
.filter(category::Column::ServerId.eq(server_id))
|
.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 sea_orm::ActiveValue::Set;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::models::server;
|
use crate::models::server;
|
||||||
|
use crate::repositories::types::{ServerExplorerItem, ServerTree};
|
||||||
|
use super::{CategorySerializer, ChannelSerializer};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ServerSerializer {
|
pub struct ServerSerializer {
|
||||||
@@ -52,3 +54,38 @@ impl ServerSerializer {
|
|||||||
active_model
|
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