feat: Implement enhanced user profiles with social features including direct messaging, post comments, and reposts, and introduce new routing for Docs and Changelog views.
This commit is contained in:
141
server/db.js
141
server/db.js
@@ -7,82 +7,89 @@ const db = new duckdb.Database(dbPath);
|
||||
const con = db.connect();
|
||||
|
||||
// Initialize Schema
|
||||
con.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
wallet_address VARCHAR PRIMARY KEY,
|
||||
username VARCHAR UNIQUE,
|
||||
bio VARCHAR DEFAULT '',
|
||||
banner_color VARCHAR DEFAULT '#6366f1',
|
||||
balance INTEGER DEFAULT 100,
|
||||
last_seen TIMESTAMP
|
||||
);
|
||||
function initDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
con.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
wallet_address VARCHAR PRIMARY KEY,
|
||||
username VARCHAR UNIQUE,
|
||||
bio VARCHAR DEFAULT '',
|
||||
banner_color VARCHAR DEFAULT '#6366f1',
|
||||
balance INTEGER DEFAULT 100,
|
||||
last_seen TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
channel_id VARCHAR,
|
||||
wallet_address VARCHAR,
|
||||
content VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
tx_id VARCHAR,
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
channel_id VARCHAR,
|
||||
wallet_address VARCHAR,
|
||||
content VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
tx_id VARCHAR,
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reactions (
|
||||
message_id INTEGER,
|
||||
wallet_address VARCHAR,
|
||||
emoji VARCHAR,
|
||||
PRIMARY KEY (message_id, wallet_address, emoji),
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS reactions (
|
||||
message_id INTEGER,
|
||||
wallet_address VARCHAR,
|
||||
emoji VARCHAR,
|
||||
PRIMARY KEY (message_id, wallet_address, emoji),
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
wallet_address VARCHAR,
|
||||
content VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_msg_id START 1;
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_post_id START 1;
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
wallet_address VARCHAR,
|
||||
content VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
-- Migration: Add tx_id to messages if it doesn't exist (for existing DBs)
|
||||
PRAGMA table_info('messages');
|
||||
`, (err) => {
|
||||
if (err) return console.error('Schema error:', err);
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INTEGER PRIMARY KEY,
|
||||
post_id INTEGER,
|
||||
wallet_address VARCHAR,
|
||||
content VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id),
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
// Check if tx_id exists, if not add it
|
||||
con.all("PRAGMA table_info('messages')", (err, rows) => {
|
||||
if (err) return;
|
||||
const hasTxId = rows.some(r => r.name === 'tx_id');
|
||||
if (!hasTxId) {
|
||||
con.run("ALTER TABLE messages ADD COLUMN tx_id VARCHAR", (err) => {
|
||||
if (err) console.error("Error adding tx_id column:", err);
|
||||
CREATE TABLE IF NOT EXISTS reposts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
post_id INTEGER,
|
||||
wallet_address VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id),
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_msg_id START 1;
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_post_id START 1;
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_comment_id START 1;
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_repost_id START 1;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_reposts_unique ON reposts(post_id, wallet_address);
|
||||
`, (err) => {
|
||||
if (err) {
|
||||
console.error('Schema initialization error:', err);
|
||||
return resolve(); // Resolve anyway so server starts
|
||||
}
|
||||
console.log('Database schema created/verified');
|
||||
resolve();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Fatal database initialization error:', e);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Migration: Add bio and banner_color to users
|
||||
con.all("PRAGMA table_info('users')", (err, rows) => {
|
||||
if (err) return;
|
||||
const hasBio = rows.some(r => r.name === 'bio');
|
||||
if (!hasBio) {
|
||||
con.run("ALTER TABLE users ADD COLUMN bio VARCHAR DEFAULT ''", (err) => {
|
||||
if (err) console.error("Error adding bio column:", err);
|
||||
});
|
||||
con.run("ALTER TABLE users ADD COLUMN banner_color VARCHAR DEFAULT '#6366f1'", (err) => {
|
||||
if (err) console.error("Error adding banner_color column:", err);
|
||||
});
|
||||
}
|
||||
|
||||
const hasBalance = rows.some(r => r.name === 'balance');
|
||||
if (!hasBalance) {
|
||||
con.run("ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 100", (err) => {
|
||||
if (err) console.error("Error adding balance column:", err);
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log('Database initialized and cleared');
|
||||
initDb().then(() => {
|
||||
console.log('Database initialized successfully');
|
||||
}).catch(err => {
|
||||
console.error('Failed to initialize database (continuing anyway):', err);
|
||||
});
|
||||
|
||||
module.exports = { db, con };
|
||||
|
||||
443
server/index.js
443
server/index.js
@@ -50,29 +50,21 @@ io.on('connection', (socket) => {
|
||||
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) => {
|
||||
// User exists, update last seen and ensure balance
|
||||
const existingUser = rows[0];
|
||||
const existingUsername = existingUser.username;
|
||||
const existingBalance = existingUser.balance ?? 100;
|
||||
|
||||
con.prepare(`UPDATE users SET last_seen = ?, balance = ? WHERE wallet_address = ?`, (err, uStmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
uStmt.run(now, walletAddress, (err) => {
|
||||
// If balance is NULL or 0, give them 100
|
||||
const finalBalance = (existingBalance < 30) ? 100 : existingBalance;
|
||||
|
||||
uStmt.run(now, finalBalance, walletAddress, (err) => {
|
||||
uStmt.finalize();
|
||||
if (err) console.error("Update error:", err);
|
||||
socket.emit('usernameUpdated', { username: existingUsername });
|
||||
|
||||
// Send balance
|
||||
con.prepare(`SELECT balance FROM users WHERE wallet_address = ?`, (err, bStmt) => {
|
||||
if (err) return console.error("Balance prepare error:", err);
|
||||
bStmt.all(walletAddress, (err, bRows) => {
|
||||
bStmt.finalize();
|
||||
if (err) return console.error("Balance fetch error:", err);
|
||||
if (bRows.length > 0) {
|
||||
socket.emit('balanceUpdated', { balance: bRows[0].balance });
|
||||
} else {
|
||||
console.error("No user found for balance fetch");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.emit('balanceUpdated', { balance: finalBalance });
|
||||
broadcastUserList();
|
||||
});
|
||||
});
|
||||
@@ -89,13 +81,13 @@ io.on('connection', (socket) => {
|
||||
finalUsername = `${username}_${walletAddress.slice(0, 4)}`;
|
||||
}
|
||||
|
||||
con.prepare(`INSERT INTO users (wallet_address, username, last_seen) VALUES (?, ?, ?)`, (err, iStmt) => {
|
||||
con.prepare(`INSERT INTO users (wallet_address, username, last_seen, balance) VALUES (?, ?, ?, ?)`, (err, iStmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
iStmt.run(walletAddress, finalUsername, now, (err) => {
|
||||
iStmt.run(walletAddress, finalUsername, now, 100, (err) => {
|
||||
iStmt.finalize();
|
||||
if (err) console.error("Insert error:", err);
|
||||
socket.emit('usernameUpdated', { username: finalUsername });
|
||||
socket.emit('balanceUpdated', { balance: 100 }); // Default balance
|
||||
socket.emit('balanceUpdated', { balance: 100 });
|
||||
broadcastUserList();
|
||||
});
|
||||
});
|
||||
@@ -109,59 +101,63 @@ io.on('connection', (socket) => {
|
||||
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) => {
|
||||
// First check if user exists and has enough balance
|
||||
con.prepare(`SELECT balance FROM users WHERE wallet_address = ?`, (err, bStmt) => {
|
||||
if (err) return socket.emit('error', { message: 'Database error' });
|
||||
stmt.all(newUsername, (err, rows) => {
|
||||
stmt.finalize();
|
||||
bStmt.all(walletAddress, (err, rows) => {
|
||||
bStmt.finalize();
|
||||
if (err) return socket.emit('error', { message: 'Database error' });
|
||||
if (rows.length === 0) return socket.emit('error', { message: 'User not found' });
|
||||
if (rows[0].balance < 30) return socket.emit('error', { message: 'Insufficient $PLEXUS balance' });
|
||||
|
||||
if (rows.length > 0) {
|
||||
return socket.emit('error', { message: 'Username already taken' });
|
||||
}
|
||||
// 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, uRows) => {
|
||||
stmt.finalize();
|
||||
if (err) return socket.emit('error', { message: 'Database error' });
|
||||
|
||||
// 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' });
|
||||
if (uRows.length > 0) {
|
||||
return socket.emit('error', { message: 'Username already taken' });
|
||||
}
|
||||
|
||||
console.log(`Username updated for ${walletAddress} to ${newUsername}`);
|
||||
socket.emit('usernameUpdated', { username: newUsername });
|
||||
// Update username and deduct balance in one go if possible (or chain)
|
||||
con.prepare(`UPDATE users SET username = ?, balance = balance - 30 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' });
|
||||
|
||||
// Deduct 30 PLEXUS
|
||||
con.prepare(`UPDATE users SET balance = balance - 30 WHERE wallet_address = ?`, (err, bStmt) => {
|
||||
if (!err) {
|
||||
bStmt.run(walletAddress, () => {
|
||||
bStmt.finalize();
|
||||
// Fetch new balance
|
||||
con.prepare(`SELECT balance FROM users WHERE wallet_address = ?`, (err, sStmt) => {
|
||||
if (!err) {
|
||||
sStmt.all(walletAddress, (err, rows) => {
|
||||
sStmt.finalize();
|
||||
if (!err && rows.length > 0) {
|
||||
socket.emit('balanceUpdated', { balance: rows[0].balance });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log(`Username updated for ${walletAddress} to ${newUsername}`);
|
||||
socket.emit('usernameUpdated', { username: newUsername });
|
||||
|
||||
// Fetch new balance to sync
|
||||
con.prepare(`SELECT balance FROM users WHERE wallet_address = ?`, (err, sStmt) => {
|
||||
if (!err) {
|
||||
sStmt.all(walletAddress, (err, rRows) => {
|
||||
sStmt.finalize();
|
||||
if (!err && rRows.length > 0) {
|
||||
socket.emit('balanceUpdated', { balance: rRows[0].balance });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
status: 'validated'
|
||||
};
|
||||
io.emit('newMessage', systemMsg);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -239,27 +235,150 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
});
|
||||
|
||||
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) => {
|
||||
socket.on('getProfile', (targetAddress) => {
|
||||
console.log(`[Profile] Fetching for: ${targetAddress}`);
|
||||
con.prepare(`SELECT wallet_address, username, bio, banner_color, balance, last_seen FROM users WHERE wallet_address = ?`, (err, stmt) => {
|
||||
if (err) {
|
||||
console.error('[Profile] DB Prepare error:', err);
|
||||
return socket.emit('error', { message: 'Database error' });
|
||||
}
|
||||
stmt.all(targetAddress, (err, rows) => {
|
||||
stmt.finalize();
|
||||
if (err || rows.length === 0) return socket.emit('error', { message: 'User not found' });
|
||||
if (err) {
|
||||
console.error('[Profile] DB Exec error:', err);
|
||||
return socket.emit('error', { message: 'Database error' });
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.warn(`[Profile] User not found: ${targetAddress}`);
|
||||
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) => {
|
||||
// Helper to handle BigInt serialization
|
||||
const serializeBigInt = (data) => {
|
||||
return JSON.parse(JSON.stringify(data, (key, value) =>
|
||||
typeof value === 'bigint'
|
||||
? value.toString()
|
||||
: value
|
||||
));
|
||||
};
|
||||
|
||||
// Fetch posts with comment counts and repost counts
|
||||
con.prepare(`
|
||||
SELECT p.*,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM comments WHERE post_id = p.id) as comment_count,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM reposts WHERE post_id = p.id) as repost_count
|
||||
FROM posts p
|
||||
WHERE p.wallet_address = ?
|
||||
ORDER BY p.timestamp DESC
|
||||
LIMIT 50
|
||||
`, (err, pStmt) => {
|
||||
if (err) {
|
||||
console.error('[Profile] Posts prepare error:', err);
|
||||
return socket.emit('profileData', serializeBigInt({ ...user, posts: [], reposts: [] }));
|
||||
}
|
||||
pStmt.all(targetAddress, (err, posts) => {
|
||||
pStmt.finalize();
|
||||
socket.emit('profileData', { ...user, posts: posts || [] });
|
||||
if (err) console.error('[Profile] Posts exec error:', err);
|
||||
|
||||
posts = posts || [];
|
||||
|
||||
// Fetch who reposted each post
|
||||
if (posts.length > 0) {
|
||||
const postIds = posts.map(p => p.id);
|
||||
con.prepare(`SELECT post_id, wallet_address FROM reposts WHERE post_id IN (${postIds.join(',')})`, (err, rStmt) => {
|
||||
if (err) {
|
||||
console.error('[Profile] Reposts prepare error:', err);
|
||||
posts = posts.map(p => ({ ...p, reposted_by: [] }));
|
||||
emitProfileWithReposts();
|
||||
return;
|
||||
}
|
||||
rStmt.all((err, repostRows) => {
|
||||
rStmt.finalize();
|
||||
if (!err && repostRows) {
|
||||
posts = posts.map(p => ({
|
||||
...p,
|
||||
reposted_by: repostRows.filter(r => r.post_id === p.id).map(r => r.wallet_address)
|
||||
}));
|
||||
} else {
|
||||
posts = posts.map(p => ({ ...p, reposted_by: [] }));
|
||||
}
|
||||
emitProfileWithReposts();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
emitProfileWithReposts();
|
||||
}
|
||||
|
||||
// Fetch posts this user reposted (from other users)
|
||||
function emitProfileWithReposts() {
|
||||
con.prepare(`
|
||||
SELECT p.*, u.username as original_username, r.timestamp as repost_timestamp
|
||||
FROM reposts r
|
||||
JOIN posts p ON r.post_id = p.id
|
||||
JOIN users u ON p.wallet_address = u.wallet_address
|
||||
WHERE r.wallet_address = ?
|
||||
ORDER BY r.timestamp DESC
|
||||
LIMIT 20
|
||||
`, (err, rpStmt) => {
|
||||
if (err) {
|
||||
console.error('[Profile] User reposts error:', err);
|
||||
return socket.emit('profileData', serializeBigInt({ ...user, posts, reposts: [] }));
|
||||
}
|
||||
rpStmt.all(targetAddress, (err, userReposts) => {
|
||||
rpStmt.finalize();
|
||||
socket.emit('profileData', serializeBigInt({
|
||||
...user,
|
||||
posts,
|
||||
reposts: userReposts || []
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('repost', ({ postId, walletAddress }) => {
|
||||
console.log(`User ${walletAddress} toggling repost for post ${postId}`);
|
||||
|
||||
// Check if user already reposted this post
|
||||
con.prepare(`SELECT id FROM reposts WHERE post_id = ? AND wallet_address = ?`, (err, checkStmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
checkStmt.all(postId, walletAddress, (err, rows) => {
|
||||
checkStmt.finalize();
|
||||
if (err) return console.error("Check error:", err);
|
||||
|
||||
if (rows.length > 0) {
|
||||
// Already reposted, so toggle OFF (delete)
|
||||
const repostId = rows[0].id;
|
||||
con.prepare(`DELETE FROM reposts WHERE id = ?`, (err, delStmt) => {
|
||||
if (err) return console.error("Delete prepare error:", err);
|
||||
delStmt.run(repostId, (err) => {
|
||||
delStmt.finalize();
|
||||
if (err) return console.error("Delete error:", err);
|
||||
io.emit('repostToggled', { postId, walletAddress, action: 'removed' });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Not reposted yet, so toggle ON (insert)
|
||||
const timestamp = new Date().toISOString();
|
||||
con.prepare(`INSERT INTO reposts (id, post_id, wallet_address, timestamp) VALUES (nextval('seq_repost_id'), ?, ?, ?) RETURNING id`, (err, stmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
stmt.all(postId, walletAddress, timestamp, (err, insertRows) => {
|
||||
stmt.finalize();
|
||||
if (err) return console.error("Insert error:", err);
|
||||
io.emit('repostToggled', { postId, walletAddress, repostId: insertRows[0].id, action: 'added' });
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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) => {
|
||||
@@ -274,22 +393,73 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
socket.on('createPost', ({ walletAddress, content }) => {
|
||||
if (!content || content.trim() === '') return;
|
||||
console.log(`New post from ${walletAddress}: ${content}`);
|
||||
console.log(`Creating post for ${walletAddress}`);
|
||||
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) => {
|
||||
con.prepare(`INSERT INTO posts (id, wallet_address, content, timestamp) VALUES (nextval('seq_post_id'), ?, ?, ?) RETURNING id`, (err, stmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
stmt.all(walletAddress, content, timestamp, (err, rows) => {
|
||||
stmt.finalize();
|
||||
if (err) return socket.emit('error', { message: 'Failed to create post' });
|
||||
if (err) return console.error("Insert error:", err);
|
||||
|
||||
// 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 });
|
||||
const post = {
|
||||
id: rows[0].id,
|
||||
wallet_address: walletAddress,
|
||||
content,
|
||||
timestamp,
|
||||
comments: []
|
||||
};
|
||||
|
||||
// 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
|
||||
// Broadcast to all (or just profile viewers? for now all)
|
||||
io.emit('postCreated', post);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('createComment', ({ postId, walletAddress, content }) => {
|
||||
console.log(`Creating comment on post ${postId} by ${walletAddress}`);
|
||||
const timestamp = new Date().toISOString();
|
||||
con.prepare(`INSERT INTO comments (id, post_id, wallet_address, content, timestamp) VALUES (nextval('seq_comment_id'), ?, ?, ?, ?) RETURNING id`, (err, stmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
stmt.all(postId, walletAddress, content, timestamp, (err, rows) => {
|
||||
stmt.finalize();
|
||||
if (err) return console.error("Insert error:", err);
|
||||
|
||||
// Fetch username
|
||||
con.prepare(`SELECT username FROM users WHERE wallet_address = ?`, (err, uStmt) => {
|
||||
if (err) return;
|
||||
uStmt.all(walletAddress, (err, uRows) => {
|
||||
uStmt.finalize();
|
||||
const username = uRows.length > 0 ? uRows[0].username : walletAddress.slice(0, 4);
|
||||
|
||||
const comment = {
|
||||
id: rows[0].id,
|
||||
post_id: postId,
|
||||
wallet_address: walletAddress,
|
||||
username,
|
||||
content,
|
||||
timestamp
|
||||
};
|
||||
io.emit('commentCreated', comment);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('getComments', (postId) => {
|
||||
con.prepare(`
|
||||
SELECT c.*, u.username
|
||||
FROM comments c
|
||||
LEFT JOIN users u ON c.wallet_address = u.wallet_address
|
||||
WHERE c.post_id = ?
|
||||
ORDER BY c.timestamp ASC
|
||||
`, (err, stmt) => {
|
||||
if (err) return;
|
||||
stmt.all(postId, (err, rows) => {
|
||||
stmt.finalize();
|
||||
if (!err) {
|
||||
socket.emit('commentsLoaded', { postId, comments: rows });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -410,6 +580,99 @@ app.get('/api/messages/:channelId', (req, res) => {
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
// AI Summary Endpoint
|
||||
app.post('/api/summary', async (req, res) => {
|
||||
const { channelId } = req.body;
|
||||
console.log(`[AI] Summary request for channel: ${channelId}`);
|
||||
|
||||
if (!process.env.OPENROUTER_API_KEY) {
|
||||
console.error('[AI] Missing OPENROUTER_API_KEY environment variable');
|
||||
return res.status(500).json({ error: 'AI service not configured (API key missing)' });
|
||||
}
|
||||
|
||||
try {
|
||||
con.prepare(`
|
||||
SELECT m.content, u.username, m.timestamp
|
||||
FROM messages m
|
||||
JOIN users u ON m.wallet_address = u.wallet_address
|
||||
WHERE m.channel_id = ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 50
|
||||
`, (err, stmt) => {
|
||||
if (err) {
|
||||
console.error('[AI] DB Prepare error:', err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
|
||||
stmt.all(channelId, async (err, rows) => {
|
||||
stmt.finalize();
|
||||
if (err) {
|
||||
console.error('[AI] DB Execution error:', err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
console.log(`[AI] No messages found for channel ${channelId}`);
|
||||
return res.json({ summary: "This channel is a quiet void... for now. Send some messages to generate a summary!" });
|
||||
}
|
||||
|
||||
const conversation = rows.reverse().map(r => `${r.username}: ${r.content}`).join('\n');
|
||||
console.log(`[AI] Summarizing ${rows.length} messages...`);
|
||||
|
||||
try {
|
||||
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://plexus.social",
|
||||
"X-Title": "Plexus Social"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "google/learnlm-1.5-pro-experimental:free",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": `You are Plexus AI, a high-signal crypto analyst.
|
||||
Summarize the conversation for #${channelId} with extreme precision.
|
||||
|
||||
Structure your output in Markdown:
|
||||
# 📊 EXECUTIVE SUMMARY
|
||||
# 💎 KEY TOPICS & ALPHA
|
||||
# 🎭 SENTIMENT ANALYSIS
|
||||
# 📜 NOTABLE QUOTES
|
||||
|
||||
Use emojis and bold text for impact. Keep it high-signal.`
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": `Analyze and summarize this conversation:\n\n${conversation}`
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.choices && data.choices[0]) {
|
||||
console.log('[AI] Summary generated successfully');
|
||||
res.json({ summary: data.choices[0].message.content });
|
||||
} else {
|
||||
console.error('[AI] OpenRouter error response:', JSON.stringify(data));
|
||||
res.status(500).json({ error: 'AI Error: ' + (data.error?.message || 'Unknown provider error') });
|
||||
}
|
||||
} catch (apiErr) {
|
||||
console.error('[AI] Fetch exception:', apiErr);
|
||||
res.status(500).json({ error: 'Failed to reach the AI collective.' });
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[AI] Critical error:', e);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
46
server/test_profile_fix.js
Normal file
46
server/test_profile_fix.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const { io } = require("socket.io-client");
|
||||
|
||||
const socket = io("http://localhost:3000");
|
||||
|
||||
const walletAddress = "8vRn2vQb3RsDw2yNtyQESodn7Tr6THzgAgn1ai7srdvM"; // Use the one from logs
|
||||
const username = "TestUser";
|
||||
|
||||
console.log("Connecting to server...");
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected:", socket.id);
|
||||
socket.emit("join", { walletAddress, username });
|
||||
});
|
||||
|
||||
socket.on("balanceUpdated", (data) => {
|
||||
console.log("Joined successfully, balance:", data.balance);
|
||||
console.log("Requesting profile...");
|
||||
socket.emit("getProfile", walletAddress);
|
||||
});
|
||||
|
||||
socket.on("profileData", (data) => {
|
||||
console.log("SUCCESS: Profile data received!");
|
||||
console.log("Username:", data.username);
|
||||
console.log("Comment Count Type:", typeof data.posts[0]?.comment_count); // Should be number/string, not BigInt (object in JS if not serialized)
|
||||
|
||||
// Check if serialization worked (BigInts become strings usually)
|
||||
// But data over JSON is already parsed.
|
||||
console.log("Data sample:", JSON.stringify(data, null, 2));
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
console.error("Error received:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Disconnected from server");
|
||||
});
|
||||
|
||||
// Timeout
|
||||
setTimeout(() => {
|
||||
console.error("Timeout waiting for profile data");
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
63
server/tests/social.test.js
Normal file
63
server/tests/social.test.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const io = require('socket.io-client');
|
||||
const assert = require('chai').assert;
|
||||
|
||||
describe('Social Features (RT & Profile)', function () {
|
||||
this.timeout(5000);
|
||||
let client;
|
||||
const testWallet = 'social_test_wallet_' + Date.now();
|
||||
|
||||
before((done) => {
|
||||
client = io('http://localhost:3000');
|
||||
client.on('connect', () => {
|
||||
client.emit('join', { walletAddress: testWallet, username: 'SocialUser' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it('should update profile bio and banner', (done) => {
|
||||
const updateData = {
|
||||
walletAddress: testWallet,
|
||||
bio: 'New bio content',
|
||||
bannerColor: '#ff0000'
|
||||
};
|
||||
client.emit('updateProfile', updateData);
|
||||
client.on('profileUpdated', (data) => {
|
||||
assert.equal(data.bio, 'New bio content');
|
||||
assert.equal(data.bannerColor, '#ff0000');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a post and then repost it', (done) => {
|
||||
client.emit('createPost', { walletAddress: testWallet, content: 'Social Post' });
|
||||
|
||||
client.once('postCreated', (post) => {
|
||||
assert.equal(post.content, 'Social Post');
|
||||
const postId = post.id;
|
||||
|
||||
client.emit('repost', { postId, walletAddress: testWallet });
|
||||
client.on('postReposted', (repostData) => {
|
||||
assert.equal(repostData.postId, postId);
|
||||
assert.equal(repostData.walletAddress, testWallet);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch profile with correct post counts', (done) => {
|
||||
client.emit('getProfile', testWallet);
|
||||
client.on('profileData', (data) => {
|
||||
assert.equal(data.wallet_address, testWallet);
|
||||
assert.isArray(data.posts);
|
||||
// The post we just created should have a repost count of 1
|
||||
const post = data.posts.find(p => p.content === 'Social Post');
|
||||
assert.exists(post);
|
||||
assert.equal(post.repost_count, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user