~firefoxreact-appqfsserver
9 itemsDownload ./*

..
uploads
.env
index.js
name.log
package-lock.json
package.json
sessions.log
startserver.sh
word.log


serverindex.js
14 KB• 9•  3 days ago•  DownloadRawClose
3 days ago•  9

{}
/*
   Author: Twily        2025
   Website:       twily.info
*/
//require('dotenv').config();
//const SERVER_PORT = process.env.PORT || 3000;
//const CLIENT_URL = process.env.CLIENT_URL || 'http://localhost:5173';
const SERVER_PORT = 3000;
//const CLIENT_URL = '/'; // prod
const CLIENT_URL = 'http://10.0.0.64:5173'; // dev

const express = require('express');
const cors = require('cors');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http, { cors: { origin: CLIENT_URL, credentials: true } });
const fs = require('fs').promises;
const path = require('path');
const multer = require('multer');

const DB_FILE = path.join(__dirname, 'sessions.log');
const NAME_FILE = path.join(__dirname, 'name.log');
const WORD_FILE = path.join(__dirname, 'word.log');
const upload = multer({ dest: 'uploads/' });
let sessionsCache = [];
const activeDownloads = {}; // { code: { fileName: count } }

app.use(cors({
    origin: CLIENT_URL,
    methods: ["GET", "POST"],
    credentials: true
}));
app.use(express.static('public'));

const capitalizeFirstLetter = (str) => {
  if (typeof str !== 'string' || str.length === 0) {
    return str; // Handle non-string input or empty strings
  }
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

const loadWords = async (filePath) => {
  try {
    const data = await fs.readFile(filePath, 'utf8');
    return data.split('\n').filter(line => line.trim());
  } catch (e) {
    console.error(`Error reading ${filePath}:`, e);
    return [];
  }
}

const generateCode = async () => {
  const names = await loadWords(NAME_FILE);
  const words = await loadWords(WORD_FILE);
  if (names.length === 0 || words.length === 0) {
    throw new Error('Word lists are empty or missing');
  }
  const name = capitalizeFirstLetter(names[Math.floor(Math.random() * names.length)]);
  const word = capitalizeFirstLetter(words[Math.floor(Math.random() * words.length)]);
  const order = Math.random() < 0.5;
  return order ? `${name} ${word}` : `${word} ${name}`;
}

app.get('/api/generate-code', async (req, res) => {
  try {
    const code = await generateCode();
    res.json({ code });
  } catch (e) {
    console.error('Error generating code:', e);
    res.status(500).json({ error: 'Failed to generate code' });
  }
});

const loadSessions = async () => {
  try {
    const data = await fs.readFile(DB_FILE, 'utf8');
    sessionsCache = data.split('\n').filter(line => line).map(line => JSON.parse(line));
  } catch (e) {
    console.error('Error loading sessions:', e);
  }
}

const saveSession = async (session) => {
  try {
    sessionsCache.push(session);
    await fs.appendFile(DB_FILE, JSON.stringify(session) + '\n');
  } catch (e) {
    console.error('Error saving session:', e);
  }
}

const deleteSession = async (code) => {
  try {
    const session = sessionsCache.find((s) => s.code === code);
    if (session) {
      console.log(`Deleting files for room ${code}: ${session.files.length} files`);
      for (const file of session.files) {
        try {
          const filePath = path.join(__dirname, 'uploads', path.basename(file.path || ''));
          await fs.unlink(filePath);
          console.log(`Deleted file: ${filePath}`);
        } catch (e) {
          console.error(`Error deleting file ${file.path || 'unknown'}:`, e);
        }
      }
    } else {
      console.log(`No session found for room ${code}`);
    }
    sessionsCache = sessionsCache.filter((s) => s.code !== code);
    await fs.writeFile(DB_FILE, sessionsCache.map((s) => JSON.stringify(s) + '\n').join(''));
    delete activeDownloads[code];
    console.log(`Deleted session for room ${code}`);
  } catch (e) {
    console.error('Error deleting session:', e);
  }
}

// Create or join session
io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  socket.on('create', async (code, callback) => {
    if (!code || sessionsCache.some((s) => s.code === code)) {
      socket.emit('error', 'Room already exists or invalid code');
      return;
    }
    const session = { code, peers: [socket.id], files: [] };
    await saveSession(session);
    socket.join(code);
    socket.emit('created', code);
    if (callback) callback({ success: true, message: `Created room ${code}` });
    console.log(`Room created: ${code}, peer: ${socket.id}`);
  });

  socket.on('join', async (code, callback) => {
    if (!code) {
      socket.emit('error', 'Invalid room code');
      if (callback) callback({ error: 'No room code provided' });
      return;
    }
    const session = sessionsCache.find((s) => s.code === code);
    if (!session) {
      socket.emit('error', 'Room does not exist');
      if (callback) callback({ error: `Room ${code} not found` });
      return;
    }
    if (session.peers.includes(socket.id)) {
      socket.emit('error', `Already in room ${code}`);
      if (callback) callback({ error: `Already in room ${code}` });
      return;
    }
    session.peers.push(socket.id);
    socket.join(code);
    socket.to(code).emit('peer-joined', { peerId: socket.id });
    
    if(session.useFallback) {
      socket.emit('files', session.files);
      socket.emit('room-status', { useFallback: session.useFallback || false, textHistory: session.textHistory });
    } else {
      console.log('skipping room-status for webrtc file and text~');
      // file list and text history is shared over webrtc
    }
    if (callback) callback({ success: true, message: `Joined room ${code}` });
    console.log(`User ${socket.id} joined room ${code}, useFallback=${session.useFallback || false}`);
  });

  socket.on('leave', async (code, callback) => {
    if (!code) {
      if (callback) callback({ error: 'No room code provided' });
      return;
    }
    const session = sessionsCache.find((s) => s.code === code);
    if (session) {
      const wasCreator = session.peers[0] === socket.id;
      session.peers = session.peers.filter((id) => id !== socket.id);
      if (session.peers.length === 0) {
        sessionsCache = sessionsCache.filter((s) => s.code !== code);
        await deleteSession(code);
        console.log(`Deleted room ${code} (no peers left)`);
        if (callback) callback({ success: true, message: `Room ${code} deleted` });
      } else if (wasCreator) {
        sessionsCache = sessionsCache.filter((s) => s.code !== code);
        await deleteSession(code);
        io.to(code).emit('error', 'Room closed by creator');
        console.log(`Closed room ${code} (creator left)`);
        if (callback) callback({ success: true, message: `Room ${code} closed by creator` });
      } else {
        socket.to(code).emit('peer-left', { peerId: socket.id });
        console.log(`User ${socket.id} left room ${code}, peers: ${session.peers}`);
        if (callback) callback({ success: true, message: `Left room ${code}` });
      }
    } else {
      if (callback) callback({ error: `Room ${code} not found` });
    }
  });
  // alt variant close when last leave (not updated)
  //socket.on('leave', async (code) => {
  //  if (!code) return;
  //  const session = sessionsCache.find((s) => s.code === code);
  //  if (session) {
  //    session.peers = session.peers.filter((id) => id !== socket.id);
  //    if (session.peers.length === 0) {
  //      await deleteSession(code);
  //    } else {
  //      await fs.writeFile(DB_FILE, sessionsCache.map((s) => JSON.stringify(s) + '\n').join(''));
  //      socket.to(code).emit('peer-left', { peerId: socket.id });
  //    }
  //  }
  //  socket.leave(code);
  //  console.log(`User ${socket.id} left room ${code}`);
  //});

  socket.on('offer', ({ code, offer, to, from }) => {
    if (!to || !code) return;
    socket.to(to).emit('offer', { code, offer, to, from });
    console.log(`Forwarded offer from ${from} to ${to}`);
  });

  socket.on('answer', ({ code, answer, to, from }) => {
    if (!to || !code) return;
    socket.to(to).emit('answer', { code, answer, to, from });
    console.log(`Forwarded answer from ${from} to ${to}`);
  });

  socket.on('ice-candidate', ({ code, candidate, to, from }) => {
    if (!to || !code) return;
    socket.to(to).emit('ice-candidate', { code, candidate, to, from });
    console.log(`Forwarded ICE candidate from ${socket.id} to ${to}`);
  });

  socket.on('text', async ({ code, text }) => {
    if (!code) return;
    const session = sessionsCache.find((s) => s.code === code);
    if (session) {
      if (!session.textHistory) session.textHistory = [];
      session.textHistory.push(text);
      await fs.writeFile(DB_FILE, sessionsCache.map((s) => JSON.stringify(s) + '\n').join(''));
      socket.to(code).emit('text', { text });
      socket.emit('text', { text }); // Echo back to sender
      console.log(`Forwarded text to room ${code}: ${text}`);
    }
  });

  socket.on('fallback', async ({ code }) => {
    const session = sessionsCache.find((s) => s.code === code);
    if (session) {
      session.useFallback = true;
      await fs.writeFile(DB_FILE, sessionsCache.map((s) => JSON.stringify(s) + '\n').join(''));
      socket.to(code).emit('fallback');
      console.log(`Room ${code} switched to fallback mode`);
    }
  });

  socket.on('file-list', ({ code, files }) => {
    if (!code) return;
    socket.to(code).emit('file-list', { files });
    console.log(`Forwarded file list to room ${code}:`, files);
  });

  socket.on('request-file', ({ code, fileName, to }) => {
    if (!to || !code || !fileName) return;
    socket.to(to).emit('request-file', { fileName, from: socket.id });
    console.log(`Forwarded file request for ${fileName} from ${socket.id} to ${to}`);
  });

  socket.on('file-data', ({ fileName, data, to }) => {
    if (!to || !fileName) return;
    socket.to(to).emit('file-data', { fileName, data });
    console.log(`Forwarded file data for ${fileName} from ${socket.id} to ${to}`);
  });

  // Server-side
  socket.on('check-room-status', async (code) => {
    const session = sessionsCache.find((s) => s.code === code);
    if (!session) {
      socket.emit('room-status-check', { exists: false });
    } else {
      const isInRoom = session.creator === socket.id || session.peers.includes(socket.id);
      socket.emit('room-status-check', { exists: true, isInRoom });
    }
  });

  socket.on('download-start-fallback', ({ code, fileName }) => {
    if (!activeDownloads[code]) activeDownloads[code] = {};
    activeDownloads[code][fileName] = (activeDownloads[code][fileName] || 0) + 1;
    io.to(code).emit('update-count-fallback', { fileName, count: activeDownloads[code][fileName] });
    console.log(`Fallback download started for ${fileName} in room ${code}, count: ${activeDownloads[code][fileName]}`);
  });
  
  socket.on('download-end-fallback', ({ code, fileName }) => {
    if (activeDownloads[code] && activeDownloads[code][fileName]) {
      activeDownloads[code][fileName]--;
      if (activeDownloads[code][fileName] <= 0) delete activeDownloads[code][fileName];
      io.to(code).emit('update-count-fallback', { fileName, count: activeDownloads[code][fileName] || 0 });
      console.log(`Fallback download ended for ${fileName} in room ${code}, count: ${activeDownloads[code][fileName] || 0}`);
    }
  });

  // Room closes if/when creator leaves
  socket.on('disconnect', async () => {
    console.log('User disconnected:', socket.id);
    const session = sessionsCache.find((s) => s.peers.includes(socket.id));
    if (session) {
      const wasCreator = session.peers[0] === socket.id;
      session.peers = session.peers.filter((id) => id !== socket.id);
      if (session.peers.length === 0) {
        sessionsCache = sessionsCache.filter((s) => s.code !== session.code);
        await deleteSession(session.code);
        console.log(`Deleted room ${session.code} (no peers left)`);
      } else if (wasCreator) {
        sessionsCache = sessionsCache.filter((s) => s.code !== session.code);
        await deleteSession(session.code);
        io.to(session.code).emit('error', 'Room closed by creator');
        console.log(`Closed room ${session.code} (creator disconnected)`);
      } else {
        //await updateSession(session);
        io.to(session.code).emit('peer-left', { peerId: socket.id });
        console.log(`User ${socket.id} disconnected from room ${session.code}, peers: ${session.peers}`);
      }
    }
  });
  // Alternative keep room alive until last leave?
  //socket.on('disconnect', async () => {
  //  console.log(`User disconnected: ${socket.id}`);
  //  const session = sessionsCache.find((s) => s.peers.includes(socket.id));
  //  if (session) {
  //    session.peers = session.peers.filter((id) => id !== socket.id);
  //    if (session.peers.length === 0) {
  //      await deleteSession(session.code);
  //    } else {
  //      await fs.writeFile(DB_FILE, sessionsCache.map((s) => JSON.stringify(s) + '\n').join(''));
  //      socket.to(session.code).emit('peer-left', { peerId: socket.id });
  //    }
  //  }
  //});
});

// File upload for fallback
app.post('/api/upload/:code', upload.array('files', 10), async (req, res) => {
  const { code } = req.params;
  const session = sessionsCache.find((s) => s.code === code);
  if (!session) {
    res.status(404).send('Room not found');
    return;
  }
  const files = req.files;
  if (!files || files.length === 0) {
    res.status(400).send('No files uploaded');
    return;
  }
  const uploaderId = req.headers['x-socket-id'];
  const fileList = files.map(file => ({
    name: file.originalname,
    path: file.path,
    uploaderId,
    size: file.size,
  }));
  session.files.push(...fileList);
  await fs.writeFile(DB_FILE, sessionsCache.map((s) => JSON.stringify(s) + '\n').join(''));
  io.to(code).emit('file-list', { files: fileList });
  res.status(200).send('Files uploaded');
});

http.listen(SERVER_PORT, async () => {
  await fs.mkdir('uploads', { recursive: true });
  await loadSessions();
  console.log(`Server running on port ${SERVER_PORT}`);
});

Top
©twily.info 2013 - 2025
twily at twily dot info



2 417 991 visits
... ^ v