v0.9.88 major change sqlite to postgres
This commit is contained in:
@@ -1,219 +1,173 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { getDb } = require('../models/db');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { query, queryOne, queryResult, exec } = require('../models/db');
|
||||
|
||||
// Delete an uploaded image file from disk if it lives under /app/uploads/images
|
||||
function deleteImageFile(imageUrl) {
|
||||
if (!imageUrl) return;
|
||||
try {
|
||||
const filePath = '/app' + imageUrl; // imageUrl is like /uploads/images/img_xxx.jpg
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
} catch (e) {
|
||||
console.warn('[Messages] Could not delete image file:', e.message);
|
||||
}
|
||||
try { const fp = '/app' + imageUrl; if (fs.existsSync(fp)) fs.unlinkSync(fp); }
|
||||
catch (e) { console.warn('[Messages] Could not delete image:', e.message); }
|
||||
}
|
||||
|
||||
module.exports = function(io) {
|
||||
const router = express.Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const imgStorage = multer.diskStorage({
|
||||
destination: '/app/uploads/images',
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `img_${Date.now()}_${Math.random().toString(36).substr(2, 6)}${ext}`);
|
||||
}
|
||||
});
|
||||
const uploadImage = multer({
|
||||
storage: imgStorage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) cb(null, true);
|
||||
else cb(new Error('Images only'));
|
||||
}
|
||||
});
|
||||
const imgStorage = multer.diskStorage({
|
||||
destination: '/app/uploads/images',
|
||||
filename: (req, file, cb) => cb(null, `img_${Date.now()}_${Math.random().toString(36).substr(2,6)}${path.extname(file.originalname)}`),
|
||||
});
|
||||
const uploadImage = multer({ storage: imgStorage, limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => file.mimetype.startsWith('image/') ? cb(null, true) : cb(new Error('Images only')),
|
||||
});
|
||||
|
||||
function getUserForMessage(db, userId) {
|
||||
return db.prepare('SELECT id, name, display_name, avatar, role, status FROM users WHERE id = ?').get(userId);
|
||||
}
|
||||
|
||||
function canAccessGroup(db, groupId, userId) {
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(groupId);
|
||||
if (!group) return null;
|
||||
if (group.type === 'public') return group;
|
||||
const member = db.prepare('SELECT id FROM group_members WHERE group_id = ? AND user_id = ?').get(groupId, userId);
|
||||
if (!member) return null;
|
||||
return group;
|
||||
}
|
||||
|
||||
// Get messages for group
|
||||
router.get('/group/:groupId', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const group = canAccessGroup(db, req.params.groupId, req.user.id);
|
||||
if (!group) return res.status(403).json({ error: 'Access denied' });
|
||||
|
||||
const { before, limit = 50 } = req.query;
|
||||
|
||||
// For managed groups: find when this user joined so we can hide older messages
|
||||
let joinedAt = null;
|
||||
if (group.is_managed) {
|
||||
const membership = db.prepare('SELECT joined_at FROM group_members WHERE group_id = ? AND user_id = ?').get(group.id, req.user.id);
|
||||
if (membership?.joined_at) {
|
||||
// Strip time — they can see messages from the start of the day they joined
|
||||
joinedAt = membership.joined_at.slice(0, 10); // 'YYYY-MM-DD'
|
||||
}
|
||||
async function canAccessGroup(schema, groupId, userId) {
|
||||
const group = await queryOne(schema, 'SELECT * FROM groups WHERE id=$1', [groupId]);
|
||||
if (!group) return null;
|
||||
if (group.type === 'public') return group;
|
||||
const member = await queryOne(schema, 'SELECT id FROM group_members WHERE group_id=$1 AND user_id=$2', [groupId, userId]);
|
||||
return member ? group : null;
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT m.*,
|
||||
u.name as user_name, u.display_name as user_display_name, u.avatar as user_avatar, u.role as user_role, u.status as user_status, u.hide_admin_tag as user_hide_admin_tag, u.about_me as user_about_me, u.allow_dm as user_allow_dm,
|
||||
rm.content as reply_content, rm.image_url as reply_image_url,
|
||||
ru.name as reply_user_name, ru.display_name as reply_user_display_name,
|
||||
rm.is_deleted as reply_is_deleted
|
||||
FROM messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
LEFT JOIN messages rm ON m.reply_to_id = rm.id
|
||||
LEFT JOIN users ru ON rm.user_id = ru.id
|
||||
WHERE m.group_id = ?
|
||||
`;
|
||||
const params = [req.params.groupId];
|
||||
// GET messages for group
|
||||
router.get('/group/:groupId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const group = await canAccessGroup(req.schema, req.params.groupId, req.user.id);
|
||||
if (!group) return res.status(403).json({ error: 'Access denied' });
|
||||
|
||||
// Enforce join-date visibility for managed groups
|
||||
if (joinedAt) {
|
||||
query += ` AND date(m.created_at) >= ?`;
|
||||
params.push(joinedAt);
|
||||
}
|
||||
const { before, limit = 50 } = req.query;
|
||||
let joinedAt = null;
|
||||
if (group.is_managed) {
|
||||
const membership = await queryOne(req.schema,
|
||||
'SELECT joined_at FROM group_members WHERE group_id=$1 AND user_id=$2',
|
||||
[group.id, req.user.id]
|
||||
);
|
||||
if (membership?.joined_at) joinedAt = new Date(membership.joined_at).toISOString().slice(0,10);
|
||||
}
|
||||
|
||||
if (before) {
|
||||
query += ' AND m.id < ?';
|
||||
params.push(before);
|
||||
}
|
||||
let sql = `
|
||||
SELECT m.*,
|
||||
u.name AS user_name, u.display_name AS user_display_name,
|
||||
u.avatar AS user_avatar, u.role AS user_role, u.status AS user_status,
|
||||
u.hide_admin_tag AS user_hide_admin_tag, u.about_me AS user_about_me, u.allow_dm AS user_allow_dm,
|
||||
rm.content AS reply_content, rm.image_url AS reply_image_url,
|
||||
ru.name AS reply_user_name, ru.display_name AS reply_user_display_name,
|
||||
rm.is_deleted AS reply_is_deleted
|
||||
FROM messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
LEFT JOIN messages rm ON m.reply_to_id = rm.id
|
||||
LEFT JOIN users ru ON rm.user_id = ru.id
|
||||
WHERE m.group_id = $1
|
||||
`;
|
||||
const params = [req.params.groupId];
|
||||
let pi = 2;
|
||||
if (joinedAt) { sql += ` AND m.created_at::date >= $${pi++}::date`; params.push(joinedAt); }
|
||||
if (before) { sql += ` AND m.id < $${pi++}`; params.push(before); }
|
||||
sql += ` ORDER BY m.created_at DESC LIMIT $${pi}`;
|
||||
params.push(parseInt(limit));
|
||||
|
||||
query += ' ORDER BY m.created_at DESC LIMIT ?';
|
||||
params.push(parseInt(limit));
|
||||
const messages = await query(req.schema, sql, params);
|
||||
for (const msg of messages) {
|
||||
msg.reactions = await query(req.schema,
|
||||
'SELECT r.emoji, r.user_id, u.name AS user_name FROM reactions r JOIN users u ON r.user_id=u.id WHERE r.message_id=$1',
|
||||
[msg.id]
|
||||
);
|
||||
}
|
||||
res.json({ messages: messages.reverse() });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
const messages = db.prepare(query).all(...params);
|
||||
// POST send message
|
||||
router.post('/group/:groupId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const group = await canAccessGroup(req.schema, req.params.groupId, req.user.id);
|
||||
if (!group) return res.status(403).json({ error: 'Access denied' });
|
||||
if (group.is_readonly && req.user.role !== 'admin') return res.status(403).json({ error: 'Read-only group' });
|
||||
const { content, replyToId, linkPreview } = req.body;
|
||||
if (!content?.trim() && !req.body.imageUrl) return res.status(400).json({ error: 'Message cannot be empty' });
|
||||
const r = await queryResult(req.schema,
|
||||
'INSERT INTO messages (group_id,user_id,content,reply_to_id,link_preview) VALUES ($1,$2,$3,$4,$5) RETURNING id',
|
||||
[req.params.groupId, req.user.id, content?.trim()||null, replyToId||null, linkPreview ? JSON.stringify(linkPreview) : null]
|
||||
);
|
||||
const message = await queryOne(req.schema, `
|
||||
SELECT m.*, u.name AS user_name, u.display_name AS user_display_name, u.avatar AS user_avatar, u.role AS user_role, u.allow_dm AS user_allow_dm,
|
||||
rm.content AS reply_content, ru.name AS reply_user_name, ru.display_name AS reply_user_display_name
|
||||
FROM messages m JOIN users u ON m.user_id=u.id
|
||||
LEFT JOIN messages rm ON m.reply_to_id=rm.id LEFT JOIN users ru ON rm.user_id=ru.id
|
||||
WHERE m.id=$1
|
||||
`, [r.rows[0].id]);
|
||||
message.reactions = [];
|
||||
io.to(`group:${req.params.groupId}`).emit('message:new', message);
|
||||
res.json({ message });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Get reactions for these messages
|
||||
for (const msg of messages) {
|
||||
msg.reactions = db.prepare(`
|
||||
SELECT r.emoji, r.user_id, u.name as user_name
|
||||
FROM reactions r JOIN users u ON r.user_id = u.id
|
||||
WHERE r.message_id = ?
|
||||
`).all(msg.id);
|
||||
}
|
||||
// POST image message
|
||||
router.post('/group/:groupId/image', authMiddleware, uploadImage.single('image'), async (req, res) => {
|
||||
try {
|
||||
const group = await canAccessGroup(req.schema, req.params.groupId, req.user.id);
|
||||
if (!group) return res.status(403).json({ error: 'Access denied' });
|
||||
if (group.is_readonly && req.user.role !== 'admin') return res.status(403).json({ error: 'Read-only group' });
|
||||
if (!req.file) return res.status(400).json({ error: 'No image' });
|
||||
const imageUrl = `/uploads/images/${req.file.filename}`;
|
||||
const { content, replyToId } = req.body;
|
||||
const r = await queryResult(req.schema,
|
||||
"INSERT INTO messages (group_id,user_id,content,image_url,type,reply_to_id) VALUES ($1,$2,$3,$4,'image',$5) RETURNING id",
|
||||
[req.params.groupId, req.user.id, content||null, imageUrl, replyToId||null]
|
||||
);
|
||||
const message = await queryOne(req.schema,
|
||||
'SELECT m.*, u.name AS user_name, u.display_name AS user_display_name, u.avatar AS user_avatar, u.role AS user_role, u.allow_dm AS user_allow_dm FROM messages m JOIN users u ON m.user_id=u.id WHERE m.id=$1',
|
||||
[r.rows[0].id]
|
||||
);
|
||||
message.reactions = [];
|
||||
io.to(`group:${req.params.groupId}`).emit('message:new', message);
|
||||
res.json({ message });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
res.json({ messages: messages.reverse() });
|
||||
});
|
||||
// DELETE message
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const message = await queryOne(req.schema,
|
||||
'SELECT m.*, g.type AS group_type, g.owner_id AS group_owner_id FROM messages m JOIN groups g ON m.group_id=g.id WHERE m.id=$1',
|
||||
[req.params.id]
|
||||
);
|
||||
if (!message) return res.status(404).json({ error: 'Message not found' });
|
||||
const canDelete = message.user_id === req.user.id || req.user.role === 'admin' ||
|
||||
(message.group_type === 'private' && message.group_owner_id === req.user.id);
|
||||
if (!canDelete) return res.status(403).json({ error: 'Cannot delete this message' });
|
||||
const imageUrl = message.image_url;
|
||||
await exec(req.schema, 'UPDATE messages SET is_deleted=TRUE, content=NULL, image_url=NULL WHERE id=$1', [message.id]);
|
||||
deleteImageFile(imageUrl);
|
||||
io.to(`group:${message.group_id}`).emit('message:deleted', { messageId: message.id, groupId: message.group_id });
|
||||
res.json({ success: true, messageId: message.id });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Send message
|
||||
router.post('/group/:groupId', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const group = canAccessGroup(db, req.params.groupId, req.user.id);
|
||||
if (!group) return res.status(403).json({ error: 'Access denied' });
|
||||
if (group.is_readonly && req.user.role !== 'admin') return res.status(403).json({ error: 'This group is read-only' });
|
||||
// POST reaction
|
||||
router.post('/:id/reactions', authMiddleware, async (req, res) => {
|
||||
const { emoji } = req.body;
|
||||
try {
|
||||
const message = await queryOne(req.schema, 'SELECT * FROM messages WHERE id=$1 AND is_deleted=FALSE', [req.params.id]);
|
||||
if (!message) return res.status(404).json({ error: 'Message not found' });
|
||||
const existing = await queryOne(req.schema,
|
||||
'SELECT * FROM reactions WHERE message_id=$1 AND user_id=$2 AND emoji=$3',
|
||||
[message.id, req.user.id, emoji]
|
||||
);
|
||||
if (existing) {
|
||||
await exec(req.schema, 'DELETE FROM reactions WHERE id=$1', [existing.id]);
|
||||
} else {
|
||||
await exec(req.schema, 'INSERT INTO reactions (message_id,user_id,emoji) VALUES ($1,$2,$3)', [message.id, req.user.id, emoji]);
|
||||
}
|
||||
const reactions = await query(req.schema,
|
||||
'SELECT r.emoji, r.user_id, u.name AS user_name FROM reactions r JOIN users u ON r.user_id=u.id WHERE r.message_id=$1',
|
||||
[message.id]
|
||||
);
|
||||
io.to(`group:${message.group_id}`).emit('reaction:updated', { messageId: message.id, reactions });
|
||||
res.json({ reactions });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
const { content, replyToId, linkPreview } = req.body;
|
||||
if (!content?.trim() && !req.body.imageUrl) return res.status(400).json({ error: 'Message cannot be empty' });
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO messages (group_id, user_id, content, reply_to_id, link_preview)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(req.params.groupId, req.user.id, content?.trim() || null, replyToId || null, linkPreview ? JSON.stringify(linkPreview) : null);
|
||||
|
||||
const message = db.prepare(`
|
||||
SELECT m.*,
|
||||
u.name as user_name, u.display_name as user_display_name, u.avatar as user_avatar, u.role as user_role, u.allow_dm as user_allow_dm,
|
||||
rm.content as reply_content, ru.name as reply_user_name, ru.display_name as reply_user_display_name
|
||||
FROM messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
LEFT JOIN messages rm ON m.reply_to_id = rm.id
|
||||
LEFT JOIN users ru ON rm.user_id = ru.id
|
||||
WHERE m.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
message.reactions = [];
|
||||
io.to(`group:${req.params.groupId}`).emit('message:new', message);
|
||||
res.json({ message });
|
||||
});
|
||||
|
||||
// Upload image message
|
||||
router.post('/group/:groupId/image', authMiddleware, uploadImage.single('image'), (req, res) => {
|
||||
const db = getDb();
|
||||
const group = canAccessGroup(db, req.params.groupId, req.user.id);
|
||||
if (!group) return res.status(403).json({ error: 'Access denied' });
|
||||
if (group.is_readonly && req.user.role !== 'admin') return res.status(403).json({ error: 'Read-only group' });
|
||||
if (!req.file) return res.status(400).json({ error: 'No image' });
|
||||
|
||||
const imageUrl = `/uploads/images/${req.file.filename}`;
|
||||
const { content, replyToId } = req.body;
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO messages (group_id, user_id, content, image_url, type, reply_to_id)
|
||||
VALUES (?, ?, ?, ?, 'image', ?)
|
||||
`).run(req.params.groupId, req.user.id, content || null, imageUrl, replyToId || null);
|
||||
|
||||
const message = db.prepare(`
|
||||
SELECT m.*,
|
||||
u.name as user_name, u.display_name as user_display_name, u.avatar as user_avatar, u.role as user_role, u.allow_dm as user_allow_dm
|
||||
FROM messages m JOIN users u ON m.user_id = u.id
|
||||
WHERE m.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
message.reactions = [];
|
||||
io.to(`group:${req.params.groupId}`).emit('message:new', message);
|
||||
res.json({ message });
|
||||
});
|
||||
|
||||
// Delete message
|
||||
router.delete('/:id', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const message = db.prepare('SELECT m.*, g.type as group_type, g.owner_id as group_owner_id, g.is_readonly FROM messages m JOIN groups g ON m.group_id = g.id WHERE m.id = ?').get(req.params.id);
|
||||
if (!message) return res.status(404).json({ error: 'Message not found' });
|
||||
|
||||
const canDelete = message.user_id === req.user.id ||
|
||||
req.user.role === 'admin' ||
|
||||
(message.group_type === 'private' && message.group_owner_id === req.user.id);
|
||||
|
||||
if (!canDelete) return res.status(403).json({ error: 'Cannot delete this message' });
|
||||
|
||||
const imageUrl = message.image_url;
|
||||
db.prepare("UPDATE messages SET is_deleted = 1, content = null, image_url = null WHERE id = ?").run(message.id);
|
||||
deleteImageFile(imageUrl);
|
||||
io.to(`group:${message.group_id}`).emit('message:deleted', { messageId: message.id, groupId: message.group_id });
|
||||
res.json({ success: true, messageId: message.id });
|
||||
});
|
||||
|
||||
// Add/toggle reaction
|
||||
router.post('/:id/reactions', authMiddleware, (req, res) => {
|
||||
const { emoji } = req.body;
|
||||
const db = getDb();
|
||||
const message = db.prepare('SELECT * FROM messages WHERE id = ? AND is_deleted = 0').get(req.params.id);
|
||||
if (!message) return res.status(404).json({ error: 'Message not found' });
|
||||
|
||||
// Check if user's message is from deleted/suspended user
|
||||
const msgUser = db.prepare('SELECT status FROM users WHERE id = ?').get(message.user_id);
|
||||
if (msgUser.status !== 'active') return res.status(400).json({ error: 'Cannot react to this message' });
|
||||
|
||||
const existing = db.prepare('SELECT * FROM reactions WHERE message_id = ? AND user_id = ? AND emoji = ?').get(message.id, req.user.id, emoji);
|
||||
|
||||
if (existing) {
|
||||
db.prepare('DELETE FROM reactions WHERE id = ?').run(existing.id);
|
||||
} else {
|
||||
db.prepare('INSERT INTO reactions (message_id, user_id, emoji) VALUES (?, ?, ?)').run(message.id, req.user.id, emoji);
|
||||
}
|
||||
|
||||
const reactions = db.prepare(`
|
||||
SELECT r.emoji, r.user_id, u.name as user_name
|
||||
FROM reactions r JOIN users u ON r.user_id = u.id
|
||||
WHERE r.message_id = ?
|
||||
`).all(message.id);
|
||||
io.to(`group:${message.group_id}`).emit('reaction:updated', { messageId: message.id, reactions });
|
||||
res.json({ reactions });
|
||||
});
|
||||
|
||||
|
||||
return router;
|
||||
return router;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user