649 lines
20 KiB
JavaScript
649 lines
20 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 user avatars by mongo_id
|
|
router.get('/avatar/:mongoId', async (req, res) => {
|
|
try {
|
|
const { mongoId } = req.params;
|
|
|
|
console.log(`[Avatar Debug] Looking up avatar for user mongo_id: ${mongoId}`);
|
|
|
|
// First try to find avatar by user's avataretag
|
|
const userResult = await global.pool.query(`
|
|
SELECT avataretag, username FROM users WHERE mongo_id = $1
|
|
`, [mongoId]);
|
|
|
|
let avatarPath = null;
|
|
|
|
if (userResult.rows.length > 0) {
|
|
const username = userResult.rows[0].username;
|
|
const avataretag = userResult.rows[0].avataretag;
|
|
|
|
// Try method 1: Look up by avataretag -> etag (for users with avataretag set)
|
|
if (avataretag) {
|
|
console.log(`[Avatar Debug] Found user ${username} with avataretag: ${avataretag}`);
|
|
|
|
const avatarResult = await global.pool.query(`
|
|
SELECT url, path FROM avatars WHERE etag = $1
|
|
`, [avataretag]);
|
|
|
|
if (avatarResult.rows.length > 0) {
|
|
const dbPath = avatarResult.rows[0].path || avatarResult.rows[0].url;
|
|
console.log(`[Avatar Debug] Found avatar record with path: ${dbPath}`);
|
|
|
|
if (dbPath) {
|
|
const pathParts = dbPath.split('/');
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
if (pathParts[i].includes('AmazonS3:Avatars') && i + 1 < pathParts.length) {
|
|
const avatarMongoId = pathParts[i + 1];
|
|
avatarPath = path.join(__dirname, 'db-convert/db/files/avatars', avatarMongoId);
|
|
console.log(`[Avatar Debug] Extracted avatar mongo_id: ${avatarMongoId}, full path: ${avatarPath}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`[Avatar Debug] No avatar record found for etag: ${avataretag}`);
|
|
}
|
|
}
|
|
|
|
// Try method 2: Look up by userid directly (for users without avataretag)
|
|
if (!avatarPath) {
|
|
console.log(`[Avatar Debug] Trying direct userid lookup for user ${username} (${mongoId})`);
|
|
|
|
const avatarResult = await global.pool.query(`
|
|
SELECT url, path FROM avatars WHERE userid = $1
|
|
`, [mongoId]);
|
|
|
|
if (avatarResult.rows.length > 0) {
|
|
const dbPath = avatarResult.rows[0].path || avatarResult.rows[0].url;
|
|
console.log(`[Avatar Debug] Found avatar record by userid with path: ${dbPath}`);
|
|
|
|
if (dbPath) {
|
|
const pathParts = dbPath.split('/');
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
if (pathParts[i].includes('AmazonS3:Avatars') && i + 1 < pathParts.length) {
|
|
const avatarMongoId = pathParts[i + 1];
|
|
avatarPath = path.join(__dirname, 'db-convert/db/files/avatars', avatarMongoId);
|
|
console.log(`[Avatar Debug] Extracted avatar mongo_id: ${avatarMongoId}, full path: ${avatarPath}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`[Avatar Debug] No avatar record found for userid: ${mongoId}`);
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`[Avatar Debug] No user found for mongo_id: ${mongoId}`);
|
|
}
|
|
|
|
// Fallback: try direct lookup by user mongo_id
|
|
if (!avatarPath) {
|
|
avatarPath = path.join(__dirname, 'db-convert/db/files/avatars', mongoId);
|
|
console.log(`[Avatar Debug] Using fallback path: ${avatarPath}`);
|
|
}
|
|
|
|
// Set proper content type for images
|
|
res.set('Content-Type', 'image/jpeg'); // Most avatars are likely JPEG
|
|
|
|
// Send the file
|
|
res.sendFile(avatarPath, (err) => {
|
|
if (err) {
|
|
// If avatar doesn't exist, send a default 404 or generate initials
|
|
console.log(`[Avatar Debug] Avatar file not found at path: ${avatarPath}, error:`, err.message);
|
|
if (!res.headersSent) {
|
|
res.status(404).json({ error: 'Avatar not found' });
|
|
}
|
|
} else {
|
|
console.log(`[Avatar Debug] Successfully served avatar from: ${avatarPath}`);
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error serving avatar:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
// Serve avatars statically as fallback
|
|
router.use('/files/avatars', express.static(path.join(__dirname, 'db-convert/db/files/avatars')));
|
|
|
|
// Get all users for the "view as" dropdown (active and inactive)
|
|
router.get('/users', async (req, res) => {
|
|
try {
|
|
const result = await global.pool.query(`
|
|
SELECT id, username, name, type, active, status, lastlogin,
|
|
statustext, utcoffset, statusconnection, mongo_id, avataretag
|
|
FROM users
|
|
WHERE type = 'user'
|
|
ORDER BY
|
|
active DESC, -- Active users first
|
|
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
|
|
// Include archived and closed rooms but sort them at the bottom
|
|
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,
|
|
r.teamid,
|
|
r.archived,
|
|
s.open,
|
|
-- 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
|
|
ORDER BY
|
|
s.open DESC NULLS LAST, -- Open rooms first
|
|
r.archived NULLS FIRST, -- Non-archived first (nulls treated as false)
|
|
r.lm DESC NULLS LAST
|
|
LIMIT 50
|
|
`, [currentUserMongoId]);
|
|
|
|
// Enhance rooms with participant information for direct messages
|
|
const enhancedRooms = await Promise.all(result.rows.map(async (room) => {
|
|
if (room.type === 'd' && room.uids) {
|
|
// Get participant info (excluding current user) for direct messages
|
|
const participantResult = await global.pool.query(`
|
|
SELECT u.username, u.name, u.mongo_id, u.avataretag
|
|
FROM users u
|
|
WHERE u.mongo_id = ANY($1::text[])
|
|
AND u.mongo_id != $2
|
|
`, [room.uids, currentUserMongoId]);
|
|
|
|
room.participants = participantResult.rows;
|
|
}
|
|
return room;
|
|
}));
|
|
|
|
res.json({
|
|
status: 'success',
|
|
rooms: enhancedRooms
|
|
});
|
|
} 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;
|
|
const { userId } = req.query; // Accept current user ID as query parameter
|
|
|
|
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, r.teamid
|
|
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];
|
|
|
|
// For direct messages, get the proper display name based on current user
|
|
if (room.type === 'd' && room.uids && userId) {
|
|
// Get current user's mongo_id
|
|
const userResult = await global.pool.query(`
|
|
SELECT mongo_id FROM users WHERE id = $1
|
|
`, [userId]);
|
|
|
|
if (userResult.rows.length > 0) {
|
|
const currentUserMongoId = userResult.rows[0].mongo_id;
|
|
|
|
// Get display name from subscription table for this user
|
|
// Use room mongo_id to match with subscription.rid
|
|
const roomMongoResult = await global.pool.query(`
|
|
SELECT mongo_id FROM room WHERE id = $1
|
|
`, [roomId]);
|
|
|
|
if (roomMongoResult.rows.length > 0) {
|
|
const roomMongoId = roomMongoResult.rows[0].mongo_id;
|
|
|
|
const subscriptionResult = await global.pool.query(`
|
|
SELECT fname, name FROM subscription
|
|
WHERE rid = $1 AND u->>'_id' = $2
|
|
`, [roomMongoId, currentUserMongoId]);
|
|
|
|
if (subscriptionResult.rows.length > 0) {
|
|
const sub = subscriptionResult.rows[0];
|
|
room.display_name = sub.fname || sub.name || 'Unknown User';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get all participants for additional info
|
|
const participantResult = await global.pool.query(`
|
|
SELECT username, name
|
|
FROM users
|
|
WHERE mongo_id = ANY($1::text[])
|
|
`, [room.uids]);
|
|
|
|
room.participants = participantResult.rows;
|
|
} else {
|
|
// For channels/groups, use room's fname or name
|
|
room.display_name = room.fname || room.name || 'Unnamed Room';
|
|
}
|
|
|
|
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;
|