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:
2026-01-18 13:10:12 +01:00
parent 959b453d69
commit 62280265b4
23 changed files with 1826 additions and 458 deletions

View File

@@ -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}`);
});