488 lines
14 KiB
JavaScript
488 lines
14 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const router = express.Router();
|
|
|
|
// Serve uploaded files with proper mapping from database paths to actual file locations
|
|
router.get('/files/uploads/*', async (req, res) => {
|
|
try {
|
|
// Extract the path from the URL (everything after /files/uploads/)
|
|
const requestPath = req.params[0];
|
|
|
|
// The URL path will be like: ufs/AmazonS3:Uploads/274Mf9CyHNG72oF86/filename.jpg
|
|
// We need to extract the mongo_id (274Mf9CyHNG72oF86) from this path
|
|
const pathParts = requestPath.split('/');
|
|
let mongoId = null;
|
|
|
|
// Find the mongo_id in the path structure
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
if (pathParts[i].includes('AmazonS3:Uploads') && i + 1 < pathParts.length) {
|
|
mongoId = pathParts[i + 1];
|
|
break;
|
|
}
|
|
// Sometimes the mongo_id might be the last part of ufs/AmazonS3:Uploads/mongoId
|
|
if (pathParts[i] === 'AmazonS3:Uploads' && i + 1 < pathParts.length) {
|
|
mongoId = pathParts[i + 1];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!mongoId) {
|
|
// Try to get mongo_id from database by matching the full path
|
|
const result = await global.pool.query(`
|
|
SELECT mongo_id, name, type
|
|
FROM uploads
|
|
WHERE path = $1 OR url = $1
|
|
LIMIT 1
|
|
`, [`/ufs/AmazonS3:Uploads/${requestPath}`, `/ufs/AmazonS3:Uploads/${requestPath}`]);
|
|
|
|
if (result.rows.length > 0) {
|
|
mongoId = result.rows[0].mongo_id;
|
|
}
|
|
}
|
|
|
|
if (!mongoId) {
|
|
return res.status(404).json({ error: 'File not found' });
|
|
}
|
|
|
|
// The actual file is stored with just the mongo_id as filename
|
|
const filePath = path.join(__dirname, 'db-convert/db/files/uploads', mongoId);
|
|
|
|
// Get file info from database for proper content-type
|
|
const fileInfo = await global.pool.query(`
|
|
SELECT name, type
|
|
FROM uploads
|
|
WHERE mongo_id = $1
|
|
LIMIT 1
|
|
`, [mongoId]);
|
|
|
|
if (fileInfo.rows.length === 0) {
|
|
return res.status(404).json({ error: 'File metadata not found' });
|
|
}
|
|
|
|
const { name, type } = fileInfo.rows[0];
|
|
|
|
// Set proper content type
|
|
if (type) {
|
|
res.set('Content-Type', type);
|
|
}
|
|
|
|
// Set content disposition with original filename
|
|
if (name) {
|
|
res.set('Content-Disposition', `inline; filename="${name}"`);
|
|
}
|
|
|
|
// Send the file
|
|
res.sendFile(filePath, (err) => {
|
|
if (err) {
|
|
console.error('Error serving file:', err);
|
|
if (!res.headersSent) {
|
|
res.status(404).json({ error: 'File not found on disk' });
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error serving upload:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
// Also serve files directly by mongo_id for simpler access
|
|
router.get('/files/by-id/:mongoId', async (req, res) => {
|
|
try {
|
|
const { mongoId } = req.params;
|
|
|
|
// Get file info from database
|
|
const fileInfo = await global.pool.query(`
|
|
SELECT name, type
|
|
FROM uploads
|
|
WHERE mongo_id = $1
|
|
LIMIT 1
|
|
`, [mongoId]);
|
|
|
|
if (fileInfo.rows.length === 0) {
|
|
return res.status(404).json({ error: 'File not found' });
|
|
}
|
|
|
|
const { name, type } = fileInfo.rows[0];
|
|
const filePath = path.join(__dirname, 'db-convert/db/files/uploads', mongoId);
|
|
|
|
// Set proper content type and filename
|
|
if (type) {
|
|
res.set('Content-Type', type);
|
|
}
|
|
|
|
if (name) {
|
|
res.set('Content-Disposition', `inline; filename="${name}"`);
|
|
}
|
|
|
|
// Send the file
|
|
res.sendFile(filePath, (err) => {
|
|
if (err) {
|
|
console.error('Error serving file:', err);
|
|
if (!res.headersSent) {
|
|
res.status(404).json({ error: 'File not found on disk' });
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error serving upload by ID:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
// Serve avatars
|
|
router.use('/files/avatars', express.static(path.join(__dirname, 'db-convert/db/files/avatars')));
|
|
|
|
// Get all active users for the "view as" dropdown
|
|
router.get('/users', async (req, res) => {
|
|
try {
|
|
const result = await global.pool.query(`
|
|
SELECT id, username, name, type, active, status, lastlogin,
|
|
statustext, utcoffset, statusconnection
|
|
FROM users
|
|
WHERE active = true AND type = 'user'
|
|
ORDER BY
|
|
CASE
|
|
WHEN status = 'online' THEN 1
|
|
WHEN status = 'away' THEN 2
|
|
WHEN status = 'busy' THEN 3
|
|
ELSE 4
|
|
END,
|
|
name ASC
|
|
`);
|
|
|
|
res.json({
|
|
status: 'success',
|
|
users: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching users:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
error: 'Failed to fetch users',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get rooms for a specific user with enhanced room names for direct messages
|
|
router.get('/users/:userId/rooms', async (req, res) => {
|
|
const { userId } = req.params;
|
|
|
|
try {
|
|
// Get the current user's mongo_id for filtering
|
|
const userResult = await global.pool.query(`
|
|
SELECT mongo_id, username FROM users WHERE id = $1
|
|
`, [userId]);
|
|
|
|
if (userResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
status: 'error',
|
|
error: 'User not found'
|
|
});
|
|
}
|
|
|
|
const currentUserMongoId = userResult.rows[0].mongo_id;
|
|
const currentUsername = userResult.rows[0].username;
|
|
|
|
// Get rooms where the user is a member with proper naming from subscription table
|
|
const result = await global.pool.query(`
|
|
SELECT DISTINCT
|
|
r.id,
|
|
r.mongo_id as room_mongo_id,
|
|
r.name,
|
|
r.fname,
|
|
r.t as type,
|
|
r.msgs,
|
|
r.lm as last_message_date,
|
|
r.usernames,
|
|
r.uids,
|
|
r.userscount,
|
|
r.description,
|
|
-- Use the subscription's name for direct messages (excludes current user)
|
|
-- For channels/groups, use room's fname or name
|
|
CASE
|
|
WHEN r.t = 'd' THEN COALESCE(s.fname, s.name, 'Unknown User')
|
|
ELSE COALESCE(r.fname, r.name, 'Unnamed Room')
|
|
END as display_name
|
|
FROM room r
|
|
JOIN subscription s ON s.rid = r.mongo_id
|
|
WHERE s.u->>'_id' = $1
|
|
AND r.archived IS NOT TRUE
|
|
AND s.open = true
|
|
ORDER BY r.lm DESC NULLS LAST
|
|
LIMIT 50
|
|
`, [currentUserMongoId]);
|
|
|
|
res.json({
|
|
status: 'success',
|
|
rooms: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching user rooms:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
error: 'Failed to fetch user rooms',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get room details including participants
|
|
router.get('/rooms/:roomId', async (req, res) => {
|
|
const { roomId } = req.params;
|
|
|
|
try {
|
|
const result = await global.pool.query(`
|
|
SELECT r.id, r.name, r.fname, r.t as type, r.msgs, r.description,
|
|
r.lm as last_message_date, r.usernames, r.uids, r.userscount
|
|
FROM room r
|
|
WHERE r.id = $1
|
|
`, [roomId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({
|
|
status: 'error',
|
|
error: 'Room not found'
|
|
});
|
|
}
|
|
|
|
const room = result.rows[0];
|
|
|
|
// Get room participants for direct messages
|
|
if (room.type === 'd' && room.uids) {
|
|
const participantResult = await global.pool.query(`
|
|
SELECT username, name
|
|
FROM users
|
|
WHERE mongo_id = ANY($1::text[])
|
|
`, [room.uids]);
|
|
|
|
room.participants = participantResult.rows;
|
|
}
|
|
|
|
res.json({
|
|
status: 'success',
|
|
room: room
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching room details:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
error: 'Failed to fetch room details',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get messages for a specific room (fast, without attachments)
|
|
router.get('/rooms/:roomId/messages', async (req, res) => {
|
|
const { roomId } = req.params;
|
|
const { limit = 50, offset = 0, before } = req.query;
|
|
|
|
try {
|
|
// Fast query - just get messages without expensive attachment joins
|
|
let query = `
|
|
SELECT m.id, m.msg, m.ts, m.u, m._updatedat, m.urls, m.mentions, m.md
|
|
FROM message m
|
|
JOIN room r ON m.rid = r.mongo_id
|
|
WHERE r.id = $1
|
|
`;
|
|
|
|
const params = [roomId];
|
|
|
|
if (before) {
|
|
query += ` AND m.ts < $${params.length + 1}`;
|
|
params.push(before);
|
|
}
|
|
|
|
query += ` ORDER BY m.ts DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
params.push(limit, offset);
|
|
|
|
const result = await global.pool.query(query, params);
|
|
|
|
// Add empty attachments array for now - attachments will be loaded separately if needed
|
|
const messages = result.rows.map(msg => ({
|
|
...msg,
|
|
attachments: []
|
|
}));
|
|
|
|
res.json({
|
|
status: 'success',
|
|
messages: messages.reverse() // Reverse to show oldest first
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching messages:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
error: 'Failed to fetch messages',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get attachments for specific messages (called separately for performance)
|
|
router.post('/messages/attachments', async (req, res) => {
|
|
const { messageIds } = req.body;
|
|
|
|
if (!messageIds || !Array.isArray(messageIds) || messageIds.length === 0) {
|
|
return res.json({ status: 'success', attachments: {} });
|
|
}
|
|
|
|
try {
|
|
// Get room mongo_id from first message to limit search scope
|
|
const roomQuery = await global.pool.query(`
|
|
SELECT r.mongo_id as room_mongo_id
|
|
FROM message m
|
|
JOIN room r ON m.rid = r.mongo_id
|
|
WHERE m.id = $1
|
|
LIMIT 1
|
|
`, [messageIds[0]]);
|
|
|
|
if (roomQuery.rows.length === 0) {
|
|
return res.json({ status: 'success', attachments: {} });
|
|
}
|
|
|
|
const roomMongoId = roomQuery.rows[0].room_mongo_id;
|
|
|
|
// Get messages and their upload timestamps
|
|
const messagesQuery = await global.pool.query(`
|
|
SELECT m.id, m.ts, m.u->>'_id' as user_id
|
|
FROM message m
|
|
WHERE m.id = ANY($1::int[])
|
|
`, [messageIds]);
|
|
|
|
if (messagesQuery.rows.length === 0) {
|
|
return res.json({ status: 'success', attachments: {} });
|
|
}
|
|
|
|
// Build a map of user_id -> array of message timestamps for efficient lookup
|
|
const userTimeMap = {};
|
|
const messageMap = {};
|
|
messagesQuery.rows.forEach(msg => {
|
|
if (!userTimeMap[msg.user_id]) {
|
|
userTimeMap[msg.user_id] = [];
|
|
}
|
|
userTimeMap[msg.user_id].push(msg.ts);
|
|
messageMap[msg.id] = { ts: msg.ts, user_id: msg.user_id };
|
|
});
|
|
|
|
// Get attachments for this room and these users
|
|
const uploadsQuery = await global.pool.query(`
|
|
SELECT mongo_id, name, size, type, url, path, typegroup, identify,
|
|
userid, uploadedat
|
|
FROM uploads
|
|
WHERE rid = $1
|
|
AND userid = ANY($2::text[])
|
|
ORDER BY uploadedat
|
|
`, [roomMongoId, Object.keys(userTimeMap)]);
|
|
|
|
// Match attachments to messages based on timestamp proximity (within 5 minutes)
|
|
const attachmentsByMessage = {};
|
|
|
|
uploadsQuery.rows.forEach(upload => {
|
|
const uploadTime = new Date(upload.uploadedat).getTime();
|
|
|
|
// Find the closest message from this user within 5 minutes
|
|
let closestMessageId = null;
|
|
let closestTimeDiff = Infinity;
|
|
|
|
Object.entries(messageMap).forEach(([msgId, msgData]) => {
|
|
if (msgData.user_id === upload.userid) {
|
|
const msgTime = new Date(msgData.ts).getTime();
|
|
const timeDiff = Math.abs(uploadTime - msgTime);
|
|
|
|
if (timeDiff < 300000 && timeDiff < closestTimeDiff) { // 5 minutes = 300000ms
|
|
closestMessageId = msgId;
|
|
closestTimeDiff = timeDiff;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (closestMessageId) {
|
|
if (!attachmentsByMessage[closestMessageId]) {
|
|
attachmentsByMessage[closestMessageId] = [];
|
|
}
|
|
|
|
attachmentsByMessage[closestMessageId].push({
|
|
id: upload.id,
|
|
mongo_id: upload.mongo_id,
|
|
name: upload.name,
|
|
size: upload.size,
|
|
type: upload.type,
|
|
url: upload.url,
|
|
path: upload.path,
|
|
typegroup: upload.typegroup,
|
|
identify: upload.identify
|
|
});
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
status: 'success',
|
|
attachments: attachmentsByMessage
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching message attachments:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
error: 'Failed to fetch attachments',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Search messages in accessible rooms for a user
|
|
router.get('/users/:userId/search', async (req, res) => {
|
|
const { userId } = req.params;
|
|
const { q, limit = 20 } = req.query;
|
|
|
|
if (!q || q.length < 2) {
|
|
return res.status(400).json({
|
|
status: 'error',
|
|
error: 'Search query must be at least 2 characters'
|
|
});
|
|
}
|
|
|
|
try {
|
|
const userResult = await global.pool.query(`
|
|
SELECT mongo_id FROM users WHERE id = $1
|
|
`, [userId]);
|
|
|
|
if (userResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
status: 'error',
|
|
error: 'User not found'
|
|
});
|
|
}
|
|
|
|
const currentUserMongoId = userResult.rows[0].mongo_id;
|
|
|
|
const result = await global.pool.query(`
|
|
SELECT m.id, m.msg, m.ts, m.u, r.id as room_id, r.name as room_name, r.fname as room_fname, r.t as room_type
|
|
FROM message m
|
|
JOIN room r ON m.rid = r.mongo_id
|
|
JOIN subscription s ON s.rid = r.mongo_id AND s.u->>'_id' = $1
|
|
WHERE m.msg ILIKE $2
|
|
AND r.archived IS NOT TRUE
|
|
ORDER BY m.ts DESC
|
|
LIMIT $3
|
|
`, [currentUserMongoId, `%${q}%`, limit]);
|
|
|
|
res.json({
|
|
status: 'success',
|
|
results: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error searching messages:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
error: 'Failed to search messages',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|