Files
rosterchirp-dev/backend/src/routes/messages.js
2026-03-13 10:51:27 -04:00

177 lines
6.9 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const path = require('path');
const router = express.Router();
const { getDb } = require('../models/db');
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'));
}
});
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;
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,
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];
if (before) {
query += ' AND m.id < ?';
params.push(before);
}
query += ' ORDER BY m.created_at DESC LIMIT ?';
params.push(parseInt(limit));
const messages = db.prepare(query).all(...params);
// 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);
}
res.json({ messages: messages.reverse() });
});
// 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' });
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,
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 = [];
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
FROM messages m JOIN users u ON m.user_id = u.id
WHERE m.id = ?
`).get(result.lastInsertRowid);
message.reactions = [];
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' });
db.prepare("UPDATE messages SET is_deleted = 1, content = null, image_url = null WHERE id = ?").run(message.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);
res.json({ removed: true, emoji });
} else {
db.prepare('INSERT INTO reactions (message_id, user_id, emoji) VALUES (?, ?, ?)').run(message.id, req.user.id, emoji);
res.json({ added: true, emoji });
}
});
module.exports = router;