/** * 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;