270 lines
8.5 KiB
JavaScript
270 lines
8.5 KiB
JavaScript
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;
|