const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const cors = require('cors'); const { db, con } = require('./db'); const app = express(); app.use(cors()); app.use(express.json()); const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*", // Allow all for dev, restrict in prod methods: ["GET", "POST"] } }); const CHANNELS = [ { id: 'nebula', name: 'Nebula' }, { id: 'solstice', name: 'Solstice' }, { id: 'zenith', name: 'Zenith' }, { id: 'aether', name: 'Aether' }, { id: 'vortex', name: 'Vortex' }, { id: 'borealis', name: 'Borealis' }, { id: 'chronos', name: 'Chronos' }, { id: 'elysium', name: 'Elysium' }, { id: 'ignis', name: 'Ignis' }, { id: 'nova', name: 'Nova' } ]; // Store connected users in memory for "Online" status // Map const connectedSockets = new Map(); io.on('connection', (socket) => { console.log('A user connected:', socket.id); socket.on('join', async ({ walletAddress, username }) => { console.log(`User joining: ${username} (${walletAddress})`); connectedSockets.set(socket.id, walletAddress); const now = new Date().toISOString(); // First, check if this wallet already has a username con.prepare(`SELECT username FROM users WHERE wallet_address = ?`, (err, stmt) => { if (err) return console.error("Prepare error:", err); stmt.all(walletAddress, (err, rows) => { stmt.finalize(); if (err) return console.error("Execute error:", err); if (rows.length > 0) { // User exists, update last seen const existingUsername = rows[0].username; con.prepare(`UPDATE users SET last_seen = ? WHERE wallet_address = ?`, (err, uStmt) => { if (err) return console.error("Prepare error:", err); uStmt.run(now, walletAddress, (err) => { uStmt.finalize(); if (err) console.error("Update error:", err); socket.emit('usernameUpdated', { username: existingUsername }); broadcastUserList(); }); }); } else { // New user, check if username is taken con.prepare(`SELECT wallet_address FROM users WHERE username = ?`, (err, nStmt) => { if (err) return console.error("Prepare error:", err); nStmt.all(username, (err, uRows) => { nStmt.finalize(); if (err) return console.error("Check error:", err); let finalUsername = username; if (uRows.length > 0) { finalUsername = `${username}_${walletAddress.slice(0, 4)}`; } con.prepare(`INSERT INTO users (wallet_address, username, last_seen) VALUES (?, ?, ?)`, (err, iStmt) => { if (err) return console.error("Prepare error:", err); iStmt.run(walletAddress, finalUsername, now, (err) => { iStmt.finalize(); if (err) console.error("Insert error:", err); socket.emit('usernameUpdated', { username: finalUsername }); broadcastUserList(); }); }); }); }); } }); }); }); socket.on('updateUsername', ({ walletAddress, newUsername, txId }) => { console.log(`User ${walletAddress} requesting username change to ${newUsername} (TX: ${txId})`); // Check if username is taken con.prepare(`SELECT wallet_address FROM users WHERE username = ?`, (err, stmt) => { if (err) return socket.emit('error', { message: 'Database error' }); stmt.all(newUsername, (err, rows) => { stmt.finalize(); if (err) return socket.emit('error', { message: 'Database error' }); if (rows.length > 0) { return socket.emit('error', { message: 'Username already taken' }); } // Update username con.prepare(`UPDATE users SET username = ? WHERE wallet_address = ?`, (err, uStmt) => { if (err) return socket.emit('error', { message: 'Failed to update username' }); uStmt.run(newUsername, walletAddress, (err) => { uStmt.finalize(); if (err) return socket.emit('error', { message: 'Failed to update username' }); console.log(`Username updated for ${walletAddress} to ${newUsername}`); socket.emit('usernameUpdated', { username: newUsername }); broadcastUserList(); // Also broadcast a system message about the change const systemMsg = { id: Date.now(), channelId: 'nebula', walletAddress: 'system', username: 'System', content: `${walletAddress.slice(0, 4)}... changed their name to ${newUsername}`, timestamp: new Date().toISOString() }; io.emit('newMessage', systemMsg); }); }); }); }); }); socket.on('sendMessage', ({ channelId, walletAddress, content, txId }) => { if (!content || content.trim() === '') return; console.log(`Message from ${walletAddress} in ${channelId} (TX: ${txId}): ${content}`); const timestamp = new Date().toISOString(); con.prepare(`INSERT INTO messages (id, channel_id, wallet_address, content, timestamp, tx_id) VALUES (nextval('seq_msg_id'), ?, ?, ?, ?, ?) RETURNING id`, (err, stmt) => { if (err) return console.error("Prepare error:", err); stmt.all(channelId, walletAddress, content, timestamp, txId, (err, rows) => { stmt.finalize(); if (err) { console.error("Error saving message:", err); return; } const msgId = rows[0].id; con.prepare(`SELECT username FROM users WHERE wallet_address = ?`, (err, uStmt) => { if (err) return console.error("Prepare error:", err); uStmt.all(walletAddress, (err, uRows) => { uStmt.finalize(); const username = uRows.length > 0 ? uRows[0].username : walletAddress.slice(0, 4) + '...'; const message = { id: msgId, channelId, walletAddress, username, content, timestamp, txId }; io.emit('newMessage', message); }); }); }); }); }); socket.on('getProfile', (walletAddress) => { console.log(`Fetching profile for ${walletAddress}`); con.prepare(`SELECT wallet_address, username, bio, banner_color, last_seen FROM users WHERE wallet_address = ?`, (err, stmt) => { if (err) return socket.emit('error', { message: 'Database error' }); stmt.all(walletAddress, (err, rows) => { stmt.finalize(); if (err || rows.length === 0) return socket.emit('error', { message: 'User not found' }); const user = rows[0]; // Fetch posts con.prepare(`SELECT * FROM posts WHERE wallet_address = ? ORDER BY timestamp DESC LIMIT 50`, (err, pStmt) => { if (err) return socket.emit('profileData', { ...user, posts: [] }); pStmt.all(walletAddress, (err, posts) => { pStmt.finalize(); socket.emit('profileData', { ...user, posts: posts || [] }); }); }); }); }); }); socket.on('updateProfile', ({ walletAddress, bio, bannerColor }) => { console.log(`Updating profile for ${walletAddress}`); con.prepare(`UPDATE users SET bio = ?, banner_color = ? WHERE wallet_address = ?`, (err, stmt) => { if (err) return socket.emit('error', { message: 'Database error' }); stmt.run(bio, bannerColor, walletAddress, (err) => { stmt.finalize(); if (err) return socket.emit('error', { message: 'Failed to update profile' }); socket.emit('profileUpdated', { bio, bannerColor }); broadcastUserList(); }); }); }); socket.on('createPost', ({ walletAddress, content }) => { if (!content || content.trim() === '') return; console.log(`New post from ${walletAddress}: ${content}`); const timestamp = new Date().toISOString(); con.prepare(`INSERT INTO posts (id, wallet_address, content, timestamp) VALUES (nextval('seq_post_id'), ?, ?, ?)`, (err, stmt) => { if (err) return socket.emit('error', { message: 'Database error' }); stmt.run(walletAddress, content, timestamp, (err) => { stmt.finalize(); if (err) return socket.emit('error', { message: 'Failed to create post' }); // Fetch all posts to broadcast update or just emit the new one // For simplicity, we'll just tell the user it was created socket.emit('postCreated', { content, timestamp }); // If we want a live feed, we could broadcast to a "profile room" // For now, the user can just refresh or we emit to them }); }); }); socket.on('toggleReaction', ({ messageId, walletAddress, emoji }) => { console.log(`Toggling reaction: ${emoji} on message ${messageId} by ${walletAddress}`); con.prepare(`SELECT * FROM reactions WHERE message_id = ? AND wallet_address = ? AND emoji = ?`, (err, stmt) => { if (err) return console.error("Prepare error:", err); stmt.all(messageId, walletAddress, emoji, (err, rows) => { stmt.finalize(); if (err) return console.error("Error checking reaction:", err); if (rows.length > 0) { con.prepare(`DELETE FROM reactions WHERE message_id = ? AND wallet_address = ? AND emoji = ?`, (err, dStmt) => { if (err) return console.error("Prepare error:", err); dStmt.run(messageId, walletAddress, emoji, (err) => { dStmt.finalize(); if (err) return console.error("Error removing reaction:", err); broadcastReactions(messageId); }); }); } else { con.prepare(`INSERT INTO reactions (message_id, wallet_address, emoji) VALUES (?, ?, ?)`, (err, iStmt) => { if (err) return console.error("Prepare error:", err); iStmt.run(messageId, walletAddress, emoji, (err) => { iStmt.finalize(); if (err) return console.error("Error adding reaction:", err); broadcastReactions(messageId); }); }); } }); }); }); function broadcastReactions(messageId) { console.log("Broadcasting reactions for message:", messageId); con.prepare(`SELECT emoji, wallet_address FROM reactions WHERE message_id = ?`, (err, stmt) => { if (err) return console.error("Prepare error:", err); stmt.all(messageId, (err, rows) => { stmt.finalize(); if (err) { console.error("Error fetching reactions for broadcast:", err); return; } console.log(`Found ${rows.length} reactions, emitting updateReactions`); io.emit('updateReactions', { messageId, reactions: rows }); }); }); } socket.on('disconnect', () => { const walletAddress = connectedSockets.get(socket.id); connectedSockets.delete(socket.id); if (walletAddress) { // Update last seen? broadcastUserList(); } console.log('User disconnected:', socket.id); }); function broadcastUserList() { // Get all users from DB to show offline ones too con.prepare(`SELECT wallet_address, username, last_seen FROM users`, (err, stmt) => { if (err) return console.error("Prepare error:", err); stmt.all((err, rows) => { stmt.finalize(); if (err) return; const onlineWallets = new Set(connectedSockets.values()); const users = rows.map(u => ({ ...u, online: onlineWallets.has(u.wallet_address) })); io.emit('userList', users); }); }); } }); app.get('/api/channels', (req, res) => { res.json(CHANNELS); }); app.get('/api/messages/:channelId', (req, res) => { const { channelId } = req.params; con.prepare(` SELECT m.*, u.username FROM messages m LEFT JOIN users u ON m.wallet_address = u.wallet_address WHERE m.channel_id = ? ORDER BY m.timestamp ASC LIMIT 100 `, (err, stmt) => { if (err) return res.status(500).json({ error: err.message }); stmt.all(channelId, (err, messages) => { stmt.finalize(); if (err) return res.status(500).json({ error: err.message }); // Fetch reactions for all these messages con.prepare(`SELECT * FROM reactions WHERE message_id IN (SELECT id FROM messages WHERE channel_id = ?)`, (err, rStmt) => { if (err) return res.json(messages.map(m => ({ ...m, reactions: [] }))); rStmt.all(channelId, (err, reactions) => { rStmt.finalize(); if (err) return res.json(messages.map(m => ({ ...m, reactions: [] }))); const messagesWithReactions = messages.map(m => ({ ...m, reactions: reactions.filter(r => r.message_id === m.id) })); res.json(messagesWithReactions); }); }); }); }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });