node-red-contrib-viseo-iadvize
Version:
VISEO Bot Maker - iAdvize
518 lines (450 loc) • 14.5 kB
JavaScript
const helper = require("node-red-viseo-helper");
const botmgr = require("node-red-viseo-bot-manager");
const uuidv4 = require("uuid/v4");
const cryptoJS = require("crypto-js");
const EventEmitter = require("events");
const CARRIER = "iAdvize";
const VERBOSE = true;
// --------------------------------------------------------------------------
// NODE-RED
// --------------------------------------------------------------------------
module.exports = function(RED) {
const register = function(config) {
RED.nodes.createNode(this, config);
var node = this;
config.uiUrl = node.credentials.uiUrl;
config.botName = node.credentials.botName;
config.botID = node.credentials.botID;
config.botToken = node.credentials.botToken;
start(RED, node, config);
this.on("close", done => {
stop(node, config, done);
});
};
RED.nodes.registerType("server-iadvize", register, {
credentials: {
botName: { type: "text" },
botID: { type: "text" },
botToken: { type: "text" },
uiUrl: { type: "text" }
}
});
};
let emitter;
let token;
let LISTENERS_REPLY = {};
let LISTENERS_TRANSFER = {};
let DELAY = 1000;
const start = (RED, node, config) => {
// Token
if (config.botToken) token = config.botToken;
// Delay
if (config.delay) DELAY = Number(config.delay) || 1000;
// Event emitter
class NodeEmitter extends EventEmitter {}
emitter = new NodeEmitter();
/** --------------------------
* EXTERNAL BOT ENDPOINTS
* -------------------------- */
/* 0 - Health verification */
RED.httpNode.use("/iadvize/health", function(req, res) {
if (VERBOSE) node.log("get /health");
res.send({ status: "UP" });
});
/* 1 - Get external bots */
RED.httpNode.use("/iadvize/external-bots", function(req, res) {
if (VERBOSE) node.log("get /external-bots");
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
res.send([
{
idBot: config.botID || "bot",
name: config.name || "bot",
editorUrl: config.uiUrl
}
]);
});
/* 2 - Put bot */
RED.httpNode.put("/iadvize/bots/:idOperator", function(req, res) {
if (VERBOSE) node.log("put /bots/:idOperator");
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
let now = new Date().toISOString();
res.send({
idOperator: req.params.idOperator,
external: {
idBot: config.botID || "bot",
name: config.name || "bot",
editorUrl: config.uiUrl
},
distributionRules: req.body.distributionRules,
createdAt: now,
updatedAt: now
});
});
/* 3 - Get bot */
RED.httpNode.use("/iadvize/bots/:idOperator", function(req, res) {
if (VERBOSE) node.log("get /bots/:idOperator");
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
let now = new Date().toISOString();
res.send({
idOperator: req.params.idOperator,
external: {
idBot: config.botID || "bot",
name: config.name || "bot",
editorUrl: config.uiUrl
},
distributionRules: req.body.distributionRules,
createdAt: now,
updatedAt: now
});
});
/* 4 - Get availibility strategies */
RED.httpNode.use("/iadvize/availability-strategies", function(req, res) {
/* if (VERBOSE) node.log("get /availability-strategies"); */
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
res.send([
{
strategy: "customAvailability",
availability: true
}
]);
});
/** --------------------------
* CONVERSATION ENDPOINTS
* -------------------------- */
/* 1 - Post conversation */
RED.httpNode.post("/iadvize/conversations", function(req, res) {
if (!req.body) res.status(400).send({ error: "empty body" });
if (VERBOSE)
node.log("post /conversations, ", req.body.idOperator);
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
let now = new Date().toISOString();
res.send({
idConversation: req.body.idConversation,
idOperator: req.body.idOperator,
replies: [],
variables: [],
createdAt: now,
updatedAt: now
});
});
/* 2 - Post message */
RED.httpNode.post("/iadvize/conversations/:conversationId/messages", function(
req,
res
) {
if (!req.body) res.status(400).send({ error: "empty body" });
if (VERBOSE) {
/*node.log("post /conversations/:conversationId/messages");*/
}
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
receive(node, config, req, res);
});
/* 3 - Get conversation */
RED.httpNode.use("/iadvize/conversations/:conversationId", function(
req,
res
) {
if (VERBOSE) node.log("get /conversations/:conversationId");
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
res.sendStatus(200).end();
});
/** --------------------------
* CALLBACKS
* -------------------------- */
RED.httpNode.use("/iadvize/callback", function(req, res) {
if (VERBOSE) node.log("get /callback");
if (!isValidSignature(req.header("X-iAdvize-Signature"), req.body)) {
if (VERBOSE) node.log("Security issue: bad signature");
return res.sendStatus(403).end();
}
node.send([{ callback: req.body }, undefined]);
if (req.body.eventType === "v2.conversation.pushed")
emitter.emit("messsage_transfer");
res.sendStatus(200).end();
});
let listenerReply = (LISTENERS_REPLY[node.id] = (
srcNode,
data,
srcConfig
) => {
reply(node, data, config);
});
helper.listenEvent("reply", listenerReply);
};
const stop = (node, done) => {
let listenerReply = LISTENERS_REPLY[node.id];
helper.removeListener("reply", listenerReply);
done();
};
function isValidSignature(receivedSignature, receivedPayload) {
if (!receivedSignature || !token) return false;
let signature = receivedSignature.split("=", 2);
let algo = signature[0].toUpperCase();
let hash = cryptoJS["Hmac" + algo](
JSON.stringify(receivedPayload),
token
).toString();
let recSignLen = signature[1].length;
let hashLen = hash.length;
if (recSignLen != hashLen) return false;
for (let i = 0; i < hashLen; ++i)
if (hashLen[i] != recSignLen[i]) return false;
return true;
}
// ------------------------------------------
// LRU REQUESTS
// ------------------------------------------
const LRUMap = require("./lru.js").LRUMap;
// Should it be init in start() ?
let _CONTEXTS = new LRUMap(CONFIG.server.contextLRU || 10000);
let _CONTEXT_KEY = "contextId";
const getMessageContext = message => {
if (message === undefined) return;
let uuid = helper.getByString(message, _CONTEXT_KEY);
let context = _CONTEXTS.get(uuid);
if (!context) {
context = {};
let convId = helper.getByString(message, "address.conversation.id");
uuid = convId + "-" + uuidv4();
helper.setByString(message, _CONTEXT_KEY, uuid);
_CONTEXTS.set(uuid, context);
}
return context;
};
// ------------------------------------------
// RECEIVE
// ------------------------------------------
const receive = (node, config, req, res) => {
// Received
if (req.body.message.author.role === "operator") {
let now = new Date().toISOString();
let msg = {
idConversation: req.params.conversationId,
idOperator: req.body.idOperator,
replies: [],
variables: [],
createdAt: now,
updatedAt: now
};
node.log('answered message to ' + msg.idConversation);
return res.send(msg);
}
// Log activity
try {
setTimeout(function() {
helper.trackActivities(node);
}, 0);
} catch (err) {
console.log(err);
}
req.body.message.conversationId = req.params.conversationId;
req.body.message.author.id = req.params.conversationId;
let data = botmgr.buildMessageFlow(
{ message: req.body.message },
{
userId: "message.author.id",
convId: "message.conversationId",
payload: "message.payload.value",
inputType: "message.payload.contentType",
source: CARRIER
}
);
let context = getMessageContext(data.message);
context.res = res;
context.req = req;
// Handle Prompt
let convId = botmgr.getConvId(data);
if (botmgr.hasDelayedCallback(convId, data.message)) return;
// Trigger received message
helper.emitEvent("received", node, data, config);
node.log('received message for ' + req.body.message.author.id);
node.send([undefined, data]);
};
// ------------------------------------------
// REPLY
// ------------------------------------------
const reply = (node, data, config) => {
let address = botmgr.getUserAddress(data);
if (!address || address.carrier !== CARRIER) return false;
// The address is not used because we reply to HTTP Response
let context = data.prompt
? getMessageContext(data.prompt)
: getMessageContext(data.message);
let res = context.res;
let req = context.req;
// Building the message
let now = new Date().toISOString();
let replies = getMessages(data.reply);
let msg = {
idConversation: req.params.conversationId,
idOperator: req.body.idOperator,
replies: replies.replies,
variables: replies.variables,
createdAt: now,
updatedAt: now
};
if (!replies.transfer) {
helper.fireAsyncCallback(data);
res.send(msg);
} else {
data.error = 1;
LISTENERS_TRANSFER[context.channel] = function() {
delete data.error;
};
emitter.addListener(
"messsage_transfer",
LISTENERS_TRANSFER[context.channel]
);
res.send(msg);
setTimeout(function() {
helper.fireAsyncCallback(data);
emitter.removeListener(
"messsage_transfer",
LISTENERS_TRANSFER[context.channel]
);
}, 6000);
}
};
// ------------------------------------------
// MESSAGES
// https://developers.iadvize.com/documentation/test/AAA-EPIC-Rich-Content-Documentation#external-bot
// ------------------------------------------
const getMessages = (exports.getMessage = replies => {
let messages = [];
let variables = [];
let transfer = false;
let prompt = false;
let event = false;
let carouselDone = false;
let trueReplies = [];
let carousel = [];
for (let reply of replies) {
if (
reply.type === "confirm" ||
reply.type === "adaptiveCard" ||
reply.type === "signin"
) {
continue;
}
if (reply.type === "card") carousel.push(reply);
else trueReplies.push(reply);
}
let delayMsg = {
type: "await",
duration: {
unit: "millis",
value: DELAY
}
};
for (let reply of replies) {
let message = {
type: "message",
payload: {}
};
if (reply.type === "transfer") {
messages.pop();
messages.push(reply);
messages.push({
type: "await",
duration: {
unit: "seconds",
value: 3
}
});
transfer = true;
continue;
} else if (reply.type === "event") {
messages.pop();
variables.push({
key: reply.event.name,
value: reply.event.value
});
event = true;
continue;
} else if (reply.type === "text") {
message.payload.contentType = "text";
message.payload.value = reply.text;
} else if (reply.type === "quick") {
message.payload.contentType = "text";
message.payload.value = reply.quicktext;
message.quickReplies = [];
for (let button of reply.buttons) {
message.quickReplies.push({
contentType: "text/quick-reply",
value: button.title,
idQuickReply: uuidv4()
});
}
} else if (reply.type === "media") {
message.payload = {
contentType: "file",
fileName: "Cliquez pour télécharger",
mimeType: reply.mediaContentType,
url: reply.media
};
} else if (reply.type === "card" && !carouselDone) {
carouselDone = true;
let _cards = [];
for (let _card of carousel) {
let _obj = {
contentType: "card/content",
title: _card.title,
text: _card.subtitle,
actions: []
};
if (_card.attach) {
_obj.image = {
url: _card.attach,
description: _card.subtext
};
}
for (let button of _card.buttons) {
_obj.actions.push({
type: "LINK",
name: button.title,
url: button.value
});
}
_cards.push(_obj);
}
if (_cards.length > 1) {
message.payload = {
contentType: "bundle/card",
cards: _cards
};
} else {
message.payload = _cards[0];
}
} else continue;
if (reply.prompt) prompt = true;
messages.push(message);
messages.push(delayMsg);
}
messages.pop();
if (!prompt && !transfer && !event) messages.push({ type: "close" });
return { replies: messages, transfer: transfer, variables: variables };
});
// ------------------------------------------
// LOGGER
// ------------------------------------------