import net from 'net'; import fs from 'fs'; import path from 'path'; const DEFAULT_CONFIG = { host: 'localhost', port: 1235 }; function loadBridgeConfig() { try { const cfgPath = path.resolve(process.cwd(), 'backend/config/chatBridge.json'); if (fs.existsSync(cfgPath)) { const raw = fs.readFileSync(cfgPath, 'utf-8'); const json = JSON.parse(raw); return { ...DEFAULT_CONFIG, ...json }; } } catch (e) { console.warn('Failed to load chatBridge.json, using defaults:', e.message); } return { ...DEFAULT_CONFIG }; } export default class ChatTcpBridge { constructor(ioSocket, user) { this.ioSocket = ioSocket; this.user = user || {}; this.token = null; this.tcp = null; this.buffer = ''; this.config = loadBridgeConfig(); this.pending = []; this.objStart = -1; this.depthCurly = 0; this.depthSquare = 0; this.inString = false; this.escapeNext = false; } connect(initialRoomName = '') { if (this.tcp) return; const { host, port } = this.config; this.ioEmitStatus('connecting', `${host}:${port}`); console.log(`[ChatBridge] Connecting to ${host}:${port}`); this.tcp = new net.Socket(); this.tcp.on('connect', () => { this.ioEmitStatus('connected', `${host}:${port}`); console.log(`[ChatBridge] Connected to ${host}:${port}`); // Erstnachricht: init const initPayload = { type: 'init', name: this.user.username || '', room: initialRoomName || '', }; this.sendRaw(initPayload); }); this.tcp.on('data', (data) => { this.processChunk(data.toString('utf-8')); }); this.tcp.on('error', (err) => { this.ioEmitStatus('error', err.message); console.error('[ChatBridge] Error:', err.message); }); this.tcp.on('close', () => { this.ioEmitStatus('disconnected'); console.warn('[ChatBridge] Disconnected'); }); this.tcp.connect(port, host); } handleIncomingLine(line) { try { const obj = JSON.parse(line); this.handleIncoming(obj); } catch (e) { this.ioSocket.emit('chat:incoming', { type: 'system', text: line }); } } processChunk(chunk) { if (!chunk) return; for (let i = 0; i < chunk.length; i++) { const ch = chunk[i]; this.buffer += ch; if (this.escapeNext) { this.escapeNext = false; continue; } if (this.inString) { if (ch === '\\') { this.escapeNext = true; } else if (ch === '"') { this.inString = false; } continue; } if (ch === '"') { this.inString = true; continue; } if (ch === '{') { this.depthCurly++; if (this.objStart === -1) this.objStart = this.buffer.length - 1; } else if (ch === '}') { this.depthCurly--; } else if (ch === '[') { this.depthSquare++; } else if (ch === ']') { this.depthSquare--; } // Wenn ein komplettes Objekt abgeschlossen ist (außerhalb von Strings und beide Tiefen 0) if (this.objStart !== -1 && this.depthCurly === 0) { const jsonStr = this.buffer.slice(this.objStart, this.buffer.length); // Alles vor objStart (falls vorhanden) rausfiltern const prefix = this.buffer.slice(0, this.objStart); if (prefix.trim()) { // Unerwarteter Text, als System ausgeben this.ioSocket.emit('chat:incoming', { type: 'system', text: prefix.trim() }); } this.buffer = ''; this.objStart = -1; this.parseAndHandle(jsonStr); } } } parseAndHandle(jsonStr) { try { const obj = JSON.parse(jsonStr); // Forward raw parsed inbound message to browser console this.ioSocket.emit('chat:debug', { dir: 'in', payload: obj }); this.handleIncoming(obj); } catch (e) { // Wenn mehrere Objekte direkt aneinander hängen (}{), versuchen sie zu splitten const parts = jsonStr.split('}{').map((p, idx, arr) => (idx === 0 ? p + '}' : (idx === arr.length - 1 ? '{' + p : '{' + p + '}'))); if (parts.length > 1) { parts.forEach(p => { try { const o = JSON.parse(p); this.ioSocket.emit('chat:debug', { dir: 'in', payload: o }); this.handleIncoming(o); } catch (_) { this.ioSocket.emit('chat:debug', { dir: 'in', raw: p }); this.ioSocket.emit('chat:incoming', { type: 'system', text: p }); } }); } else { this.ioSocket.emit('chat:debug', { dir: 'in', raw: jsonStr }); this.ioSocket.emit('chat:incoming', { type: 'system', text: jsonStr }); } } } handleIncoming(obj) { if (!obj) return; // Map numeric type codes if (typeof obj.type === 'number') { switch (obj.type) { case 1: // token if (typeof obj.message === 'string' && obj.message) { this.token = obj.message; this.ioSocket.emit('chat:incoming', { type: 'system', text: 'Token received' }); const actions = this.pending; this.pending = []; actions.forEach(fn => { try { fn(); } catch (_) { } }); return; } break; case 3: // room list if (Array.isArray(obj.message)) { const names = obj.message.map(r => r.name).filter(Boolean).join(', '); this.ioSocket.emit('chat:incoming', { type: 'system', text: names ? `Rooms: ${names}` : 'Rooms updated' }); return; } break; case 5: { // generic server event (room entered, color changes, etc.) const msg = obj.message; if (!msg) break; if (typeof msg === 'string') { if (msg === 'room_entered') { const to = obj.to || obj.name || obj.room || ''; this.ioSocket.emit('chat:incoming', { type: 'system', code: 'room_entered', tr: 'room_entered', to }); return; } if (msg === 'color_changed' || msg === 'user_color_changed') { this.ioSocket.emit('chat:incoming', { type: 'system', code: msg, color: obj.color, userName: obj.userName || '' }); return; } // unknown message string; fallthrough to text echo this.ioSocket.emit('chat:incoming', { type: 'system', text: msg }); return; } if (typeof msg === 'object') { const tr = msg.tr || 'room_entered'; const to = msg.to || msg.name || msg.room || ''; this.ioSocket.emit('chat:incoming', { type: 'system', code: tr, tr, to }); return; } break; } case 6: { // scream const userName = obj.userName || obj.user || obj.name || ''; const message = obj.message || ''; this.ioSocket.emit('chat:incoming', { type: 'scream', userName, message, color: obj.color || null }); return; } default: break; } } if (obj.type === 'token' && obj.message) { this.token = obj.message; this.ioSocket.emit('chat:incoming', { type: 'system', text: 'Token received' }); // Ausstehende Aktionen jetzt mit Token senden const actions = this.pending; this.pending = []; actions.forEach(fn => { try { fn(); } catch (_) { } }); return; } // Normalize generic messages if (typeof obj.message === 'string' && obj.message && (obj.userName || obj.user || obj.name)) { this.ioSocket.emit('chat:incoming', { type: 'message', message: obj.message, userName: obj.userName || obj.user || obj.name, color: obj.color || null }); return; } // As system fallback this.ioSocket.emit('chat:incoming', { type: 'system', text: JSON.stringify(obj) }); } sendRaw(obj) { if (!this.tcp) return; try { const str = JSON.stringify(obj) + '\n'; // Forward outbound payload to browser console this.ioSocket.emit('chat:debug', { dir: 'out', payload: obj }); this.tcp.write(str); } catch (e) { this.ioEmitStatus('error', e.message); } } sendWithToken(obj) { if (this.token) obj.token = this.token; this.sendRaw(obj); } withTokenOrQueue(fn) { if (this.token) { fn(); } else { this.pending.push(fn); } } joinRoom(roomName, password = '') { // Prefer keys name/room if server expects them for room switch; fallback to newroom if required elsewhere this.withTokenOrQueue(() => this.sendWithToken({ type: 'join', room: roomName, name: this.user.username || '', password })); } sendMessage(text) { this.withTokenOrQueue(() => this.sendWithToken({ type: 'message', message: text, userName: this.user.username })); } scream(text) { this.withTokenOrQueue(() => this.sendWithToken({ type: 'scream', message: text })); } doAction(text) { this.withTokenOrQueue(() => this.sendWithToken({ type: 'do', message: text })); } dice(expr) { this.withTokenOrQueue(() => this.sendWithToken({ type: 'dice', message: expr || '' })); } color(hex) { let value = typeof hex === 'string' ? hex.trim() : ''; if (!value) return; if (!value.startsWith('#')) value = '#' + value; // Normalize 3-digit to 6-digit when possible const m3 = /^#([0-9a-fA-F]{3})$/.exec(value); if (m3) { value = '#' + m3[1].split('').map(c => c + c).join(''); } const m6 = /^#([0-9a-fA-F]{6})$/.exec(value); if (!m6) return; this.withTokenOrQueue(() => this.sendWithToken({ type: 'color', value })); } close() { try { this.tcp?.destroy(); } catch (_) { } this.tcp = null; } ioEmitStatus(type, detail) { this.ioSocket.emit('chat:status', { type, detail }); } }