Server : Apache System : Linux 145.162.205.92.host.secureserver.net 5.14.0-611.45.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Apr 1 05:56:53 EDT 2026 x86_64 User : tradze ( 1001) PHP Version : 8.1.34 Disable Function : NONE Directory : /home/tradze/public_html/test.tradze.com/node-socket/sockets/ |
const moment = require("moment");
const db = require("../config/db"); // import DB connection here
const redis = require("../config/redis");
const axios = require("axios");
// const fetch = require("node-fetch"); // Make sure this is installed
const GOOGLE_API_KEY = "AIzaSyBKNCp4bzgPwiGlg62ZOraLUb8zQB8UaBE"; // Replace with your key
function socketHandler(io) {
return async (socket) => {
const allOnlineUsers = await redis.hgetall("users");
// console.log("socket users:", JSON.stringify(allOnlineUsers, null, 2));
socket.on("register", async (data) => {
const { userId, role, bookingId, providerId, bookingLat, bookingLng } = data;
socket.userId = userId;
socket.role = role;
// users[data.userId] = socket.id;
await redis.hset("users", userId, socket.id);
console.log(`User registered → userId=${userId}, socket=${socket.id}`);
/**
* -----------------------------
* CUSTOMER OPENS LIVE TRACKING
* -----------------------------
*/
if (role === "customer") {
if (bookingId && providerId) {
const providerListKey = `providerBookings:${providerId}`;
const existingBookings = await redis.lrange(providerListKey, 0, -1);
if (existingBookings.length) {
const deletePromises = existingBookings.map(id => redis.del(`booking:${id}`));
await Promise.all(deletePromises);
}
await redis.del(providerListKey);
await redis.hmset(`booking:${bookingId}`, {
bookingId, // IMPORTANT!
providerId,
customerId: userId,
lat: bookingLat,
lng: bookingLng
});
await redis.lpush(providerListKey, bookingId);
console.log(`All previous bookings cleared. New booking saved → provider ${providerId}, booking ${bookingId}`);
}
}
/**
* -----------------------------
* PROVIDER CONNECTS
* -----------------------------
*/
if (role === "provider") {
socket.userId = userId;
console.log(`Provider connected: ${userId}`);
}
});
socket.on("providerLocation", async (data) => {
const providerId = socket.userId;
const { lat, lng } = data;
await redis.hmset(`providerLastLocation:${providerId}`, {
lat,
lng,
lastUpdateAt: Date.now()
});
console.log(`[DEBUG] Received location from provider ${providerId}: ${lat}, ${lng}`);
// --- Get bookings for this provider ---
const providerListKey = `providerBookings:${providerId}`;
const bookingIds = await redis.lrange(providerListKey, 0, -1);
console.log(`[DEBUG] Booking IDs in Redis for provider ${providerId}:`, bookingIds);
if (!bookingIds.length) {
console.log("[DEBUG] No active bookings in Redis for this provider");
return;
}
// Use first booking
const bookingId = bookingIds[0];
const booking = await redis.hgetall(`booking:${bookingId}`);
console.log(`[DEBUG] Booking data fetched from Redis for booking ${bookingId}:`, booking);
if (!booking || !booking.bookingId) {
console.log("[DEBUG] Booking not found or missing bookingId in Redis");
return;
}
// --- Get customer socket ---
const customerSocketId = await redis.hget("users", booking.customerId);
if (!customerSocketId) {
console.log("[DEBUG] Customer socket not found. Customer might be offline.");
return;
}
// --- ETA state ---
const etaKey = `eta:${providerId}:${bookingId}`;
let state = await redis.hgetall(etaKey);
if (!state || !state.initialFetched) {
state = {
lastETA: 0,
lastCallAt: 0,
initialFetched: false,
apiCallCount: 0,
maxApiCallCount: 5
};
} else {
state = {
lastETA: parseInt(state.lastETA),
lastCallAt: parseInt(state.lastCallAt),
apiCallCount: parseInt(state.apiCallCount),
maxApiCallCount: parseInt(state.maxApiCallCount),
initialFetched: state.initialFetched === "true"
};
}
const nowTs = Date.now();
const secondsSinceLastCall = (nowTs - state.lastCallAt) / 1000;
// Decide whether to call Google Maps API
let shouldCallAPI = false;
if (!state.initialFetched) {
shouldCallAPI = true; // First time
} else {
shouldCallAPI = secondsSinceLastCall >= (state.lastETA / 5);
}
console.log(`[DEBUG] shouldCallAPI: ${shouldCallAPI}, apiCallCount: ${state.apiCallCount}, maxApiCallCount: ${state.maxApiCallCount}`);
// --- Calculate ETA to send ---
let etaToSend = state.lastETA;
if (shouldCallAPI && state.apiCallCount < state.maxApiCallCount) {
try {
console.log(`[DEBUG] Calling Google Maps API for booking ${bookingId}`);
const response = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${lat},${lng}&destination=${booking.lat},${booking.lng}&mode=driving&key=${GOOGLE_API_KEY}`
);
const mapData = await response.json();
console.log(mapData)
const leg = mapData?.routes?.[0]?.legs?.[0];
const etaSec = leg?.duration?.value || 900;
console.log(`[DEBUG] Google Maps ETA for booking ${bookingId}: ${etaSec} seconds`);
// First-time API call setup
if (!state.initialFetched) {
let interval = 60;
if (etaSec > 800) interval = 180;
else if (etaSec > 400) interval = 120;
else if (etaSec > 200) interval = 60;
else if (etaSec > 60) interval = 30;
else interval = 15;
state.maxApiCallCount = Math.ceil(etaSec / interval);
}
// Update state
state.lastETA = etaSec;
state.lastCallAt = nowTs;
state.apiCallCount++;
state.initialFetched = true;
await redis.hmset(etaKey, {
lastETA: state.lastETA,
lastCallAt: state.lastCallAt,
initialFetched: true,
apiCallCount: state.apiCallCount,
maxApiCallCount: state.maxApiCallCount
});
etaToSend = etaSec;
} catch (error) {
console.error("[ERROR] Google Maps API call failed:", error);
// Fallback: reduce ETA by time passed since last call
etaToSend = Math.max(0, state.lastETA - Math.floor(secondsSinceLastCall));
}
} else {
// Reduce ETA based on elapsed time since last API call
etaToSend = Math.max(0, state.lastETA - Math.floor(secondsSinceLastCall));
console.log(`[DEBUG] Not calling API, sending decremented ETA: ${etaToSend}`);
}
// --- Determine status ---
let status = "unknown";
if (etaToSend <= 10) status = "reached";
else if (etaToSend <= 60) status = "arriving";
else if (etaToSend <= 300) status = "near";
else if (etaToSend <= 600) status = "approaching";
else status = "far";
// Generate customer-friendly message
let message = "";
switch (status) {
case "reached":
message = "Provider has arrived at your location.";
break;
case "arriving":
message = "Provider is arriving shortly.";
/* ASYNC Send Noti (DO NOT AWAIT) */
sendNotificationAsync(booking.customerId, message,'Service provider tracking');
break;
case "near":
message = "Provider is nearby.";
break;
case "approaching":
message = "Provider is approaching.";
break;
case "far":
message = "Provider is on the way.";
break;
default:
message = "Provider location is being tracked.";
}
// --- Emit updated location & ETA to customer ---
io.to(customerSocketId).emit("providerLocationUpdate", {
providerId,
bookingId,
lat,
lng,
eta: {
minutes: Math.floor(etaToSend / 60),
seconds: etaToSend % 60,
totalSec: etaToSend,
status: status,
message: message
}
});
console.log(`[DEBUG] Sent location & ETA ${etaToSend} sec to customer ${booking.customerId} for booking ${bookingId}`);
// --- Check if provider reached ---
if (etaToSend <= 10) {
await redis.del(`booking:${bookingId}`);
await redis.lrem(providerListKey, 1, bookingId);
await redis.del(etaKey);
console.log(`[DEBUG] Booking ${bookingId} completed and removed from Redis`);
}
});
// Respond to book requests
socket.on("respond-book-request", async (data) => {
console.log('respond-book-request:', data)
const users = await redis.hgetall("users");
let customerSocket = users[data.customer_id];
if (customerSocket) {
if (data.clickedFrom === 'profileModal') {
io.to(customerSocket).emit("respond-book-request-client-modal", data);
} else {
io.to(customerSocket).emit("respond-book-request-client", data);
}
}
});
socket.on("expire-req", async (data) => {
console.log(data)
const users = await redis.hgetall("users");
let customerSocket = users[data.customer_id];
if (customerSocket) {
io.to(customerSocket).emit("expire-req-client", data);
}
});
// New Event: Fetch latest & past book requests
socket.on("get-latest-book-req", async (data) => {
console.log(data);
const users = await redis.hgetall("users");
let customerSocket = users[data.service_provider];
if (customerSocket) {
io.to(customerSocket).emit("get-latest-book-req-response", data);
}
try {
const service_provider = data.service_provider;
const now = moment().utc().format("YYYY-MM-DD HH:mm:ss");
// === 1️⃣ Get latest active request ===
const activeQuery = `SELECT * FROM book_requests WHERE service_provider = ? AND status = 0 AND req_expire > ? ORDER BY created_at DESC LIMIT 1`;
const [activeResults] = await db.promise().query(activeQuery, [service_provider, now]);
let activeReq = activeResults.length ? activeResults[0] : null;
if (activeReq) {
// Parse booking JSON
if (activeReq.booking && typeof activeReq.booking === "string") {
try {
activeReq.booking = JSON.parse(activeReq.booking);
} catch (e) {
activeReq.booking = null;
}
}
// Calculate remaining seconds
const expireTime = moment(activeReq.req_expire);
const diffSeconds = expireTime.diff(moment(now), "seconds");
activeReq.remaining_time_sec = diffSeconds > 0 ? diffSeconds : 0;
}
// === 2️⃣ Get past/expired requests (last 5 days) ===
const fiveDaysAgo = moment(now).subtract(5, "days").format("YYYY-MM-DD HH:mm:ss");
const pastQuery = `
SELECT * FROM book_requests
WHERE service_provider = ?
AND (
req_expire <= ? OR status = 3
)
AND created_at BETWEEN ? AND ?
ORDER BY created_at DESC
`;
const [pastResults] = await db.promise().query(pastQuery, [
service_provider,
now,
fiveDaysAgo,
now,
]);
// Parse JSON for all past requests
const pastRequest = pastResults.map((req) => {
if (req.booking && typeof req.booking === "string") {
try {
req.booking = JSON.parse(req.booking);
} catch (e) {
req.booking = null;
}
}
return req;
});
// === 3️⃣ Emit back to socket ===
io.to(socket.id).emit("get-latest-book-req-response", {
success: true,
message: "Requests fetched successfully!",
activeRequest: activeReq ? [activeReq] : [],
pastRequest: pastRequest,
});
} catch (error) {
console.error("Error fetching book requests:", error);
io.to(socket.id).emit("get-latest-book-req-response", {
success: false,
message: "Something went wrong while fetching book requests.",
error: error.message,
});
}
});
// -----------------------------------
// SEND CHAT MESSAGE (redis users)
// -----------------------------------
socket.on("joinConversation", async ({ conversation_id, user_id }) => {
const room = `conversation_${conversation_id}`;
socket.join(room);
// Optional tracking
await redis.hset(`conversation:${conversation_id}`, user_id, socket.id);
console.log(`User ${user_id} joined ${room}`);
});
// -----------------------------------
// TYPING INDICATOR
// -----------------------------------
socket.on("typing", async ({ conversation_id, user_id, sender_name }) => {
const room = `conversation_${conversation_id}`;
// Broadcast to others (not sender)
socket.to(room).emit("typing", {
user_id,
sender_name,
conversation_id
});
});
socket.on("stopTyping", async ({ conversation_id, user_id }) => {
const room = `conversation_${conversation_id}`;
socket.to(room).emit("stopTyping", {
user_id,
conversation_id
});
});
socket.on("sendChatMessage", async (data) => {
console.log(data)
const { sender_name, sender_id, message, conversation_id, avatar } = data;
if (!sender_id || !conversation_id || !message) return;
const payload = {
sender_id,
sender_name,
message,
conversation_id,
avatar,
timestamp: new Date()
};
const room = `conversation_${conversation_id}`;
io.to(room).emit("newChatMessage", payload);
// Update conversation list for all participants
const participants = await redis.hkeys(`conversation:${conversation_id}`);
// console.log(participants)
participants.forEach(async (participant_id) => {
const socketId = await redis.hget(`conversation:${conversation_id}`, participant_id);
// console.log(participant_id, socketId);
if (socketId) {
try {
// Call your Laravel route
const res = await fetch(`https://test.tradze.com/api/socket/messages?user_id=${participant_id}`);
const convList = await res.json();
// console.log(convList)
// Emit updated conversation list to this participant
io.to(socketId).emit("updateConversationList", convList);
} catch (err) {
console.error("Failed to fetch conversation list:", err);
}
}
});
/* ASYNC SAVE (DO NOT AWAIT) */
saveMessageAsync(conversation_id, sender_id, message);
});
// -----------------------------------
// ON DISCONNECT → REMOVE SOCKET
// -----------------------------------
socket.on("disconnect", async () => {
console.log("User disconnected:", socket.id);
const usersMap = await redis.hgetall("users");
for (const [uid, sock] of Object.entries(usersMap)) {
if (sock === socket.id) {
await redis.hdel("users", uid);
// ALSO emit stopTyping (important fix)
socket.broadcast.emit("stopTyping", {
user_id: uid
});
console.log(`Removed user ${uid} from Redis`);
}
}
});
};
}
module.exports = socketHandler;
// export accessor so controllers always get latest users
// module.exports.getUsers = () => users;
module.exports.getUsers = async () => {
return await redis.hgetall("users");
}
function saveMessageAsync(conversationId, senderId, message) {
console.log('sent to laravel db')
axios.post(
`https://test.tradze.com/api/socket/save-message-node/${conversationId}`,
{
sender_id: senderId,
message: message
},
{
timeout: 10000, // protect Node
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
}
).then(res => console.log(res.data))
.catch(err => {
// IMPORTANT: NEVER crash socket server
console.error("Laravel save failed:", err);
// Optional: push to retry queue (Redis / Bull)
});
}
function sendNotificationAsync(userId, message, notificationHead = 'Tradze') {
console.log('Called send push notification')
axios.post(
`https://test.tradze.com/api/send-push-noti`,
{
userId: userId,
message: message,
notificationHead: notificationHead
},
{
timeout: 10000, // protect Node
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
}
).then(res => console.log(res.data))
.catch(err => {
// IMPORTANT: NEVER crash socket server
console.error("Laravel save failed:", err);
// Optional: push to retry queue (Redis / Bull)
});
}