From 261353b51113f11737cc61ac1717461f2a689e95 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 11:35:30 -0800 Subject: [PATCH] feature/IO-3000-messaging-sockets-migrations2 - Base cleanup Signed-off-by: Dave Richer --- .../chat-affix/chat-affix.container.jsx | 79 +-------- .../registerMessagingSocketHandlers.js | 27 ++-- .../chat-conversation.container.jsx | 15 +- .../profile-shops/profile-shops.container.jsx | 2 +- client/src/contexts/SocketIO/useSocket.js | 1 - client/src/utils/fcm-handler.js | 70 -------- server/sms/receive.js | 78 ++++----- server/sms/send.js | 151 ++++++++---------- server/web-sockets/redisSocketEvents.js | 27 ++-- 9 files changed, 145 insertions(+), 305 deletions(-) delete mode 100644 client/src/utils/fcm-handler.js diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 0c13d8327..951783f8a 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -1,11 +1,6 @@ import { useApolloClient } from "@apollo/client"; -import { getToken, onMessage } from "@firebase/messaging"; -import { Button, notification, Space } from "antd"; -import axios from "axios"; import React, { useContext, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { messaging, requestForToken } from "../../firebase/firebase.utils"; -import FcmHandler from "../../utils/fcm-handler"; import ChatPopupComponent from "../chat-popup/chat-popup.component"; import "./chat-affix.styles.scss"; import SocketContext from "../../contexts/SocketIO/socketContext"; @@ -20,81 +15,17 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { if (!bodyshop || !bodyshop.messagingservicesid) return; //Register WS handlers - registerMessagingHandlers({ socket, client }); - - async function SubscribeToTopic() { - try { - const r = await axios.post("/notifications/subscribe", { - fcm_tokens: await getToken(messaging, { - vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY - }), - type: "messaging", - imexshopid: bodyshop.imexshopid - }); - console.log("FCM Topic Subscription", r.data); - } catch (error) { - console.log("Error attempting to subscribe to messaging topic: ", error); - notification.open({ - key: "fcm", - type: "warning", - message: t("general.errors.fcm"), - btn: ( - - - - - ) - }); - } + if (socket && socket.connected) { + registerMessagingHandlers({ socket, client }); } - SubscribeToTopic(); - // eslint-disable-next-line react-hooks/exhaustive-deps - return () => { - unregisterMessagingHandlers({ socket }); + if (socket && socket.connected) { + unregisterMessagingHandlers({ socket }); + } }; }, [bodyshop, socket, t, client]); - useEffect(() => { - function handleMessage(payload) { - FcmHandler({ - client, - payload: (payload && payload.data && payload.data.data) || payload.data - }); - } - - let stopMessageListener, channel; - try { - stopMessageListener = onMessage(messaging, handleMessage); - channel = new BroadcastChannel("imex-sw-messages"); - channel.addEventListener("message", handleMessage); - } catch (error) { - console.log("Unable to set event listeners."); - } - return () => { - stopMessageListener && stopMessageListener(); - channel && channel.removeEventListener("message", handleMessage); - }; - }, [client]); - if (!bodyshop || !bodyshop.messagingservicesid) return <>; return ( diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 08d2e86bb..c6cd66c0e 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -1,8 +1,9 @@ import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; -export function registerMessagingHandlers({ socket, client }) { +export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; - function handleNewMessageSummary(message) { + + const handleNewMessageSummary = (message) => { console.log("🚀 ~ SUMMARY CONSOLE LOG:", message); if (!message.isoutbound) { @@ -69,9 +70,9 @@ export function registerMessagingHandlers({ socket, client }) { } }); } - } + }; - function handleNewMessageDetailed(message) { + const handleNewMessageDetailed = (message) => { console.log("🚀 ~ DETAIL CONSOLE LOG:", message); //They're looking at the conversation right now. Need to merge into the list of messages i.e. append to the end. //Add the message to the overall cache. @@ -97,9 +98,9 @@ export function registerMessagingHandlers({ socket, client }) { // We got this as a receive. else { } - } + }; - function handleMessageChanged(message) { + const handleMessageChanged = (message) => { //Find it in the cache, and just update it based on what was sent. client.cache.modify({ id: client.cache.identify({ @@ -114,23 +115,23 @@ export function registerMessagingHandlers({ socket, client }) { } } }); - } + }; - function handleConversationChanged(conversation) { + const handleConversationChanged = (conversation) => { //If it was archived, marked unread, etc. - } + }; socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); - socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc. -} + socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc. +}; -export function unregisterMessagingHandlers({ socket }) { +export const unregisterMessagingHandlers = ({ socket }) => { if (!socket) return; socket.off("new-message-summary"); socket.off("new-message-detailed"); socket.off("message-changed"); socket.off("message-changed"); socket.off("conversation-changed"); -} +}; diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 2ea760e3c..9a57a43e2 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,11 +1,10 @@ -import { useMutation, useQuery } from "@apollo/client"; +import { useQuery } from "@apollo/client"; import axios from "axios"; -import React, { useEffect, useState, useContext } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import SocketContext from "../../contexts/SocketIO/socketContext"; import { GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; -import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatConversationComponent from "./chat-conversation.component"; @@ -31,10 +30,16 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { const { socket } = useContext(SocketContext); useEffect(() => { - socket.emit("join-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation }); + socket.emit("join-bodyshop-conversation", { + bodyshopId: bodyshop.id, + conversationId: selectedConversation + }); return () => { - socket.emit("leave-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation }); + socket.emit("leave-bodyshop-conversation", { + bodyshopId: bodyshop.id, + conversationId: selectedConversation + }); }; }, [selectedConversation, bodyshop, socket]); diff --git a/client/src/components/profile-shops/profile-shops.container.jsx b/client/src/components/profile-shops/profile-shops.container.jsx index 4c5150913..46fe59aa2 100644 --- a/client/src/components/profile-shops/profile-shops.container.jsx +++ b/client/src/components/profile-shops/profile-shops.container.jsx @@ -5,7 +5,7 @@ import { QUERY_ALL_ASSOCIATIONS, UPDATE_ACTIVE_ASSOCIATION } from "../../graphql import AlertComponent from "../alert/alert.component"; import ProfileShopsComponent from "./profile-shops.component"; import axios from "axios"; -import { getToken } from "firebase/messaging"; +import { getToken } from " firebase/messaging"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 69c9402bd..885602fe3 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -67,7 +67,6 @@ const useSocket = (bodyshop) => { store.dispatch(setWssStatus("disconnected")); }; - //TODO: Check these handlers. socketInstance.on("connect", handleConnect); socketInstance.on("reconnect", handleReconnect); socketInstance.on("connect_error", handleConnectionError); diff --git a/client/src/utils/fcm-handler.js b/client/src/utils/fcm-handler.js deleted file mode 100644 index e3f7c8d43..000000000 --- a/client/src/utils/fcm-handler.js +++ /dev/null @@ -1,70 +0,0 @@ -export default async function FcmHandler({ client, payload }) { - console.log("FCM", payload); - // switch (payload.type) { - // case "messaging-inbound": - // client.cache.modify({ - // id: client.cache.identify({ - // __typename: "conversations", - // id: payload.conversationid - // }), - // fields: { - // messages_aggregate(cached) { - // return { aggregate: { count: cached.aggregate.count + 1 } }; - // } - // } - // }); - // client.cache.modify({ - // fields: { - // messages_aggregate(cached) { - // return { aggregate: { count: cached.aggregate.count + 1 } }; - // } - // } - // }); - // break; - // case "messaging-outbound": - // client.cache.modify({ - // id: client.cache.identify({ - // __typename: "conversations", - // id: payload.conversationid - // }), - // fields: { - // updated_at(oldupdated0) { - // return new Date(); - // } - // // messages_aggregate(cached) { - // // return { aggregate: { count: cached.aggregate.count + 1 } }; - // // }, - // } - // }); - // break; - // case "messaging-mark-conversation-read": - // let previousUnreadCount = 0; - // client.cache.modify({ - // id: client.cache.identify({ - // __typename: "conversations", - // id: payload.conversationid - // }), - // fields: { - // messages_aggregate(cached) { - // previousUnreadCount = cached.aggregate.count; - // return { aggregate: { count: 0 } }; - // } - // } - // }); - // client.cache.modify({ - // fields: { - // messages_aggregate(cached) { - // return { - // aggregate: { - // count: cached.aggregate.count - previousUnreadCount - // } - // }; - // } - // } - // }); - // break; - // default: - // console.log("No payload type set."); - // break; - // } -} diff --git a/server/sms/receive.js b/server/sms/receive.js index 72dcb0c39..3fb721eec 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -11,7 +11,6 @@ const logger = require("../utils/logger"); const InstanceManager = require("../utils/instanceMgr").default; exports.receive = async (req, res) => { - // Perform request validation const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } @@ -50,20 +49,17 @@ exports.receive = async (req, res) => { }; if (response.bodyshops[0]) { - if (response.bodyshops[0].conversations.length === 0) { - // No conversation found, create one + const bodyshop = response.bodyshops[0]; + if (bodyshop.conversations.length === 0) { newMessage.conversation = { data: { - bodyshopid: response.bodyshops[0].id, + bodyshopid: bodyshop.id, phone_num: phone(req.body.From).phoneNumber } }; try { - // Insert new conversation and message const insertresp = await client.request(queries.RECEIVE_MESSAGE, { msg: newMessage }); - - // Safely access conversation and message const createdConversation = insertresp?.insert_messages?.returning?.[0]?.conversation || null; const message = insertresp?.insert_messages?.returning?.[0]; @@ -71,12 +67,12 @@ exports.receive = async (req, res) => { throw new Error("Conversation data is missing from the response."); } - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const broadcastRoom = getBodyshopRoom(createdConversation.bodyshop.id); const conversationRoom = getBodyshopConversationRoom({ bodyshopId: message.conversation.bodyshop.id, conversationId: message.conversation.id }); - // Broadcast new message to the conversation room + ioRedis.to(broadcastRoom).emit("new-message-summary", { isoutbound: false, existingConversation: false, @@ -86,6 +82,7 @@ exports.receive = async (req, res) => { msid: message.sid, summary: true }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { newMessage: message, isoutbound: false, @@ -103,24 +100,12 @@ exports.receive = async (req, res) => { res.status(200).send(""); return; } catch (e) { - 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), - messagingServiceSid: req.body.MessagingServiceSid, - error: e - }); - - res.status(500).json(e); + handleError(req, e, res, "RECEIVE_MESSAGE"); return; } - } else if (response.bodyshops[0].conversations.length === 1) { - // Add to the existing conversation - // conversation UPDATED - newMessage.conversationid = response.bodyshops[0].conversations[0].id; + } else if (bodyshop.conversations.length === 1) { + newMessage.conversationid = bodyshop.conversations[0].id; } else { - // Duplicate phone error logger.log("sms-inbound-error", "ERROR", "api", null, { msid: req.body.SmsMessageSid, text: req.body.Body, @@ -134,7 +119,6 @@ exports.receive = async (req, res) => { } try { - // Insert message into an existing conversation const insertresp = await client.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid: newMessage.conversationid @@ -167,52 +151,36 @@ exports.receive = async (req, res) => { fcmresp }); - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const broadcastRoom = getBodyshopRoom(message.conversation.bodyshop.id); const conversationRoom = getBodyshopConversationRoom({ bodyshopId: message.conversation.bodyshop.id, conversationId: message.conversation.id }); - // Broadcast new message to the conversation room + ioRedis.to(broadcastRoom).emit("new-message-summary", { isoutbound: false, existingConversation: true, - conversationId: conversationid, + conversationId: message.conversationid, updated_at: message.updated_at, msid: message.sid, summary: true }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { newMessage: message, isoutbound: false, existingConversation: true, - conversationId: conversationid, + conversationId: message.conversationid, summary: false }); res.status(200).send(""); } catch (e) { - 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), - messagingServiceSid: req.body.MessagingServiceSid, - error: e - }); - - res.status(500).json(e); + handleError(req, e, res, "INSERT_MESSAGE"); } } } catch (e) { - 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), - messagingServiceSid: req.body.MessagingServiceSid, - error: e - }); - res.status(500).json(e); + handleError(req, e, res, "FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID"); } }; @@ -228,3 +196,17 @@ const generateMediaArray = (body) => { return null; } }; + +const handleError = (req, error, res, context) => { + 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), + messagingServiceSid: req.body.MessagingServiceSid, + context, + error + }); + + res.status(500).json({ error: error.message || "Internal Server Error" }); +}; diff --git a/server/sms/send.js b/server/sms/send.js index 7975cc2f9..eb8cb6e5f 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -8,10 +8,9 @@ const { phone } = require("phone"); const queries = require("../graphql-client/queries"); const logger = require("../utils/logger"); const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY); -const { admin } = require("../firebase/firebase-handler"); const gqlClient = require("../graphql-client/graphql-client").client; -exports.send = (req, res) => { +exports.send = async (req, res) => { const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body; const { ioRedis, @@ -19,7 +18,7 @@ exports.send = (req, res) => { } = req; logger.log("sms-outbound", "DEBUG", req.user.email, null, { - messagingServiceSid: messagingServiceSid, + messagingServiceSid, to: phone(to).phoneNumber, mediaUrl: selectedMedia.map((i) => i.src), text: body, @@ -30,85 +29,10 @@ exports.send = (req, res) => { image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] }); - if (!!to && !!messagingServiceSid && (!!body || !!selectedMedia.length > 0) && !!conversationid) { - client.messages - .create({ - body: body, - messagingServiceSid: messagingServiceSid, - to: phone(to).phoneNumber, - mediaUrl: selectedMedia.map((i) => i.src) - }) - .then((message) => { - let newMessage = { - msid: message.sid, - text: body, - conversationid, - isoutbound: true, - userid: req.user.email, - image: req.body.selectedMedia.length > 0, - image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] - }; - gqlClient - .request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid }) - .then((r2) => { - //console.log("Responding GQL Message ID", JSON.stringify(r2)); - logger.log("sms-outbound-success", "DEBUG", req.user.email, null, { - msid: message.sid, - conversationid - }); - - const data = { - type: "messaging-outbound", - conversationid: newMessage.conversationid || "" - }; - - // TODO Verify - // const messageData = response.insert_messages.returning[0]; - - // Broadcast new message to conversation room - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); - const conversationRoom = getBodyshopConversationRoom({ - bodyshopId: r2.insert_messages.returning[0].conversation.bodyshop.id, - conversationId: r2.insert_messages.returning[0].conversation.id - }); - - ioRedis.to(broadcastRoom).emit("new-message-summary", { - isoutbound: true, - conversationId: conversationid, - updated_at: r2.insert_messages.returning[0].updated_at, - msid: message.sid, - summary: true - }); - ioRedis.to(conversationRoom).emit("new-message-detailed", { - newMessage: r2.insert_messages.returning[0], - conversationId: conversationid, - summary: false - }); - res.sendStatus(200); - }) - .catch((e2) => { - logger.log("sms-outbound-error", "ERROR", req.user.email, null, { - msid: message.sid, - conversationid, - error: e2.message, - stack: e2.stack - }); - - //res.json({ success: false, message: e2 }); - }); - }) - .catch((e1) => { - //res.json({ success: false, message: error }); - logger.log("sms-outbound-error", "ERROR", req.user.email, null, { - conversationid, - error: e1.message, - stack: e1.stack - }); - }); - } else { + if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) { logger.log("sms-outbound-error", "ERROR", req.user.email, null, { type: "missing-parameters", - messagingServiceSid: messagingServiceSid, + messagingServiceSid, to: phone(to).phoneNumber, text: body, conversationid, @@ -118,5 +42,72 @@ exports.send = (req, res) => { image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] }); res.status(400).json({ success: false, message: "Missing required parameter(s)." }); + return; + } + + try { + const message = await client.messages.create({ + body, + messagingServiceSid, + to: phone(to).phoneNumber, + mediaUrl: selectedMedia.map((i) => i.src) + }); + + const newMessage = { + msid: message.sid, + text: body, + conversationid, + isoutbound: true, + userid: req.user.email, + image: req.body.selectedMedia.length > 0, + image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] + }; + + try { + const gqlResponse = await gqlClient.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid }); + + logger.log("sms-outbound-success", "DEBUG", req.user.email, null, { + msid: message.sid, + conversationid + }); + + const insertedMessage = gqlResponse.insert_messages.returning[0]; + const broadcastRoom = getBodyshopRoom(insertedMessage.conversation.bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: insertedMessage.conversation.bodyshop.id, + conversationId: insertedMessage.conversation.id + }); + + ioRedis.to(broadcastRoom).emit("new-message-summary", { + isoutbound: true, + conversationId: conversationid, + updated_at: insertedMessage.updated_at, + msid: message.sid, + summary: true + }); + + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: insertedMessage, + conversationId: conversationid, + summary: false + }); + + res.sendStatus(200); + } catch (gqlError) { + logger.log("sms-outbound-error", "ERROR", req.user.email, null, { + msid: message.sid, + conversationid, + error: gqlError.message, + stack: gqlError.stack + }); + res.status(500).json({ success: false, message: "Failed to insert message into database." }); + } + } catch (twilioError) { + logger.log("sms-outbound-error", "ERROR", req.user.email, null, { + conversationid, + error: twilioError.message, + stack: twilioError.stack + }); + res.status(500).json({ success: false, message: "Failed to send message through Twilio." }); } }; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 51e8039d8..8e1358994 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -1,12 +1,4 @@ const { admin } = require("../firebase/firebase-handler"); -const { MARK_MESSAGES_AS_READ, GET_CONVERSATIONS, GET_CONVERSATION_DETAILS } = require("../graphql-client/queries"); -const { phone } = require("phone"); -const { client: gqlClient } = require("../graphql-client/graphql-client"); -const queries = require("../graphql-client/queries"); -const twilio = require("twilio"); -const client = require("../graphql-client/graphql-client").client; - -const twilioClient = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY); const redisSocketEvents = ({ io, @@ -84,6 +76,7 @@ const redisSocketEvents = ({ }; socket.on("update-token", updateToken); }; + // Room Broadcast Events const registerRoomAndBroadcastEvents = (socket) => { const joinBodyshopRoom = (bodyshopUUID) => { @@ -138,6 +131,7 @@ const redisSocketEvents = ({ socket.on("disconnect", disconnect); }; + // Messaging Events const registerMessagingEvents = (socket) => { const joinConversationRoom = async ({ bodyshopId, conversationId }) => { @@ -145,19 +139,26 @@ const redisSocketEvents = ({ const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); socket.join(room); } catch (error) { - logger.log("error", "Failed to join conversation", error); + logger.log("Failed to Join Conversation Room", "error", "io-redis", null, { + bodyshopId, + conversationId, + error: error.message, + stack: error.stack + }); socket.emit("error", { message: "Failed to join conversation" }); } }; - const leaveConversationRoom = ({ bodyshopId, conversationId }) => { try { const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); socket.leave(room); - // Optionally notify the client - //socket.emit("conversation-left", { conversationId }); } catch (error) { - socket.emit("error", { message: "Failed to leave conversation" }); + logger.log("Failed to Leave Conversation Room", "error", "io-redis", null, { + bodyshopId, + conversationId, + error: error.message, + stack: error.stack + }); } };