api-v1/routes/chatRoutes.js

267 lines
9.6 KiB
JavaScript

import express from "express";
import pool from "../db/pool.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 queryCheck = `
SELECT * FROM conversations
WHERE (buyer_id = $1 AND seller_id = $2)
OR (buyer_id = $2 AND seller_id = $1)
AND deleted = FALSE
`;
const checkResult = await pool.query(queryCheck, [buyer_id, seller_id]);
if (checkResult.rows.length > 0) {
return res.json(checkResult.rows[0]);
}
// Create new
const queryInsert = `
INSERT INTO conversations (buyer_id, seller_id)
VALUES ($1, $2)
RETURNING *
`;
const insertResult = await pool.query(queryInsert, [buyer_id, seller_id]);
res.status(201).json(insertResult.rows[0]);
} 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 queryText = `
SELECT
c.*,
CASE
WHEN c.buyer_id = $1 THEN u_seller.name
ELSE u_buyer.name
END as other_user_name,
CASE
WHEN c.buyer_id = $1 THEN u_seller.avatar_url
ELSE u_buyer.avatar_url
END as other_user_avatar,
CASE
WHEN c.buyer_id = $1 THEN u_seller.id
ELSE u_buyer.id
END as other_user_id
FROM conversations c
JOIN users u_buyer ON c.buyer_id = u_buyer.id
JOIN users u_seller ON c.seller_id = u_seller.id
WHERE (c.buyer_id = $1 OR c.seller_id = $1)
AND c.deleted = FALSE
ORDER BY c.updated_at DESC
`;
const result = await pool.query(queryText, [userId]);
res.json(result.rows);
} 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 queryText = `
SELECT
m.*
FROM messages m
WHERE m.conversation_id = $1
AND m.deleted = FALSE
ORDER BY m.created_at DESC
LIMIT $2 OFFSET $3
`;
const result = await pool.query(queryText, [conversationId, limit, offset]);
// Reverse for frontend if needed, but API usually sends standard order.
// Sending newest first (DESC) is common for pagination.
res.json(result.rows);
} 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) => {
const client = await pool.connect();
try {
await client.query("BEGIN");
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 insertMessageQuery = `
INSERT INTO messages (
conversation_id, sender_id, receiver_id, message_type, content,
message_media, media_type, media_metadata
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`;
const messageResult = await client.query(insertMessageQuery, [
conversation_id,
sender_id,
receiver_id,
message_type,
content,
media_url || null,
media_type || null,
media_metadata || null
]);
await client.query("COMMIT");
// Real-time update via Socket.io
const receiverSocketId = getSocketId(receiver_id);
if (receiverSocketId) {
getIO().to(receiverSocketId).emit("receive_message", messageResult.rows[0]);
}
// else {
// // Receiver is OFFLINE: Send Push Notification
// const fcmQuery = `SELECT fcm_token FROM user_devices WHERE user_id = $1 AND fcm_token IS NOT NULL AND is_active = TRUE`;
// const fcmResult = await pool.query(fcmQuery, [receiver_id]);
// if (fcmResult.rows.length > 0) {
// const tokens = fcmResult.rows.map(row => row.fcm_token);
// // Title could be sender's name if we fetched it, or generic. For speed, generic "New Message".
// // Ideally, we'd join sender info in the SELECT or pass it if available.
// 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.rows[0].id
// });
// }
// }
res.status(201).json(messageResult.rows[0]);
} catch (err) {
await client.query("ROLLBACK");
console.error("Error sending message:", err);
res.status(500).json({ error: "Internal server error" });
} finally {
client.release();
}
});
// 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 queryText = `
UPDATE messages
SET is_read = TRUE, read_at = NOW()
WHERE conversation_id = $1
AND receiver_id = $2
AND is_read = FALSE
RETURNING id, sender_id, conversation_id
`;
const result = await pool.query(queryText, [conversationId, userId]);
// Even if 0 rows updated, we return success (idempotent)
res.json({
message: "Messages marked as read",
updated_count: result.rows.length,
updated_messages: result.rows
});
// 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.rows.length > 0) {
const uniqueSenders = [...new Set(result.rows.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.rows.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) => {
const client = await pool.connect();
try {
const {
conversation_id,
buyer_id,
seller_id,
communication_type,
call_status,
duration_seconds,
call_recording_url
} = req.body;
await client.query("BEGIN");
const insertQuery = `
INSERT INTO communication_records (
conversation_id, buyer_id, seller_id,
communication_type, call_status, duration_seconds, call_recording_url
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`;
const result = await client.query(insertQuery, [
conversation_id, buyer_id, seller_id,
communication_type, call_status, duration_seconds || 0, call_recording_url
]);
await client.query("COMMIT");
res.status(201).json(result.rows[0]);
} catch (err) {
await client.query("ROLLBACK");
console.error("Error logging communication:", err);
res.status(500).json({ error: "Internal server error" });
} finally {
client.release();
}
});
export default router;