Merge branch 'master-AIO' into feature/IO-3020-IO-3036-imex-lite-rome-lite

This commit is contained in:
Patrick Fic
2024-12-04 12:58:23 -08:00
81 changed files with 4206 additions and 3939 deletions

View File

@@ -11,186 +11,161 @@ const logger = require("../utils/logger");
const InstanceManager = require("../utils/instanceMgr").default;
exports.receive = async (req, res) => {
//Perform request validation
const {
ioRedis,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
} = req;
logger.log("sms-inbound", "DEBUG", "api", null, {
const loggerData = {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
image_path: generateMediaArray(req.body)
});
};
if (!!!req.body || !!!req.body.MessagingServiceSid || !!!req.body.SmsMessageSid) {
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(queries.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];
// Sort conversations by `updated_at` (or `created_at`) and pick the last one
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),
type: "malformed-request"
});
res.status(400);
res.json({ success: false, error: "Malformed Request" });
} else {
try {
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
mssid: req.body.MessagingServiceSid,
phone: phone(req.body.From).phoneNumber
});
isoutbound: false,
userid: null // Add additional fields as necessary
};
let newMessage = {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
image_path: generateMediaArray(req.body)
};
if (response.bodyshops[0]) {
//Found a bodyshop - should always happen.
if (response.bodyshops[0].conversations.length === 0) {
//No conversation Found, create one.
//console.log("[SMS Receive] No conversation found. Creating one.");
newMessage.conversation = {
data: {
bodyshopid: response.bodyshops[0].id,
phone_num: phone(req.body.From).phoneNumber
}
};
} else if (response.bodyshops[0].conversations.length === 1) {
//Just add it to the conversation
//console.log("[SMS Receive] Conversation found. Added ID.");
newMessage.conversationid = response.bodyshops[0].conversations[0].id;
} else {
//We should never get here.
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,
type: "duplicate-phone"
});
}
try {
let insertresp;
if (response.bodyshops[0].conversations[0]) {
insertresp = await client.request(queries.INSERT_MESSAGE, {
msg: newMessage,
conversationid: response.bodyshops[0].conversations[0] && response.bodyshops[0].conversations[0].id
});
} else {
insertresp = await client.request(queries.RECEIVE_MESSAGE, {
msg: newMessage
});
}
const message = insertresp.insert_messages.returning[0];
const data = {
type: "messaging-inbound",
conversationid: message.conversationid || "",
text: message.text || "",
messageid: message.id || "",
phone_num: message.conversation.phone_num || ""
};
if (existingConversation) {
// Use the existing conversation
conversationid = existingConversation.id;
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {
title: InstanceManager({
imex: `ImEX Online Message - ${data.phone_num}`,
rome: `Rome Online Message - ${data.phone_num}`
}),
body: message.image_path ? `Image ${message.text}` : message.text
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //TODO:AIO Resolve addresses for other instances
},
data
});
logger.log("sms-inbound-success", "DEBUG", "api", null, {
newMessage,
fcmresp
});
res.status(200).send("");
} catch (e2) {
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: e2
});
res.sendStatus(500).json(e2);
}
// Unarchive the conversation if necessary
if (existingConversation.archived) {
await client.request(queries.UNARCHIVE_CONVERSATION, {
id: conversationid,
archived: false
});
}
} catch (e1) {
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: e1
} else {
// Create a new conversation
const newConversationResponse = await client.request(queries.CREATE_CONVERSATION, {
conversation: {
bodyshopid: bodyshop.id,
phone_num: phone(req.body.From).phoneNumber,
archived: false
}
});
res.sendStatus(500).json(e1);
const createdConversation = newConversationResponse.insert_conversations.returning[0];
conversationid = createdConversation.id;
}
// Ensure `conversationid` is added to the message
newMessage.conversationid = conversationid;
// Step 3: Insert the message into the conversation
const insertresp = await client.request(queries.INSERT_MESSAGE, {
msg: newMessage,
conversationid: 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 4: Notify clients through Redis
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
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
};
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 5: 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");
}
};
// const sampleMessage: {
// "ToCountry": "CA",
// "ToState": "BC",
// "SmsMessageSid": "SMad7bddaf3454c0904999d6018b1e8f49",
// "NumMedia": "0",
// "ToCity": "Vancouver",
// "FromZip": "",
// "SmsSid": "SMad7bddaf3454c0904999d6018b1e8f49",
// "FromState": "BC",
// "SmsStatus": "received",
// "FromCity": "VANCOUVER",
// "Body": "Hi",
// "FromCountry": "CA",
// "To": "+16043301606",
// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1",
// "ToZip": "",
// "NumSegments": "1",
// "MessageSid": "SMad7bddaf3454c0904999d6018b1e8f49",
// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c",
// "From": "+16049992002",
// "ApiVersion": "2010-04-01"
// }
// ] req.body {
// [0] ToCountry: 'CA',
// [0] MediaContentType0: 'image/jpeg',
// [0] ToState: 'BC',
// [0] SmsMessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
// [0] NumMedia: '1',
// [0] ToCity: 'Vancouver',
// [0] FromZip: '',
// [0] SmsSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
// [0] FromState: 'BC',
// [0] SmsStatus: 'received',
// [0] FromCity: 'VANCOUVER',
// [0] Body: '',
// [0] FromCountry: 'CA',
// [0] To: '+16043301606',
// [0] MessagingServiceSid: 'MG6e259e2add04ffa0d0aa355038670ee1',
// [0] ToZip: '',
// [0] NumSegments: '1',
// [0] MessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
// [0] AccountSid: 'AC6c09d337d6b9c68ab6488c2052bd457c',
// [0] From: '+16049992002',
// [0] MediaUrl0: 'https://api.twilio.com/2010-04-01/Accounts/AC6c09d337d6b9c68ab6488c2052bd457c/Messages/MM14fa2851ba26e0dc2b62073f8e7cdf27/Media/MEf129dd37979852f395eb29ffb126e19e',
// [0] ApiVersion: '2010-04-01'
// [0] }
// [0] MediaContentType0: 'image/jpeg',
// MediaContentType0: 'video/3gpp',
const generateMediaArray = (body) => {
const { NumMedia } = body;
if (parseInt(NumMedia) > 0) {
//stuff
const ret = [];
for (var i = 0; i < parseInt(NumMedia); i++) {
for (let i = 0; i < parseInt(NumMedia); i++) {
ret.push(body[`MediaUrl${i}`]);
}
return ret;
@@ -198,3 +173,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" });
};

View File

@@ -8,15 +8,17 @@ 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,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
} = 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,
@@ -27,66 +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 || ""
};
admin.messaging().send({
topic: `${imexshopid}-messaging`,
data
});
res.sendStatus(200);
})
.catch((e2) => {
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
msid: message.sid,
conversationid,
error: e2
});
//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
});
});
} 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,
@@ -96,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." });
}
};

View File

@@ -5,59 +5,102 @@ require("dotenv").config({
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
const { phone } = require("phone");
const logger = require("../utils/logger");
const { admin } = require("../firebase/firebase-handler");
exports.status = (req, res) => {
exports.status = async (req, res) => {
const { SmsSid, SmsStatus } = req.body;
client
.request(queries.UPDATE_MESSAGE_STATUS, {
const {
ioRedis,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
} = req;
try {
// Ignore status 'queued'
if (SmsStatus === "queued") {
return res.status(200).json({ message: "Status 'queued' disregarded." });
}
// Update message status in the database
const response = await client.request(queries.UPDATE_MESSAGE_STATUS, {
msid: SmsSid,
fields: { status: SmsStatus }
})
.then((response) => {
});
const message = response.update_messages.returning[0];
if (message) {
logger.log("sms-status-update", "DEBUG", "api", null, {
msid: SmsSid,
fields: { status: SmsStatus }
});
})
.catch((error) => {
logger.log("sms-status-update-error", "ERROR", "api", null, {
// Emit WebSocket event to notify the change in message status
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: message.conversation.bodyshopid,
conversationId: message.conversationid
});
ioRedis.to(conversationRoom).emit("message-changed", {
...message,
status: SmsStatus,
type: "status-changed"
});
} else {
logger.log("sms-status-update-warning", "WARN", "api", null, {
msid: SmsSid,
fields: { status: SmsStatus },
error
warning: "No message returned from the database update."
});
}
res.sendStatus(200);
} catch (error) {
logger.log("sms-status-update-error", "ERROR", "api", null, {
msid: SmsSid,
fields: { status: SmsStatus },
stack: error.stack,
message: error.message
});
res.sendStatus(200);
res.status(500).json({ error: "Failed to update message status." });
}
};
exports.markConversationRead = async (req, res) => {
const { conversationid, imexshopid } = req.body;
admin.messaging().send({
topic: `${imexshopid}-messaging`,
// notification: {
// title: `ImEX Online Message - ${data.phone_num}`,
// body: message.image_path ? `Image ${message.text}` : message.text,
// imageUrl: "https://thinkimex.com/img/logo512.png",
// },
data: {
type: "messaging-mark-conversation-read",
conversationid: conversationid || ""
}
});
res.send(200);
};
const {
ioRedis,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
} = req;
const { conversation, imexshopid, bodyshopid } = req.body;
// Inbound Sample
// {
// "SmsSid": "SM5205ea340e06437799d9345e7283457c",
// "SmsStatus": "queued",
// "MessageStatus": "queued",
// "To": "+16049992002",
// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1",
// "MessageSid": "SM5205ea340e06437799d9345e7283457c",
// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c",
// "From": "+16043301606",
// "ApiVersion": "2010-04-01"
// }
// Alternatively, support both payload formats
const conversationId = conversation?.id || req.body.conversationId;
if (!conversationId || !imexshopid || !bodyshopid) {
return res.status(400).json({ error: "Invalid conversation data provided." });
}
try {
const response = await client.request(queries.MARK_MESSAGES_AS_READ, {
conversationId
});
const updatedMessageIds = response.update_messages.returning.map((message) => message.id);
const broadcastRoom = getBodyshopRoom(bodyshopid);
ioRedis.to(broadcastRoom).emit("conversation-changed", {
type: "conversation-marked-read",
conversationId,
affectedMessages: response.update_messages.affected_rows,
messageIds: updatedMessageIds
});
res.status(200).json({
success: true,
message: "Conversation marked as read."
});
} catch (error) {
console.error("Error marking conversation as read:", error);
res.status(500).json({ error: "Failed to mark conversation as read." });
}
};