236 lines
7.8 KiB
JavaScript
236 lines
7.8 KiB
JavaScript
import express from "express";
|
|
import pool from "../db/pool.js";
|
|
import { getIO, getSocketId } from "../socket.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
|
|
]);
|
|
|
|
// Update conversation timestamp
|
|
const updateConvQuery = `UPDATE conversations SET updated_at = NOW() WHERE id = $1`;
|
|
await client.query(updateConvQuery, [conversation_id]);
|
|
|
|
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]);
|
|
}
|
|
|
|
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 /messages/:messageId/read (Mark Read)
|
|
router.put("/messages/:messageId/read", async (req, res) => {
|
|
try {
|
|
const { messageId } = req.params;
|
|
const queryText = `
|
|
UPDATE messages
|
|
SET is_read = TRUE, read_at = NOW()
|
|
WHERE id = $1
|
|
RETURNING *
|
|
`;
|
|
const result = await pool.query(queryText, [messageId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "Message not found" });
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
|
|
// Real-time update via Socket.io to the sender (so they know it's read)
|
|
const updatedMessage = result.rows[0];
|
|
const senderId = updatedMessage.sender_id;
|
|
const senderSocketId = getSocketId(senderId);
|
|
|
|
if (senderSocketId) {
|
|
getIO().to(senderSocketId).emit("message_read", updatedMessage);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error marking message 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
|
|
]);
|
|
|
|
// Trigger updates conversation via DB trigger, but we might want to emit socket event?
|
|
// For now, just return success.
|
|
|
|
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;
|