import express from "express"; import { insert, select, update, execute } from "../db/queryHelper/index.js"; import { getIO, getSocketId } from "../socket.js"; // import { sendPushNotification } from "../utils/fcm.js"; const router = express.Router(); // 1. POST /conversations (Create or Get existing) router.post("/conversations", async (req, res) => { try { // We expect explicit buyer/seller ID, or just two user IDs. // Based on schema, we have buyer_id and seller_id. // Ideally, the client sends which is which, or we just treat them as user1/user2 and check both permutations. const { buyer_id, seller_id } = req.body; if (!buyer_id || !seller_id) { return res.status(400).json({ error: "buyer_id and seller_id are required" }); } // Check if conversation exists (bidirectional check for robustness, although schema has specific columns) const checkResult = await execute({ type: 'raw-builder', handler: async (knex) => { return await knex('conversations') .where(function() { this.where({ buyer_id, seller_id }) .orWhere({ buyer_id: seller_id, seller_id: buyer_id }); }) .where({ deleted: false }); } }); if (checkResult.length > 0) { return res.json(checkResult[0]); } // Create new const insertResult = await insert({ table: 'conversations', data: { buyer_id, seller_id }, returning: '*' }); res.status(201).json(insertResult); } catch (err) { console.error("Error creating/getting conversation:", err); res.status(500).json({ error: "Internal server error" }); } }); // 2. GET /conversations/user/:userId (Inbox) router.get("/conversations/user/:userId", async (req, res) => { try { const { userId } = req.params; // Fetch conversations where user is involved. // Also fetch the OTHER user's name/avatar. const result = await execute({ type: 'raw-builder', handler: async (knex) => { return await knex('conversations as c') .select( 'c.*', knex.raw(`CASE WHEN c.buyer_id = ? THEN u_seller.name ELSE u_buyer.name END as other_user_name`, [userId]), knex.raw(`CASE WHEN c.buyer_id = ? THEN u_seller.avatar_url ELSE u_buyer.avatar_url END as other_user_avatar`, [userId]), knex.raw(`CASE WHEN c.buyer_id = ? THEN u_seller.id ELSE u_buyer.id END as other_user_id`, [userId]) ) .join('users as u_buyer', 'c.buyer_id', 'u_buyer.id') .join('users as u_seller', 'c.seller_id', 'u_seller.id') .where(function() { this.where('c.buyer_id', userId).orWhere('c.seller_id', userId); }) .where('c.deleted', false) .orderBy('c.updated_at', 'desc'); } }); res.json(result); } catch (err) { console.error("Error fetching user conversations:", err); res.status(500).json({ error: "Internal server error" }); } }); // 3. GET /conversations/:conversationId/messages (History) router.get("/conversations/:conversationId/messages", async (req, res) => { try { const { conversationId } = req.params; const { limit = 50, offset = 0 } = req.query; const result = await select({ table: 'messages', where: { conversation_id: conversationId, deleted: false }, orderBy: { column: 'created_at', direction: 'desc' }, limit: parseInt(limit), offset: parseInt(offset) }); // Reverse for frontend if needed, but API usually sends standard order. // Sending newest first (DESC) is common for pagination. res.json(result); } catch (err) { console.error("Error fetching messages:", err); res.status(500).json({ error: "Internal server error" }); } }); // 4. POST /messages (Send Message) router.post("/messages", async (req, res) => { try { const { conversation_id, sender_id, receiver_id, content, message_type = 'text', media_url, media_type, media_metadata } = req.body; // Insert Message with embedded media fields const messageData = { conversation_id, sender_id, receiver_id, message_type, content, message_media: media_url || null, media_type: media_type || null, media_metadata: media_metadata || null }; const messageResult = await insert({ table: 'messages', data: messageData, returning: '*' }); // Real-time update via Socket.io const receiverSocketId = getSocketId(receiver_id); if (receiverSocketId) { getIO().to(receiverSocketId).emit("receive_message", messageResult); } // else { // // Receiver is OFFLINE: Send Push Notification // const fcmResult = await select({ // table: 'user_devices', // columns: ['fcm_token'], // where: { // user_id: receiver_id, // is_active: true // } // }); // const tokens = fcmResult.filter(row => row.fcm_token).map(row => row.fcm_token); // if (tokens.length > 0) { // const notificationTitle = "New Message"; // const notificationBody = message_type === 'text' ? (content.substring(0, 100) + (content.length > 100 ? "..." : "")) : "Sent a media file"; // sendPushNotification(tokens, notificationTitle, notificationBody, { // type: "new_message", // conversation_id: conversation_id, // message_id: messageResult.id // }); // } // } res.status(201).json(messageResult); } catch (err) { console.error("Error sending message:", err); res.status(500).json({ error: "Internal server error" }); } }); // 5. PUT /conversations/:conversationId/read (Mark Conversation as Read) router.put("/conversations/:conversationId/read", async (req, res) => { try { const { conversationId } = req.params; const { userId } = req.body; // The user who is reading the messages if (!userId) { return res.status(400).json({ error: "userId is required" }); } const result = await execute({ type: 'raw-builder', handler: async (knex) => { return await knex('messages') .where({ conversation_id: conversationId, receiver_id: userId, is_read: false }) .update({ is_read: true, read_at: knex.fn.now() }) .returning(['id', 'sender_id', 'conversation_id']); } }); // Even if 0 rows updated, we return success (idempotent) res.json({ message: "Messages marked as read", updated_count: result.length, updated_messages: result }); // Real-time update via Socket.io // Notify the SENDER(s) that their messages have been read. // In a 1-on-1 chat, this is just one person. if (result.length > 0) { const uniqueSenders = [...new Set(result.map(m => m.sender_id))]; for (const senderId of uniqueSenders) { const senderSocketId = getSocketId(senderId); if (senderSocketId) { getIO().to(senderSocketId).emit("conversation_read", { conversation_id: conversationId, read_by_user_id: userId, updated_count: result.length // Simplification: total count, not per sender, but fine for 1:1 }); } } } } catch (err) { console.error("Error marking conversation as read:", err); res.status(500).json({ error: "Internal server error" }); } }); // 6. POST /communications (Log Call/Communication) router.post("/communications", async (req, res) => { try { const { conversation_id, buyer_id, seller_id, communication_type, call_status, duration_seconds, call_recording_url } = req.body; const communicationData = { conversation_id, buyer_id, seller_id, communication_type, call_status, duration_seconds: duration_seconds || 0, call_recording_url }; const result = await insert({ table: 'communication_records', data: communicationData, returning: '*' }); res.status(201).json(result); } catch (err) { console.error("Error logging communication:", err); res.status(500).json({ error: "Internal server error" }); } }); export default router;