// server.js (Node.js with Express and ws) const express = require('express'); const { WebSocketServer } = require('ws'); const cors = require('cors'); // Import CORS const { initializeApp, cert } = require('firebase-admin/app'); const { getFirestore } = require('firebase-admin/firestore'); var admin = require("firebase-admin"); const serviceAccount = require("./serviceAcc.json"); initializeApp({ credential: cert(serviceAccount) }); const db = getFirestore(); const app = express(); const port = process.env.PORT || 8080; const wss = new WebSocketServer({ noServer: true }); app.use(cors()); initialData = { devices: [], rooms: [], users: [] }; // Provide default empty data on error let currentDeviceStates = {}; // const fetchInitialData = async () => { // const devices = []; // const rooms = []; // const users = []; // try { // const deviceSnapshot = await db.collection('Devices').get(); // deviceSnapshot.forEach(doc => devices.push({ id: doc.id, ...doc.data() })); // const roomsSnapshot = await db.collectionGroup('Rooms').get(); // Get all rooms across hostels // roomsSnapshot.forEach(doc => rooms.push({ id: doc.id, ...doc.data(), hostelId: doc.ref.parent.parent.id })); // Include hostelId // const usersSnapshot = await db.collection('Users').get(); // usersSnapshot.forEach(doc => users.push({ id: doc.id, ...doc.data() })); // initialData = { devices, rooms, users }; // // console.log("Initial data fetched:", initialData); // //for each hostel id mantain a list of rooms with devices pertaining to that hostel // //setall to off // initialData.devices.forEach(device => { // device.state = false; // } ); // initialData.rooms.forEach(room => { // if (!currentDeviceStates[room.id]) { // currentDeviceStates[room.id] = []; // } // const roomDevices = initialData.devices.filter(device => device.hostelId === room.hostelId); // currentDeviceStates[room.id] = { ...room, devices: roomDevices }; // }); // } catch (error) { // console.error("Error fetching initial data:", error); // // return { devices: [], rooms: [], users: [] }; // initialData = { devices, rooms, users }; // } // }; // const fetchInitialData = async () => { // try { // const [deviceSnapshot, roomsSnapshot, usersSnapshot] = await Promise.all([ // db.collection('Devices').get(), // db.collectionGroup('Rooms').get(), // db.collection('Users').get() // ]); // const devices = deviceSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // const rooms = roomsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), hostelId: doc.ref.parent.parent.id })); // const users = usersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // initialData = { devices, rooms, users }; // initialData.devices.forEach(device => { // device.state = false; // }); // initialData.rooms.forEach(room => { // const roomDevices = initialData.devices.filter(device => device.hostelId === room.hostelId); // currentDeviceStates[room.id] = { ...room, devices: roomDevices.map(device => ({...device})) }; // Deep copy to avoid modifying initialData // }); // console.log("Initial data fetched and processed."); // } catch (error) { // console.error("Error fetching initial data:", error); // initialData = { devices: [], rooms: [], users: [] }; // Provide default empty data on error // currentDeviceStates = {}; // } // }; // fetchInitialData().then(() => { // setupFirestoreListeners(); // }); // const updateInitialData = (collectionName, snapshot) => { // const changes = snapshot.docChanges(); // changes.forEach(change => { // const docData = { id: change.doc.id, ...change.doc.data() }; // console.log(change.type, collectionName, "\t"); // var index; // switch (change.type) { // case 'added': // index = initialData[collectionName]?.findIndex(item => item.id === change.doc.id); // if (index !== -1) { // initialData[collectionName][index] = docData; // } // else { // initialData[collectionName].push(docData); // } // console.log("Length of ", collectionName, " is ", initialData[collectionName].length); // console.log("DATA: ",docData); // break; // case 'modified': // index = initialData[collectionName].findIndex(item => item.id === change.doc.id); // if (index !== -1) { // initialData[collectionName][index] = docData; // for (const roomId in currentDeviceStates) { // const roomDevices = currentDeviceStates[roomId].devices; // const deviceIndex = roomDevices.findIndex(dev => dev.id === change.doc.id); // if (deviceIndex !== -1) { // currentDeviceStates[roomId].devices[deviceIndex] = {...docData, state: roomDevices[deviceIndex].state} // } // } // } // break; // case 'removed': // initialData[collectionName] = initialData[collectionName].filter(item => item.id !== change.doc.id); // break; // } // }); // console.log(`Updated ${collectionName} in initialData.\n\n`); // if (collectionName === 'devices' || collectionName === 'rooms') { // rebuildCurrentDeviceStates(); // } // }; // const setupFirestoreListeners = () => { // db.collection('Devices').onSnapshot(snapshot => { // updateInitialData('devices', snapshot); // }); // db.collectionGroup('Rooms').onSnapshot(snapshot => { // updateInitialData('rooms', snapshot); // }); // db.collection('Users').onSnapshot(snapshot => { // updateInitialData('users', snapshot); // }); // } // const rebuildCurrentDeviceStates = () => { // currentDeviceStates = {}; // initialData.rooms.forEach(room => { // const roomDevices = initialData.devices.filter(device => device.hostelId === room.hostelId); // currentDeviceStates[room.id] = { ...room, devices: roomDevices.map(device => ({ ...device})) }; // console.log("Rebuilt room: ", room.roomNumber, " with ", roomDevices.length, " devices.","all Devices", room.hostelId); // }); // console.log("currentDeviceStates rebuilt."); // } // // fetchInitialData().then(() => { // // setupFirestoreListeners(); // // }); // setupFirestoreListeners(); const fetchInitialData = async () => { try { const [deviceSnapshot, roomsSnapshot, usersSnapshot] = await Promise.all([ db.collection('Devices').get(), db.collectionGroup('Rooms').get(), db.collection('Users').get() ]); initialData.devices = deviceSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), state: false })); // Initialize state to false initialData.rooms = roomsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), hostelId: doc.ref.parent.parent.id })); initialData.users = usersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); rebuildCurrentDeviceStates(); // Rebuild after fetching initial data console.log("Initial data fetched and processed."); } catch (error) { console.error("Error fetching initial data:", error); initialData = { devices: [], rooms: [], users: [] }; currentDeviceStates = {}; } }; const updateInitialData = (collectionName, snapshot) => { snapshot.docChanges().forEach(change => { let docData; if (collectionName === 'rooms') { docData = { id: change.doc.id, ...change.doc.data(), hostelId: change.doc.ref.parent.parent.id }; } else { docData = { id: change.doc.id, ...change.doc.data() }; } let index; switch (change.type) { case 'added': console.log('added', collectionName, docData); if (!initialData[collectionName].some(item => item.id === change.doc.id)) { initialData[collectionName].push(docData); if (collectionName === 'devices') { initialData[collectionName][initialData[collectionName].length - 1].state = false; } } break; case 'modified': console.log('modified', collectionName, docData); index = initialData[collectionName].findIndex(item => item.id === change.doc.id); if (index !== -1) { initialData[collectionName][index] = docData; if (collectionName === 'devices') { for (const roomId in currentDeviceStates) { const roomDevices = currentDeviceStates[roomId].devices; const deviceIndex = roomDevices?.findIndex(dev => dev.id === change.doc.id); if (deviceIndex !== -1) { console.log("Device found in room", roomId, "old state", roomDevices[deviceIndex].state, "new state", docData.state); currentDeviceStates[roomId].devices[deviceIndex] = { ...docData, state: roomDevices[deviceIndex].state }; console.log(currentDeviceStates[roomId]); } } } } break; case 'removed': console.log('removed', collectionName, docData); initialData[collectionName] = initialData[collectionName].filter(item => item.id !== change.doc.id); break; } }); console.log(`Updated ${collectionName} in initialData.\n\n`); if (collectionName === 'devices' || collectionName === 'rooms') { rebuildCurrentDeviceStates(); } }; const setupFirestoreListeners = () => { db.collection('Devices').onSnapshot(snapshot => updateInitialData('devices', snapshot)); db.collectionGroup('Rooms').onSnapshot(snapshot => updateInitialData('rooms', snapshot)); db.collection('Users').onSnapshot(snapshot => updateInitialData('users', snapshot)); }; const rebuildCurrentDeviceStates = () => { initialData.rooms.forEach(room => { if (!room.hostelId) { console.warn(`Room ${room.id} is missing hostelId`); return; } const roomDevices = initialData.devices.filter(device => device.hostelId === room.hostelId); if (!currentDeviceStates[room.id]) { currentDeviceStates[room.id] = { ...room, devices: roomDevices.map(device => ({ ...device })) }; } else { currentDeviceStates[room.id] = { ...room, devices: roomDevices.map(device => ({ ...device, state: currentDeviceStates[room.id].devices.find(dev => dev.id === device.id)?.state || false, })), }; } }); console.log("currentDeviceStates rebuilt."); broadcastRoomData(); }; const broadcastRoomData = () => { for (const roomId in connectedClients) { connectedClients[roomId].forEach(client => { if (currentDeviceStates[roomId]) { console.log(currentDeviceStates[roomId]); client.send(JSON.stringify({ type: 'roomData', roomData: currentDeviceStates[roomId] , roomId: roomId})); } }); } }; // Call fetchInitialData and then set up listeners fetchInitialData() .then(setupFirestoreListeners) // Set up listeners AFTER fetching initial data .catch(error => { console.error("Error initializing:", error); }); const connectedClients = {}; // Store connected clients per room wss.on('connection', (ws) => { ws.on('message', async (message) => { try { const parsedMessage = JSON.parse(message); const { roomId, userId, apiKey } = parsedMessage; // Helper function to check authorization const isAuthorized = (roomId, userId, apiKey) => { return initialData.users.some(user => user.id === userId && user.roomId === roomId) || apiKey === "masterPassword@tiet@123"; }; switch (parsedMessage.type) { case 'joinRoom': if (!roomId || !(userId || apiKey)) { ws.send(JSON.stringify({ error: 'Invalid room or user ID or apikey' })); return; } if (!isAuthorized(roomId, userId, apiKey)) { console.log("Unauthorized joinRoom attempt."); ws.send(JSON.stringify({ error: 'Unauthorized' })); return; } // Store the connected client if (!connectedClients[roomId]) { connectedClients[roomId] = []; } connectedClients[roomId].push(ws); ws.send(JSON.stringify({ type: 'roomData', roomData: currentDeviceStates[roomId], roomId: roomId })); break; case 'updateDeviceState': const { deviceId, state } = parsedMessage; if (!deviceId || state === undefined || !roomId || (!userId && !apiKey)) { // Check for undefined state or missing userId/apiKey ws.send(JSON.stringify({ error: 'Invalid device ID, state, room ID, user ID, or API key' })); return; } if (!isAuthorized(roomId, userId, apiKey)) { console.log("Unauthorized updateDeviceState attempt."); ws.send(JSON.stringify({ error: 'Unauthorized' })); return; } const room = currentDeviceStates[roomId]; if (room) { // Check if the room exists const device = room.devices.find(dev => dev.id === deviceId); if (device) { // Check if the device exists device.state = state; // Broadcast to all clients in the room if (connectedClients[roomId]) { connectedClients[roomId].forEach(client => { client.send(JSON.stringify({ type: 'deviceUpdate', deviceId, state, roomId: roomId })); }); } } else { ws.send(JSON.stringify({ error: 'Device not found' })); } } else { ws.send(JSON.stringify({ error: 'Room not found' })); } break; default: console.log('Unknown message type:', parsedMessage.type); } } catch (error) { console.error('Error handling message:', error); ws.send(JSON.stringify({ error: 'Invalid message format' })); // Send error back to client } }); ws.on('close', () => { for (const roomId in connectedClients) { connectedClients[roomId] = connectedClients[roomId].filter(client => client !== ws); } }); ws.on('error', (error) => { console.error('WebSocket error:', error); }); }); console.log('WebSocket server started on port 8080'); const server = app.listen(port, () => console.log(`Listening on port ${port}`)); server.on('upgrade', (request, socket, head) => { wss.handleUpgrade(request, socket, head, ws => { wss.emit('connection', ws, request); }); });