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
}