UNPKG

@machinechat/node-red-node-machinechat-bridge

Version:

Machinechat Bridge forwards Node-RED messages to Machinechat data visualization, monitoring, and analytics applications. Effortlessly upgrade Node-RED flows with web-based user interface, responsive custom dashboards, timeseries data storage, role-based u

324 lines (291 loc) 10.7 kB
const http = require("http"); var moment = require("moment"); module.exports = function (RED) { "use strict"; var mustache = require("mustache"); function extractTokens(tokens, set) { set = set || new Set(); tokens.forEach(function (token) { if (token[0] !== "text") { set.add(token[1]); if (token.length > 4) { extractTokens(token[4], set); } } }); return set; } function parseContext(key) { var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key); if (match) { var parts = {}; parts.type = match[1]; parts.store = match[3] === "" ? "default" : match[3]; parts.field = match[4]; return parts; } return undefined; } /** * Custom Mustache Context capable to collect message property and node * flow and global context */ function NodeContext( msg, nodeContext, parent, escapeStrings, cachedContextTokens ) { this.msgContext = new mustache.Context(msg, parent); this.nodeContext = nodeContext; this.escapeStrings = escapeStrings; this.cachedContextTokens = cachedContextTokens; } NodeContext.prototype = new mustache.Context(); NodeContext.prototype.lookup = function (name) { // try message first: try { var value = this.msgContext.lookup(name); if (value !== undefined) { if (this.escapeStrings && typeof value === "string") { value = value.replace(/\\/g, "\\\\"); value = value.replace(/\n/g, "\\n"); value = value.replace(/\t/g, "\\t"); value = value.replace(/\r/g, "\\r"); value = value.replace(/\f/g, "\\f"); value = value.replace(/[\b]/g, "\\b"); } return value; } // try flow/global context: var context = parseContext(name); if (context) { var type = context.type; var store = context.store; var field = context.field; var target = this.nodeContext[type]; if (target) { return this.cachedContextTokens[name]; } } return ""; } catch (err) { throw err; } }; NodeContext.prototype.push = function push(view) { return new NodeContext( view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens ); }; function isJson(str) { try { JSON.parse(str); } catch (e) { return false; } return true; } function isObject(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; } function decodeHTML(text) { return text.replace(/&#x([0-9A-F]{1,6});/gi, (match, code) => { return String.fromCharCode(parseInt(code, 16)); }); } function machinechatBridge(config) { RED.nodes.createNode(this, config); var node = this; /** * HostURL and Port values required, UniqueIdentifier optional */ this.machinechatHostURL = config.inputHostURL.toString(); this.MachineChatHTTPPort = config.inputPort.toString(); this.machinechatUniqueIdentifier = config.inputUniqueIdentifier.toString(); this.machinechatCopyMachinechatData = config.copyMachinechatData this.nodeRedVersion = RED.version(); this.field = config.field || "payload"; this.template = config.template; this.syntax = config.syntax || "mustache"; this.fieldType = config.fieldType || "msg"; this.outputFormat = config.output || "str"; function output(msg, value, send, done, rawInput) { var req, postData, httpConfig, payload = msg, prasedRawInput = JSON.parse(JSON.stringify(rawInput)) // parse untouched raw input data /* istanbul ignore else */ if (node.outputFormat === "json") { value = msg; } if (node.fieldType === "msg") { // Clear the old status node.status({}) // Need to check the payload is not empty before processing the API call if (prasedRawInput.payload === "") { node.status({ fill: "red", shape: "dot", text: "Message has an empty payload" }); send(prasedRawInput); done(); }else{ // Machinechat Data Collector payload data Object postData = JSON.stringify({ machinechat_context: { timestamp: moment().valueOf(), unique_identifier: decodeHTML(node.machinechatUniqueIdentifier) }, node_red_context:{ nodeRedVersion : node.nodeRedVersion }, msg: payload, }); // http config setup for Machinechat Bridge on node-red httpConfig = { hostname: node.machinechatHostURL, port: node.MachineChatHTTPPort, path: "/v1/node-red/msg", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; let responseData = ""; req = http.request(httpConfig, (res) => { // Clear the Status res.setEncoding("utf8"); res.on("data", (rawData) => { responseData += rawData; }); res.on("end", () => { try { if (res.statusCode === 200) { const data = JSON.parse(responseData); // check for @machinechat_context then prosses the status if (data.machinechat_context !== undefined) { if (data.machinechat_context.status !== undefined && data.machinechat_context.status.code !== undefined) { // check the status and error code to display the status. // succes path @node-red-001 if (data.machinechat_context.status.code.toLowerCase() === "codec-nodered-001") { // check for Copy Machinechat Data to Payload flag and Input Payload is an Object if (node.machinechatCopyMachinechatData === true && data.machinechat_context.mc !== undefined) { let isPayloadObject = isObject(prasedRawInput.payload); if (isPayloadObject) { // Add Machinechat Data into @payload["mc"] data.msg.payload["mc"] = data.machinechat_context.mc // set the Status to node-red node.status({ fill: "green", shape: "dot", text: "" }); send(data.msg); done(); }else{ node.status({ fill: "red", shape: "dot", text: "Cannot copy Machinechat data into payload - payload type is not object" }) done(); } }else{ // set the Status to node-red node.status({ fill: "green", shape: "dot", text: "" }); // send output data send(prasedRawInput); done(); } }else{ node.status({ fill: "red", shape: "dot", text: data.machinechat_context.status.message, }); } }else{ // "No status information in response." if @machinechat_context without status and code. node.status({ fill: "red", shape: "dot", text: "No status information in response." }); } }else{ // "No Machinechat context in response." if @machinechat_context is not available node.status({ fill: "red", shape: "dot", text: "No Machinechat context in response." }); } }else{ // "No status code in response." if statusCode is not available node.status({ fill: "red", shape: "dot", text: "No status code in response." }); } } catch (error) { node.error(error); send(prasedRawInput) done(); } }); }); // error path req.on("error", (e) => { node.error(`problem with request: ${e.message}`); node.status({ fill: "red", shape: "dot", text: e.message }); }); // Write postData to request body req.write(postData); req.end(); RED.util.setMessageProperty(msg, node.field, value); } } else if (node.fieldType === "flow" || node.fieldType === "global") { var context = RED.util.parseContextStore(node.field); var target = node.context()[node.fieldType]; target.set(context.key, value, context.store, function (err) { if (err) { done(err); } else { send(prasedRawInput) done(); } }); } } node.on("input", function (msg, send, done) { let rawInput = msg // check if payload is JSON and parse if (isJson(msg.payload)) { msg.payload = JSON.parse(msg.payload); } var is_json = node.outputFormat === "json"; var resolvedTokens = {}; // Read UniqueIdentifier from inputUniqueIdentifier field node.machinechatUniqueIdentifier = mustache.render( node.machinechatUniqueIdentifier, new NodeContext(msg, node.context(), null, is_json, resolvedTokens) ); try { /*** * Allow template contents to be defined externally * through inbound msg.template IFF node.template empty */ output(msg, msg, send, done, rawInput); } catch (err) { done(err.message); } }); } RED.nodes.registerType("machinechat-bridge", machinechatBridge); RED.library.register("machinechat-bridge"); };