345 lines
15 KiB
JavaScript
345 lines
15 KiB
JavaScript
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<socketId, walletAddress>
|
|
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}`);
|
|
});
|