HEX
Server: Apache
System: Linux vps.mmtprep.com 4.18.0-477.21.1.el8_8.x86_64 #1 SMP Thu Aug 10 13:51:50 EDT 2023 x86_64
User: mmtprep (1001)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: /home/mmtprep/api-mmtprep/controllers/conversationController.js
const db = require("../db/db");
const { v4: uuidv4 } = require('uuid');
const fs = require('fs').promises;
const path = require('path');
const { promisify } = require('util');
const queryAsync = promisify(db.query).bind(db);

const createConversation = async (req, res) => {
    try {
        const { title, members, creatorId } = req.body;
        
        console.log('Conversation creation request received:', { title, membersCount: members?.length, creatorId });
        
        if (!title || !members || !members.length || !creatorId) {
            console.log('Missing required fields for conversation creation');
            return res.status(400).json({ status: 400, message: "Missing required fields" });
        }

        // 고유한 대화 ID 생성
        const conid = uuidv4();
        console.log('Generated conversation ID:', conid);
        
        // conversations 테이블에 대화방 생성 (SQL 파일과 일치하는 필드명 사용)
        let query = `INSERT INTO conversations(conversationId, createdBy, active, groupName) VALUES(?, ?, ?, ?)`;
        
        db.query(query, [conid, creatorId, 1, title], function (err, result) {
            if (err) {
                console.error('Error creating conversation in conversations table:', err);
                return res.status(500).json({ status: 500, message: "Failed to create conversation", error: err.message });
            } else {
                console.log('Successfully created conversation in conversations table');
                
                // 멤버들을 대화방에 추가 - members 또는 conversation_members 테이블 사용
                let groupmems = '';
                const memberInsertPromises = members.map(memberId => {
                    groupmems = groupmems + memberId + ",";
                    // 각 멤버에 대해 고유 memberId 생성
                    const memberUUID = uuidv4();
                    // 테이블 구조에 맞게 memberId 필드 추가
                    const memQuery = `INSERT INTO members(memberId, userId, conversationId) VALUES(?, ?, ?)`;
                    
                    return new Promise((resolve, reject) => {
                        db.query(memQuery, [memberUUID, memberId, conid], (err, result) => {
                            if (err) {
                                console.error(`Error adding member ${memberId} to conversation:`, err);
                                reject(err);
                            } else {
                                console.log(`Successfully added member ${memberId} to conversation`);
                                resolve(result);
                            }
                        });
                    });
                });

                Promise.all(memberInsertPromises)
                    .then(() => {
                        // 성공적으로 생성된 대화 정보 반환
                        const conversationData = {
                            cs_id: conid,
                            title: title,
                            members: members,
                            creator: creatorId,
                            created_at: new Date().toISOString()
                        };
                        
                        console.log('Conversation successfully created with all members added');
                        res.status(200).json({
                            status: 200,
                            message: "Conversation created successfully",
                            data: conversationData
                        });
                    })
                    .catch(memberErr => {
                        console.error('Error adding members to conversation:', memberErr);
                        // 회원 추가 실패 시 롤백 시도
                        const rollbackSql = 'DELETE FROM conversations WHERE conversationId = ?';
                        db.query(rollbackSql, [conid], (rollbackErr, rollbackResult) => {
                            if (rollbackErr) {
                                console.error('Error during rollback:', rollbackErr);
                            } else {
                                console.log('Successfully rolled back conversation creation');
                            }
                            res.status(500).json({ status: 500, message: "Failed to add members to conversation", error: memberErr.message });
                        });
                    });
            }
        });
    } catch (err) {
        console.error('Error in createConversation:', err);
        res.status(500).json({ status: 500, message: "Server error", error: err.message });
    }
}

const getAllConversationsByUserId = async (req, res) => {
    try {
        const userId = req.params.id;
        
        // Add more detailed debugging
        console.log(`=== getAllConversationsByUserId ===`);
        console.log(`User ID: ${userId}`);
        
        // Simplified query to directly get active conversations
        const query = `
        SELECT 
            c.*,
            CASE WHEN c.createdBy = ? THEN 1 ELSE 0 END as isOwner,
            (SELECT COUNT(*) FROM members m WHERE m.conversationId = c.conversationId) as memberCount
        FROM conversations c
        WHERE c.active = 1
            AND (c.createdBy = ? 
                OR c.conversationId IN (SELECT conversationId FROM members WHERE userId = ?))
        `;
        
        console.log(`Executing query for user: ${userId}`);
        db.query(query, [userId, userId, userId], function (err, data) {
            if (err) {
                console.error('Error executing conversation query:', err);
                return res.status(500).json({ error: err.message });
            }
            
            console.log(`Query returned ${data ? data.length : 0} results`);
            
            if (!data || data.length === 0) {
                console.log(`No conversations found for user: ${userId}`);
                
                // Fallback - check if user exists in members table at all
                const checkMembershipQuery = "SELECT COUNT(*) as count FROM members WHERE userId = ?";
                db.query(checkMembershipQuery, [userId], (memberErr, memberResult) => {
                    if (memberErr) {
                        console.error("Error checking member status:", memberErr);
                    } else {
                        console.log(`Member check result: ${JSON.stringify(memberResult)}`);
                        console.log(`User is a member of ${memberResult[0]?.count || 0} conversations`);
                    }
                    
                    // Return empty array
                    return res.status(200).json([]);
                });
                
                return;
            }
            
            // Process the data and make sure we have the expected format
            const processedConversations = data.map(conv => {
                const processed = {
                    conversationId: conv.conversationId,
                    cs_id: conv.conversationId,
                    createdBy: conv.createdBy,
                    title: conv.groupName || 'Untitled Conversation',
                    groupName: conv.groupName || 'Untitled Conversation',
                    active: conv.active === 1, 
                    isOwner: conv.isOwner === 1,
                    memberCount: conv.memberCount || 0,
                    created_at: conv.conversationCreated
                };
                
                console.log(`Processed conversation: ${JSON.stringify(processed)}`);
                return processed;
            });
            
            console.log(`Returning ${processedConversations.length} processed conversations`);
            res.status(200).json(processedConversations);
        });
    } catch (err) {
        console.error('Error in getAllConversationsByUserId:', err);
        res.status(500).json({ error: err.message });
    }
};

const getConversationById = async (req, res) => {
    try {
        let query = `SELECT * FROM messages WHERE conversationId = '${req.params.id}' ORDER BY created_at ASC`;

        db.query(query, function (err, result) {
            if (err) {
                console.error('Error fetching conversation by ID:', err);
                res.status(500).json({ error: err.message });
            } else {
                res.status(200).json(result);
            }
        });
    } catch (err) {
        console.error('Exception in getConversationById:', err);
        res.status(500).json({ error: err.message });
    }
}

// Helper function to safely parse JSON
const safeJSONParse = (str, defaultValue = []) => {
    if (!str) return defaultValue;
    try {
        return JSON.parse(str);
    } catch (e) {
        console.warn('JSON Parse error:', e.message);
        return defaultValue;
    }
};

// Update the processMessages function to properly handle text content
const processMessages = (messages) => {
    return messages.map(message => {
        // Handle reactions
        let reactions = [];
        try {
            if (message.reactions) {
                // If reactions is already an array, use it as is
                if (Array.isArray(message.reactions)) {
                    reactions = message.reactions;
                } 
                // If it's a string, try to parse it
                else if (typeof message.reactions === 'string') {
                    // Handle empty array string case
                    if (message.reactions.trim() === '[]') {
                        reactions = [];
                    } else {
                        reactions = safeJSONParse(message.reactions, []);
                    }
                }
            }
        } catch (e) {
            console.warn(`Warning: Invalid reactions for message ${message.messageId}:`, e.message);
            reactions = [];
        }

        // Handle attachments
        let attachments = [];
        try {
            if (message.attachment) {
                attachments = safeJSONParse(message.attachment, []);
            }
        } catch (e) {
            console.warn(`Warning: Invalid attachment for message ${message.messageId}:`, e.message);
            attachments = [];
        }

        // Ensure text content is properly handled
        const messageText = message.text || '';
        
        // Log the message content for debugging
        console.log('Processing message:', {
            messageId: message.messageId,
            text: messageText,
            content: message.content || messageText,
            hasText: !!messageText
        });

        return {
            messageId: message.messageId,
            mid: message.messageId, // For compatibility
            conversationId: message.conversationId,
            senderId: message.senderId,
            uid: message.senderId, // For compatibility
            senderName: message.senderName,
            text: messageText,
            content: messageText, // Include both text and content for compatibility
            readStatus: Boolean(message.readStatus),
            readBy: message.readBy,
            created_at: message.created_at ? new Date(message.created_at).toISOString() : null,
            messageCreated: message.created_at ? new Date(message.created_at).toISOString() : null, // For compatibility
            attachments,
            reactions,
            isPinned: Boolean(message.isPinned),
            replyTo: message.replyToId ? {
                replyToId: message.replyToId,
                replyToText: message.replyToText,
                replyToSenderName: message.replyToSenderName
            } : null
        };
    });
};

// Update the getMessagesByConversationId function
const getMessagesByConversationId = async (req, res) => {
    const conversationId = req.params.id;
    const limit = parseInt(req.query.limit) || 50;
    const offset = parseInt(req.query.offset) || 0;

    try {
        console.log(`Loading messages for conversation: ${conversationId}`);

        const query = `
                SELECT 
                    messageId,
                    conversationId,
                    senderId,
                    senderName,
                    text,
                    readStatus,
                    readBy,
                    created_at,
                    reactions,
                    attachment,
                isPinned,
                replyToId,
                replyToText,
                link
                                FROM messages
            WHERE conversationId = ? 
            ORDER BY created_at ASC
            LIMIT ? OFFSET ?
        `;

        const [messages] = await db.promise().query(query, [conversationId, limit, offset]);
        console.log(`Found ${messages.length} messages for conversation: ${conversationId}`);

        // Process messages with the updated helper function
        const processedMessages = processMessages(messages);

        // Log the first few processed messages for debugging
        console.log('Sample of processed messages:', 
            processedMessages.slice(0, 2).map(msg => ({
                messageId: msg.messageId,
                text: msg.text,
                content: msg.content,
                hasText: !!msg.text
            }))
        );

        // Get total count for pagination
        const [countResult] = await db.promise().query(
            'SELECT COUNT(*) as total FROM messages WHERE conversationId = ?',
            [conversationId]
        );

        const total = countResult[0].total;
        const hasMore = offset + limit < total;

        res.json({
            messages: processedMessages,
            pagination: {
                total,
                limit,
                offset,
                hasMore
            }
        });

    } catch (err) {
        console.error('Error in getMessagesByConversationId:', err);
        res.status(500).json({
            error: 'Failed to fetch messages',
            details: err.message
        });
    }
};

const getMembersByConversationId = async (req, res) => {
    try {
        const conversationId = req.params.id;
        console.log(`Fetching members for conversation: ${conversationId}`);
        
        const query = `SELECT userId as uid FROM members WHERE conversationId = ?`;
        
        db.query(query, [conversationId], function (error, data) {
            if (error) {
                console.error('Error fetching members:', error);
                return res.status(500).json({ error: error.message });
            }
            
            if (!data || data.length === 0) {
                console.log(`No members found for conversation: ${conversationId}`);
                return res.status(200).json([]);
            }
            
            let members = data.map((el) => el.uid);
            console.log(`Found ${members.length} members for conversation: ${conversationId}`);
            
            res.status(200).json(members);
        });
    } catch (error) {
        console.error('Error in getMembersByConversationId:', error);
        res.status(500).json({ error: error.message });
    }
}

const postMessageToConversation = async (req, res) => {
    // Declare variables at the top of the function scope
        const conversationId = req.params.id;
        const { messageId, senderId, senderName, text, attachments, replyTo } = req.body;

    // Log the incoming request data for debugging
    console.log('Received message data:', {
        messageId,
        senderId,
        senderName,
        text,
        hasAttachments: !!attachments?.length,
        replyTo
    });

    try {
        // Validation
        if (!messageId || !senderId || (!text && (!attachments || attachments.length === 0))) {
            return res.status(400).json({ 
                error: "Missing required fields", 
                requiredFields: ["messageId", "senderId", "text (or attachments)"] 
            });
        }

        console.log(`Checking for existing message: ${messageId}`);
        if (replyTo) {
            console.log('Reply data:', replyTo);
        }

        // First check if message already exists - using a transaction for atomicity
        const connection = await db.promise().getConnection();
        try {
            await connection.beginTransaction();

            // Check for existing message with FOR UPDATE to lock the row
            const [existingMessages] = await connection.query(
                'SELECT * FROM messages WHERE messageId = ? FOR UPDATE',
                [messageId]
            );

            if (existingMessages.length > 0) {
                await connection.commit();
                const existingMessage = existingMessages[0];
                console.log(`Found existing message: ${messageId}`);

                // Safely parse JSON fields
                const parsedAttachments = safeJSONParse(existingMessage.attachment);
                const parsedReactions = safeJSONParse(existingMessage.reactions);

                return res.status(200).json({
                    success: true,
                    messageId: existingMessage.messageId,
                    senderName: existingMessage.senderName,
                    text: existingMessage.text,
                    created_at: existingMessage.created_at,
                    attachments: parsedAttachments,
                    reactions: parsedReactions,
                    replyTo: existingMessage.replyToId ? {
                        replyToId: existingMessage.replyToId,
                        replyToText: existingMessage.replyToText,
                        replyToSenderName: existingMessage.replyToSenderName
                    } : null,
                    message: "Message already exists"
                });
            }

            // Message doesn't exist, prepare for insertion
            const messageText = text || '';
            const attachmentsJson = attachments && attachments.length > 0 
                ? JSON.stringify(attachments) 
                : null;
            const emptyReactions = JSON.stringify([]);

            // Process reply data
            let replyToId = null;
            let replyToText = null;
            let replyToSenderName = null;

            if (replyTo && typeof replyTo === 'object') {
                replyToId = replyTo.replyToId || replyTo.messageId;
                replyToText = replyTo.replyToText || replyTo.text || replyTo.content;
                replyToSenderName = replyTo.replyToSenderName || replyTo.senderName;
                
                console.log('Processing reply data:', {
                    replyToId,
                    replyToText,
                    replyToSenderName
                });
            }

            // Insert the new message
            const insertQuery = `
            INSERT INTO messages (
                messageId, 
                conversationId, 
                senderId,
                senderName, 
                text, 
                readStatus, 
                readBy,
                replyToId,
                replyToText,
                    replyToSenderName,
                attachment,
                    reactions,
                    created_at,
                    link
                ) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, NOW(), ?)
            `;

            await connection.query(
                insertQuery,
                [
                    messageId,
                    conversationId,
                    senderId,
                    senderName,
                    messageText,
                    senderId,
                    replyToId,
                    replyToText,
                    replyToSenderName,
                    attachmentsJson,
                    emptyReactions,
                    ''
                ]
            );

            await connection.commit();
            console.log(`Successfully inserted new message: ${messageId}`);
            if (replyToId) {
                console.log(`Message is a reply to: ${replyToId}`);
            }

            // Return success response
            return res.status(201).json({
                success: true,
                messageId,
                senderName,
                text: messageText,
                created_at: new Date().toISOString(),
                attachments: attachments || [],
                reactions: [],
                replyTo: replyToId ? {
                    replyToId,
                    replyToText,
                    replyToSenderName
                } : null
            });

        } catch (error) {
            await connection.rollback();
            throw error;
        } finally {
            connection.release();
        }

    } catch (error) {
        console.error('Error in postMessageToConversation:', error);
        
        // Handle duplicate entry error specifically
        if (error.code === 'ER_DUP_ENTRY') {
            try {
                // Try one more time to get the existing message
                const [existingMessage] = await db.promise().query(
                    'SELECT * FROM messages WHERE messageId = ?',
                    [messageId]
                );

                if (existingMessage.length > 0) {
                    const message = existingMessage[0];
                    // Safely parse JSON fields
                    const parsedAttachments = safeJSONParse(message.attachment);
                    const parsedReactions = safeJSONParse(message.reactions);

                    return res.status(200).json({
                        success: true,
                        messageId: message.messageId,
                        senderName: message.senderName,
                        text: message.text,
                        created_at: message.created_at,
                        attachments: parsedAttachments,
                        reactions: parsedReactions,
                        replyTo: message.replyToId ? {
                            replyToId: message.replyToId,
                            replyToText: message.replyToText,
                            replyToSenderName: message.replyToSenderName
                        } : null,
                        message: "Message already exists"
                    });
                }
            } catch (fetchError) {
                console.error('Error fetching existing message:', fetchError);
            }

            // Fallback response if we couldn't fetch the existing message
            return res.status(200).json({
                success: true,
                messageId,
                senderName,
                text: text || '',
                created_at: new Date().toISOString(),
                attachments: attachments || [],
                reactions: [],
                message: "Message already exists (fallback response)"
            });
        }
        
        // For other errors
        res.status(500).json({ 
            error: 'Failed to save message',
            details: error.message 
        });
    }
};

// Utility function to get user names by ID
const getUserNames = async (userIds) => {
    return new Promise((resolve, reject) => {
        if (!userIds || userIds.length === 0) {
            resolve({});
            return;
        }
        
        const placeholders = userIds.map(() => '?').join(',');
        const query = `SELECT id, Fname, Lname FROM accounts WHERE id IN (${placeholders})`;
        
        db.query(query, userIds, (error, results) => {
            if (error) {
                console.error('Error fetching user names:', error);
                resolve({}); // Return empty object on error
            } else {
                const userMap = {};
                results.forEach(user => {
                    userMap[user.id] = `${user.Fname || ''} ${user.Lname || ''}`.trim() || 'Unknown User';
                });
                resolve(userMap);
            }
        });
    });
};

const deleteConversation = async (req, res) => {
    try {
        const conversationId = req.params.id;
        const userId = req.body.userId;
        
        console.log(`Attempting to delete conversation: ${conversationId} by user: ${userId}`);

        if (!conversationId) {
            return res.status(400).json({ error: "Conversation ID is required" });
        }
        
        if (!userId) {
            return res.status(400).json({ error: "User ID is required" });
        }
        
        // First, verify the user is the creator of the conversation
        const ownerCheckResult = await new Promise((resolve, reject) => {
            const checkQuery = 'SELECT createdBy FROM conversations WHERE conversationId = ?';
            db.query(checkQuery, [conversationId], (err, results) => {
                if (err) {
                    console.error('Error checking conversation ownership:', err);
                    reject(err);
                } else {
                    resolve(results);
                }
            });
        });
        
        // If conversation doesn't exist or user is not the creator
        if (ownerCheckResult.length === 0) {
            return res.status(404).json({ error: "Conversation not found" });
        }
        
        const creatorId = ownerCheckResult[0].createdBy;
        if (creatorId !== userId) {
            console.log(`Permission denied: User ${userId} attempted to delete conversation ${conversationId} created by ${creatorId}`);
            return res.status(403).json({ error: "You do not have permission to delete this conversation" });
        }
        
        console.log(`User ${userId} authorized to delete conversation ${conversationId}`);
        
        // 1. Get all messages from this conversation
        const messages = await new Promise((resolve, reject) => {
            const query = 'SELECT messageId, attachment FROM messages WHERE conversationId = ?';
            db.query(query, [conversationId], (err, results) => {
                if (err) {
                    console.error('Error fetching messages for deletion:', err);
                    reject(err);
                } else {
                    resolve(results);
                }
            });
        });

        // 2. Check for attachment files that need to be deleted from storage
        if (messages.length > 0) {
            // Handle physical attachment files - they might be stored in the chat directory
            const chatDir = path.join(__dirname, '..', 'chat', conversationId);
            console.log(`Checking for attachments in directory: ${chatDir}`);
            
            try {
                // Check if the directory exists before attempting to remove it
                await fs.access(chatDir);
                
                // Directory exists, so delete recursively
                await fs.rm(chatDir, { recursive: true, force: true });
                console.log(`Successfully deleted chat directory: ${chatDir}`);
            } catch (fileErr) {
                // If directory doesn't exist, just log and continue
                console.log(`No chat directory found at ${chatDir} or error removing: ${fileErr.message}`);
            }

            // 3. Delete the messages
            await new Promise((resolve, reject) => {
                const query = 'DELETE FROM messages WHERE conversationId = ?';
                db.query(query, [conversationId], (err, results) => {
                    if (err) {
                        console.error('Error deleting messages:', err);
                        reject(err);
                    } else {
                        console.log(`Deleted ${results.affectedRows} messages from database`);
                        resolve(results);
                    }
                });
            });
        }

        // 4. Delete members from this conversation
        await new Promise((resolve, reject) => {
            const query = 'DELETE FROM members WHERE conversationId = ?';
            db.query(query, [conversationId], (err, results) => {
                if (err) {
                    console.error('Error deleting members:', err);
                    reject(err);
                } else {
                    console.log(`Deleted ${results.affectedRows} members from database`);
                    resolve(results);
                }
            });
        });

        // 5. Finally, delete the conversation itself
        await new Promise((resolve, reject) => {
            const query = 'DELETE FROM conversations WHERE conversationId = ?';
            db.query(query, [conversationId], (err, results) => {
                if (err) {
                    console.error('Error deleting conversation:', err);
                    reject(err);
                } else {
                    console.log(`Deleted conversation ${conversationId} from database`);
                    resolve(results);
                }
            });
        });

        res.status(200).json({ 
            message: 'Conversation and messages deleted successfully',
            conversationId
        });
    } catch (err) {
        console.error('Error in deleteConversation:', err);
        res.status(500).json({ error: err.message });
    }
};

module.exports = { 
    getAllConversationsByUserId, 
    getConversationById, 
    createConversation, 
    getMessagesByConversationId, 
    getMembersByConversationId, 
    postMessageToConversation,
    getUserNames,
    deleteConversation
}