diff --git a/connection-manager.js b/connection-manager.js index 5a7c66f..5c44f9c 100644 --- a/connection-manager.js +++ b/connection-manager.js @@ -1,61 +1,67 @@ /** - * Gestionnaire de connexion avec fallback SignalR → WebSocket - * Essaie d'abord SignalR, puis bascule sur WebSocket si nécessaire + * Gestionnaire de connexion avec fallback SignalR → REST + Socket.IO + * + * 1. Essaie d'abord SignalR (serveur .NET legacy) + * 2. Si SignalR échoue, bascule sur REST + Socket.IO (serveur Python) */ const signalR = require('@microsoft/signalr'); -const WebSocketAdapter = require('./websocket-adapter'); +const RestSocketAdapter = require('./rest-socket-adapter'); class ConnectionManager { constructor(serverUrl, options = {}) { this.serverUrl = serverUrl; this.options = options; this.connection = null; - this.isUsingWebSocketFallback = false; - this.connectionType = 'none'; + this.connectionType = 'none'; // 'none', 'SignalR', 'REST+SocketIO' } /** - * Établit la connexion en essayant SignalR puis WebSocket + * Établit la connexion en essayant SignalR puis REST + Socket.IO */ async connect() { console.log('🔌 Tentative de connexion au serveur...'); - - // 1. Essayer SignalR d'abord + + // 1. Essayer SignalR d'abord (serveur .NET) try { console.log('📡 Essai avec SignalR...'); - this.connection = await this.createSignalRConnection(); + this.connection = this._createSignalRConnection(); await this.connection.start(); this.connectionType = 'SignalR'; - this.isUsingWebSocketFallback = false; - console.log('✅ Connexion SignalR établie'); + console.log('✅ Connexion SignalR établie (serveur .NET)'); return this.connection; } catch (signalRError) { console.warn('⚠️ SignalR indisponible:', signalRError.message); - console.log('🔄 Basculement vers WebSocket...'); + console.log('🔄 Basculement vers REST + Socket.IO...'); } - // 2. Fallback vers WebSocket + // 2. Fallback vers REST + Socket.IO (serveur Python) try { - this.connection = new WebSocketAdapter(this.serverUrl, this.options); + this.connection = new RestSocketAdapter(this.serverUrl, this.options); await this.connection.start(); - this.connectionType = 'WebSocket'; - this.isUsingWebSocketFallback = true; - console.log('✅ Connexion WebSocket établie (mode fallback)'); + this.connectionType = 'REST+SocketIO'; + console.log('✅ Connexion REST + Socket.IO établie (serveur Python)'); return this.connection; - } catch (wsError) { - console.error('❌ Impossible de se connecter (SignalR et WebSocket ont échoué)'); - throw wsError; + } catch (restError) { + console.error('❌ REST + Socket.IO indisponible:', restError.message); + throw new Error('Impossible de se connecter (SignalR et REST+SocketIO ont échoué)'); } } /** * Crée une connexion SignalR */ - createSignalRConnection() { - const url = `http://${this.serverUrl}/signalR`; - console.log(`Creating SignalR connection to: ${url}`); - + _createSignalRConnection() { + let url = this.serverUrl; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = `http://${url}`; + } + if (!url.includes('/signalR')) { + url = `${url}/signalR`; + } + + console.log(`📡 URL SignalR: ${url}`); + return new signalR.HubConnectionBuilder() .withUrl(url) .withAutomaticReconnect([0, 2000, 5000, 10000]) @@ -87,7 +93,8 @@ class ConnectionManager { getConnectionInfo() { return { type: this.connectionType, - isWebSocketFallback: this.isUsingWebSocketFallback, + isSignalR: this.connectionType === 'SignalR', + isRestSocketIO: this.connectionType === 'REST+SocketIO', isConnected: this.connection && this.connection.state === 'Connected', serverUrl: this.serverUrl }; @@ -112,6 +119,15 @@ class ConnectionManager { } this.connection.on(eventName, handler); } + + /** + * Méthode helper pour retirer un handler d'événement + */ + off(eventName) { + if (this.connection && this.connection.off) { + this.connection.off(eventName); + } + } } -module.exports = ConnectionManager; \ No newline at end of file +module.exports = ConnectionManager; diff --git a/main.js b/main.js index 8f14520..798a6b9 100644 --- a/main.js +++ b/main.js @@ -317,7 +317,8 @@ async function startSignalRConnection() { console.log(`Connexion établie via ${connectionInfo.type}`); logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, { connectionType: connectionInfo.type, - isWebSocketFallback: connectionInfo.isWebSocketFallback, + isSignalR: connectionInfo.isSignalR, + isRestSocketIO: connectionInfo.isRestSocketIO, status: 'connected', serverUrl: connectionInfo.serverUrl }); diff --git a/rest-socket-adapter.js b/rest-socket-adapter.js new file mode 100644 index 0000000..7567726 --- /dev/null +++ b/rest-socket-adapter.js @@ -0,0 +1,285 @@ +/** + * Adaptateur REST + Socket.IO pour le serveur Python + * + * Utilise REST pour les actions (login, logout, terminaux) + * et Socket.IO pour les événements temps réel (IpbxEvent) + * + * Émule l'API SignalR pour compatibilité avec le code existant + */ + +const io = require('socket.io-client'); + +class RestSocketAdapter { + constructor(serverUrl, options = {}) { + // Nettoyer l'URL + this.serverUrl = serverUrl; + if (this.serverUrl.includes('/signalR')) { + this.serverUrl = this.serverUrl.replace('/signalR', ''); + } + if (!this.serverUrl.startsWith('http://') && !this.serverUrl.startsWith('https://')) { + this.serverUrl = `http://${this.serverUrl}`; + } + + this.socket = null; + this.connected = false; + this.handlers = new Map(); + this.options = options; + + // Token JWT pour les requêtes authentifiées + this.authToken = null; + this.currentAgent = null; + } + + /** + * Émule connection.start() de SignalR + * Établit la connexion Socket.IO pour les événements + */ + async start() { + return new Promise((resolve, reject) => { + console.log(`[RestSocketAdapter] Connexion à ${this.serverUrl}...`); + + // Vérifier d'abord que le serveur Python répond + fetch(`${this.serverUrl}/health`) + .then(response => { + if (!response.ok) { + throw new Error(`Health check failed: ${response.status}`); + } + return response.json(); + }) + .then(health => { + console.log(`[RestSocketAdapter] Serveur Python détecté:`, health); + + // Connecter Socket.IO pour les événements temps réel + this.socket = io(this.serverUrl, { + path: '/socket.io/', + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 2000, + ...this.options + }); + + this.socket.on('connect', () => { + console.log('[RestSocketAdapter] Socket.IO connecté'); + this.connected = true; + resolve(); + }); + + this.socket.on('connect_error', (error) => { + console.error('[RestSocketAdapter] Erreur Socket.IO:', error.message); + // On reste connecté même si Socket.IO échoue (REST fonctionne) + if (!this.connected) { + this.connected = true; + console.log('[RestSocketAdapter] Mode REST seul (Socket.IO indisponible)'); + resolve(); + } + }); + + this.socket.on('disconnect', (reason) => { + console.log('[RestSocketAdapter] Socket.IO déconnecté:', reason); + }); + + // Configurer les handlers d'événements existants + this.handlers.forEach((handler, eventName) => { + this.socket.on(eventName, (...args) => { + console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args); + handler(eventName, args); + }); + }); + + // Timeout de connexion + setTimeout(() => { + if (!this.connected) { + reject(new Error('Timeout de connexion au serveur Python')); + } + }, 10000); + }) + .catch(error => { + console.error('[RestSocketAdapter] Serveur Python non disponible:', error.message); + reject(error); + }); + }); + } + + /** + * Émule connection.stop() de SignalR + */ + async stop() { + if (this.socket) { + this.socket.disconnect(); + } + this.connected = false; + this.authToken = null; + this.currentAgent = null; + console.log('[RestSocketAdapter] Déconnecté'); + } + + /** + * Émule connection.invoke() de SignalR + * Redirige vers les endpoints REST appropriés + */ + async invoke(methodName, ...args) { + if (!this.connected) { + throw new Error('Non connecté au serveur'); + } + + console.log(`[RestSocketAdapter] invoke: ${methodName}`, args); + + switch (methodName) { + case 'GetTerminalListByServiceProvider': + return this._getTerminalList(args[0]); + + case 'AgentLogin': + return this._agentLogin(args[0], args[1], args[2]); + + case 'AgentLogoff': + return this._agentLogoff(args[0]); + + default: + console.warn(`[RestSocketAdapter] Méthode non implémentée: ${methodName}`); + throw new Error(`Méthode non supportée: ${methodName}`); + } + } + + /** + * GET /api/v1/terminals/{serviceProvider} + */ + async _getTerminalList(serviceProvider) { + const response = await fetch( + `${this.serverUrl}/api/v1/terminals/${encodeURIComponent(serviceProvider)}` + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || 'Erreur récupération terminaux'); + } + + const data = await response.json(); + // Retourner juste la liste des numéros de terminaux (format SignalR) + return data.terminals.map(t => t.number || t); + } + + /** + * POST /api/v1/auth/login + */ + async _agentLogin(email, password, terminal) { + const response = await fetch(`${this.serverUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + access_code: email, + password: password, + terminal: terminal, + force_disconnect: false + }) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || 'Erreur de connexion'); + } + + const data = await response.json(); + + // Stocker le token si présent + if (data.token) { + this.authToken = data.token; + } + this.currentAgent = data; + + // Enregistrer le terminal sur Socket.IO pour recevoir les événements + if (this.socket && this.socket.connected) { + this.socket.emit('register', { + terminal: terminal, + agentId: data.accessCode || data.access_code + }); + } + + // Retourner au format attendu par le client (compatible SignalR) + return { + accessCode: data.accessCode || data.access_code, + firstName: data.firstName || data.first_name, + lastName: data.lastName || data.last_name, + connList: data.connList || data.conn_list || [] + }; + } + + /** + * POST /api/v1/auth/logout + */ + async _agentLogoff(accessCode) { + // Désenregistrer du Socket.IO + if (this.socket && this.socket.connected) { + this.socket.emit('unregister', { terminal: this.currentAgent?.terminal }); + } + + const headers = { + 'Content-Type': 'application/json', + }; + + // Ajouter le token si disponible + if (this.authToken) { + headers['Authorization'] = `Bearer ${this.authToken}`; + } + + const response = await fetch(`${this.serverUrl}/api/v1/auth/logout`, { + method: 'POST', + headers: headers, + body: JSON.stringify({ access_code: accessCode }) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || 'Erreur de déconnexion'); + } + + this.authToken = null; + this.currentAgent = null; + + return { success: true }; + } + + /** + * Émule connection.on() de SignalR + */ + on(eventName, handler) { + console.log(`[RestSocketAdapter] Enregistrement handler: ${eventName}`); + this.handlers.set(eventName, handler); + + // Si Socket.IO est déjà connecté, ajouter le handler + if (this.socket) { + this.socket.on(eventName, (...args) => { + console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args); + handler(eventName, args); + }); + } + } + + /** + * Émule connection.off() de SignalR + */ + off(eventName) { + this.handlers.delete(eventName); + if (this.socket) { + this.socket.off(eventName); + } + } + + /** + * État de la connexion (émule SignalR HubConnectionState) + */ + get state() { + return this.connected ? 'Connected' : 'Disconnected'; + } + + /** + * Identifie ce type d'adaptateur + */ + get isRestSocketAdapter() { + return true; + } +} + +module.exports = RestSocketAdapter;