const client = require("../graphql-client/graphql-client").client; const { FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, UNARCHIVE_CONVERSATION, CREATE_CONVERSATION, INSERT_MESSAGE } = require("../graphql-client/queries"); const { phone } = require("phone"); const { admin } = require("../firebase/firebase-handler"); const InstanceManager = require("../utils/instanceMgr").default; /** * Receive SMS messages from Twilio and process them * @param req * @param res * @returns {Promise<*>} */ const receive = async (req, res) => { console.dir(req.body); const { logger, ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } } = req; const loggerData = { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body, logger) }; logger.log("sms-inbound", "DEBUG", "api", null, loggerData); if (!req.body || !req.body.MessagingServiceSid || !req.body.SmsMessageSid) { logger.log("sms-inbound-error", "ERROR", "api", null, { ...loggerData, type: "malformed-request" }); return res.status(400).json({ success: false, error: "Malformed Request" }); } try { // Step 1: Find the bodyshop and existing conversation const response = await client.request(FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, { mssid: req.body.MessagingServiceSid, phone: phone(req.body.From).phoneNumber }); if (!response.bodyshops[0]) { return res.status(400).json({ success: false, error: "No matching bodyshop" }); } const bodyshop = response.bodyshops[0]; // Step 4: Process conversation const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); const existingConversation = sortedConversations.length ? sortedConversations[sortedConversations.length - 1] : null; let conversationid; let newMessage = { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body, logger), isoutbound: false, userid: null }; if (existingConversation) { conversationid = existingConversation.id; if (existingConversation.archived) { await client.request(UNARCHIVE_CONVERSATION, { id: conversationid, archived: false }); } } else { const newConversationResponse = await client.request(CREATE_CONVERSATION, { conversation: { bodyshopid: bodyshop.id, phone_num: phone(req.body.From).phoneNumber, archived: false } }); const createdConversation = newConversationResponse.insert_conversations.returning[0]; conversationid = createdConversation.id; } newMessage.conversationid = conversationid; // Step 5: Insert the message const insertresp = await client.request(INSERT_MESSAGE, { msg: newMessage, conversationid }); const message = insertresp?.insert_messages?.returning?.[0]; const conversation = message?.conversation || null; if (!conversation) { throw new Error("Conversation data is missing from the response."); } // Step 6: Notify clients const conversationRoom = getBodyshopConversationRoom({ bodyshopId: conversation.bodyshop.id, conversationId: conversation.id }); const commonPayload = { isoutbound: false, conversationId: conversation.id, updated_at: message.updated_at, msid: message.sid }; const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id); ioRedis.to(broadcastRoom).emit("new-message-summary", { ...commonPayload, existingConversation: !!existingConversation, newConversation: !existingConversation ? conversation : null, summary: true }); ioRedis.to(conversationRoom).emit("new-message-detailed", { newMessage: message, ...commonPayload, newConversation: !existingConversation ? conversation : null, existingConversation: !!existingConversation, summary: false }); // Step 7: Send FCM notification const fcmresp = await admin.messaging().send({ topic: `${message.conversation.bodyshop.imexshopid}-messaging`, notification: { title: InstanceManager({ imex: `ImEX Online Message - ${message.conversation.phone_num}`, rome: `Rome Online Message - ${message.conversation.phone_num}` }), body: message.image_path ? `Image ${message.text}` : message.text }, data: { type: "messaging-inbound", conversationid: message.conversationid || "", text: message.text || "", messageid: message.id || "", phone_num: message.conversation.phone_num || "" } }); logger.log("sms-inbound-success", "DEBUG", "api", null, { newMessage, fcmresp }); res.status(200).send(""); } catch (e) { handleError(req, e, res, "RECEIVE_MESSAGE", logger); } }; /** * Generate media array from the request body * @param body * @param logger * @returns {null|*[]} */ const generateMediaArray = (body, logger) => { const { NumMedia } = body; if (parseInt(NumMedia) > 0) { const ret = []; for (let i = 0; i < parseInt(NumMedia); i++) { ret.push(body[`MediaUrl${i}`]); } return ret; } else { return null; } }; /** * Handle error logging and response * @param req * @param error * @param res * @param context * @param logger */ const handleError = (req, error, res, context, logger) => { logger.log("sms-inbound-error", "ERROR", "api", null, { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body, logger), messagingServiceSid: req.body.MessagingServiceSid, context, error }); res.status(500).json({ error: error.message || "Internal Server Error" }); }; module.exports = { receive };