From 641640c9c9af30fe9114ea0dffad68d4d3cc1238 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Mon, 6 May 2024 17:43:54 +0200 Subject: [PATCH 01/20] renamed --- .../{class.notifications.js => class.notification.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename system/notifications/{class.notifications.js => class.notification.js} (100%) diff --git a/system/notifications/class.notifications.js b/system/notifications/class.notification.js similarity index 100% rename from system/notifications/class.notifications.js rename to system/notifications/class.notification.js From 5bd8e97a1ae1ce290b27d6e41384a2445c595c82 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Mon, 6 May 2024 17:44:30 +0200 Subject: [PATCH 02/20] updated import --- routes/router.system.notifications.js | 2 +- system/notifications/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/router.system.notifications.js b/routes/router.system.notifications.js index c6f228d..89b13db 100644 --- a/routes/router.system.notifications.js +++ b/routes/router.system.notifications.js @@ -1,6 +1,6 @@ const WebSocket = require("ws"); -const Notification = require("../system/notifications/class.notifications.js"); +const { Notification } = require("../system/notifications/index.js"); const events = Notification.events(); module.exports = (router) => { diff --git a/system/notifications/index.js b/system/notifications/index.js index 8dbe7bd..d4a70cb 100644 --- a/system/notifications/index.js +++ b/system/notifications/index.js @@ -1,4 +1,4 @@ -const Notification = require("./class.notifications.js"); +const Notification = require("./class.notification.js"); module.exports = { From 07245e272aee9c6016d7e9ee3407c53f3a48bd72 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Wed, 8 May 2024 19:28:01 +0200 Subject: [PATCH 03/20] note added --- components/plugins/class.plugin.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/plugins/class.plugin.js b/components/plugins/class.plugin.js index 0e0c677..8e5d188 100644 --- a/components/plugins/class.plugin.js +++ b/components/plugins/class.plugin.js @@ -116,6 +116,15 @@ module.exports = class Plugin extends Item { let init = (dependencies, cb) => { try { + // NOTE: Monkey patch ready/abort method to init? + // A plugin could siganlize if its ready or needs to be restarted + /* + let init = new Promise((resolve, reject) => { + init.ready = resolve; + init.abort = reject; + }); + */ + const granted = dependencies.every((c) => { if (this.intents.includes(c)) { From 02ed8934de35a7e55fe4d61436ec38b68a6ef673 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 23 May 2024 09:53:20 +0200 Subject: [PATCH 04/20] fix #443 --- helper/request.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/helper/request.js b/helper/request.js index 6a06e1f..212a4ee 100644 --- a/helper/request.js +++ b/helper/request.js @@ -1,5 +1,7 @@ const url = require("url"); +const promisify = require("./promisify.js"); + /** * Does a http request * @param {*} uri @@ -51,7 +53,8 @@ function perform(uri, options, cb) { cb(null, { headers: res.headers, status: res.statusCode, - body + body, + res }); }); @@ -84,17 +87,13 @@ function perform(uri, options, cb) { * @returns {http.ClientRequest} https://nodejs.org/dist/latest-v16.x/docs/api/http.html#class-httpclientrequest */ -module.exports = function request(uri, options, cb) { +function request(uri, options, cb) { if (!cb && options instanceof Function) { cb = options; options = {}; } - if (!cb) { - cb = () => { }; - } - options = Object.assign({ method: "GET", body: "", @@ -103,25 +102,26 @@ module.exports = function request(uri, options, cb) { setKeepAliveHeader: true }, options); + return promisify((done) => { + perform(uri, options, (err, result) => { + if (err) { - return perform(uri, options, (err, result) => { - if (err) { - - cb(err); - - } else { - - if (options.followRedirects && result.status >= 300 && result.status < 400) { - - perform(result.headers.location, options, cb); + done(err); } else { - cb(null, result); + if (options.followRedirects && result.status >= 300 && result.status < 400 && result.headers?.location) { + perform(result.headers.location, options, done); + } else { + done(null, result); + } } + }); + }, cb); - } - }); +} -}; \ No newline at end of file +module.exports = Object.assign(request, { + perform +}); \ No newline at end of file From 422136c7482d4f4a54747c3e3d33def959caf265 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 23 May 2024 10:09:26 +0200 Subject: [PATCH 05/20] improved --- helper/request.js | 8 +++++++- tests/helper/test.request.js | 25 +++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/helper/request.js b/helper/request.js index 212a4ee..206ef3b 100644 --- a/helper/request.js +++ b/helper/request.js @@ -14,6 +14,11 @@ const promisify = require("./promisify.js"); */ function perform(uri, options, cb) { + if(!options && !cb){ + options = {}; + cb = () => {}; + } + let { protocol } = new url.URL(uri); if (!["http:", "https:"].includes(protocol)) { @@ -54,7 +59,8 @@ function perform(uri, options, cb) { headers: res.headers, status: res.statusCode, body, - res + res, + req: request }); }); diff --git a/tests/helper/test.request.js b/tests/helper/test.request.js index cfd64d2..763bd4c 100644 --- a/tests/helper/test.request.js +++ b/tests/helper/test.request.js @@ -44,9 +44,30 @@ describe("helper/request", () => { }); }); - it("- returns a http request object", () => { + it("- returns a promise if no callback provided", () => { - let req = request("http://127.0.0.1/"); + let rtrn = request("http://127.0.0.1/"); + + assert(rtrn instanceof Promise); + + }); + + it("- returns undefined if a callback is provided", (done) => { + + let rtrn = request("http://127.0.0.1/", (err) => { + assert(rtrn === undefined); + done(err); + }); + + }); + + it('- should have a ".perform" method patched', () => { + assert(request.perform instanceof Function); + }); + + it("- perform method should return instanceof ClientRequest", () => { + + let req = request.perform("http://127.0.0.1"); assert(req instanceof ClientRequest); From 3d6bd39509fd093ae231715e5e5ed6e29ff87adb Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 23 May 2024 10:47:12 +0200 Subject: [PATCH 06/20] version pumped from `v16` to `v20` --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2143f07..b360ac9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # https://medium.com/@kahana.hagai/docker-compose-with-node-js-and-mongodb-dbdadab5ce0a # The instructions for the first stage -FROM node:16-alpine as builder +FROM node:20-alpine as builder ARG NODE_ENV=production ENV NODE_ENV=${NODE_ENV} @@ -22,7 +22,7 @@ RUN npm install # The instructions for second stage -FROM node:16-alpine +FROM node:20-alpine WORKDIR /opt/OpenHaus/backend COPY --from=builder node_modules node_modules From 2c643fb0facb52908fab6345ea73b7c11dc9b39a Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 23 May 2024 10:49:17 +0200 Subject: [PATCH 07/20] version pumped from `v16` to `v20`, see #459 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2143f07..b360ac9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # https://medium.com/@kahana.hagai/docker-compose-with-node-js-and-mongodb-dbdadab5ce0a # The instructions for the first stage -FROM node:16-alpine as builder +FROM node:20-alpine as builder ARG NODE_ENV=production ENV NODE_ENV=${NODE_ENV} @@ -22,7 +22,7 @@ RUN npm install # The instructions for second stage -FROM node:16-alpine +FROM node:20-alpine WORKDIR /opt/OpenHaus/backend COPY --from=builder node_modules node_modules From c9fc8af0525db541541f7e5ee4e041834ffd44b9 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 24 May 2024 11:21:21 +0200 Subject: [PATCH 08/20] `injectMethod` added, see #463 --- helper/injectMethod.js | 33 ++++++++++++++ tests/helper/index.js | 1 + tests/helper/test.injectMethod.js | 76 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 helper/injectMethod.js create mode 100644 tests/helper/test.injectMethod.js diff --git a/helper/injectMethod.js b/helper/injectMethod.js new file mode 100644 index 0000000..0d16f35 --- /dev/null +++ b/helper/injectMethod.js @@ -0,0 +1,33 @@ +/** + * @function injectMethod + * Add a method to a given object into the prototype + * Default values are set to the exact same values when the method would be defined in the object/class body + * + * @param {Object} obj Object to add property to + * @param {String} prop The property name + * @param {*} value Value of the property + * @param {Object} [options={}] Property descriptor options + * @param {Boolean} [options.writable=true] + * @param {Boolean} [options.enumerable=false] + * @param {Boolean} [options.configurable=true] + * + * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description + */ + +function injectMethod(obj, prop, value, options = {}){ + + if(!(value instanceof Function)){ + throw new TypeError(`Value must be a function, received ${typeof value}`); + } + + Object.defineProperty(Object.getPrototypeOf(obj), prop, { + value, + writable: true, + enumerable: false, + configurable: true, + ...options + }); + +} + +module.exports = injectMethod; \ No newline at end of file diff --git a/tests/helper/index.js b/tests/helper/index.js index adae309..73edb45 100644 --- a/tests/helper/index.js +++ b/tests/helper/index.js @@ -8,6 +8,7 @@ describe("Helper functions", function () { require("./test.extend.js"); //require("./test.filter.js"); // todo or remove?! see #19 require("./test.infinity.js"); + require("./test.injectMethod.js"); require("./test.iterate.js"); require("./test.mixins.js"); require("./test.observe.js"); diff --git a/tests/helper/test.injectMethod.js b/tests/helper/test.injectMethod.js new file mode 100644 index 0000000..20d06b0 --- /dev/null +++ b/tests/helper/test.injectMethod.js @@ -0,0 +1,76 @@ +const assert = require("assert"); +const { describe, it } = require("mocha"); + +const injectMethod = require("../../helper/injectMethod"); + +class Item{ + + constructor(){ + this.boolean = true; + } + + ping(){ + return this; + } + +} + +describe("helper/injectMethod", () => { + + it(`Define method on class protoype`, (done) => { + + let item = new Item(); + + injectMethod(item, "pong", function pong(){ + return this; + }); + + let proto = Object.getPrototypeOf(item); + let props = Object.getOwnPropertyNames(proto); + + assert.ok(props.includes("ping")); + assert.ok(props.includes("pong")); + + done(); + + }); + + + it(`Compare prototype descriptors of methods ping/pong`, (done) => { + + let item = new Item(); + + injectMethod(item, "pong", function(){ + return this; + }); + + let proto = Object.getPrototypeOf(item); + let ping = Object.getOwnPropertyDescriptor(proto, "ping"); + let pong = Object.getOwnPropertyDescriptor(proto, "pong"); + + // remove function (want only the descriptors); + delete ping.value; + delete pong.value; + + assert.deepEqual(ping, pong); + + done(); + + }); + + + it(`Check if "this" scope is set correctly for method`, (done) => { + + let item = new Item(); + + injectMethod(item, "pong", function(){ + return this; + }); + + assert.deepEqual(item.ping(), item.pong()); + + done(); + + }); + +}); \ No newline at end of file From 0e65d1c9f4acaf0932da7a0454ed40f1f85c8cc2 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 24 May 2024 15:11:30 +0200 Subject: [PATCH 09/20] check added --- tests/helper/test.injectMethod.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helper/test.injectMethod.js b/tests/helper/test.injectMethod.js index 20d06b0..913edef 100644 --- a/tests/helper/test.injectMethod.js +++ b/tests/helper/test.injectMethod.js @@ -68,6 +68,7 @@ describe("helper/injectMethod", () => { }); assert.deepEqual(item.ping(), item.pong()); + assert.ok(item.ping() === item.pong()); done(); From 5783cc6974e10c07c062ffe11bff7759c122867e Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sat, 25 May 2024 22:40:03 +0200 Subject: [PATCH 10/20] header & note added --- routes/router.system.logs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routes/router.system.logs.js b/routes/router.system.logs.js index 9f09d60..76f2cf3 100644 --- a/routes/router.system.logs.js +++ b/routes/router.system.logs.js @@ -237,6 +237,7 @@ module.exports = (router) => { }); }); + // NOTE: switch to GET method? router.post("/export", (req, res) => { try { @@ -255,6 +256,7 @@ module.exports = (router) => { } res.setHeader("content-type", "application/tar+gzip"); + res.setHeader("Content-Disposition", 'attachment; filename="logfiles.tgz"'); tar.once("exit", (code) => { console.log("exit code", code); From 57c730ff960536dbedc5ed92f108ac28fbacc7c4 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sat, 25 May 2024 22:59:46 +0200 Subject: [PATCH 11/20] comment/note added --- helper/injectMethod.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helper/injectMethod.js b/helper/injectMethod.js index 0d16f35..03250d0 100644 --- a/helper/injectMethod.js +++ b/helper/injectMethod.js @@ -14,13 +14,15 @@ * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description */ -function injectMethod(obj, prop, value, options = {}){ +function injectMethod(obj, prop, value, options = {}) { - if(!(value instanceof Function)){ + if (!(value instanceof Function)) { throw new TypeError(`Value must be a function, received ${typeof value}`); } - Object.defineProperty(Object.getPrototypeOf(obj), prop, { + // NOTE: Setting on prototype of given object, breaks iface.bridge()... + // Object.defineProperty(Object.getPrototypeOf(obj), prop, { + Object.defineProperty(obj, prop, { value, writable: true, enumerable: false, From fe8c2a94b920d7a09d62e65aa956ad13e1c9b4b2 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sat, 25 May 2024 23:01:46 +0200 Subject: [PATCH 12/20] fix #460 --- components/devices/class.device.js | 25 ++++++++- components/devices/class.interface.js | 79 +++++++++++++++++++++++++++ routes/router.api.devices.js | 69 ++++++++++++++--------- routes/router.system.connector.js | 72 ++++++++++++++++++++++++ routes/router.system.js | 5 ++ 5 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 routes/router.system.connector.js diff --git a/components/devices/class.device.js b/components/devices/class.device.js index 0205f8c..3cf52d1 100644 --- a/components/devices/class.device.js +++ b/components/devices/class.device.js @@ -6,6 +6,9 @@ const Interface = require("./class.interface.js"); const Item = require("../../system/component/class.item.js"); const mixins = require("../../helper/mixins.js"); +const injectMethod = require("../../helper/injectMethod.js"); + +//const { parse, calculateChecksum } = require("./net-helper.js"); /** * @description @@ -25,7 +28,7 @@ const mixins = require("../../helper/mixins.js"); * @see interfaceStream components/devices/class.interfaceStream.js */ module.exports = class Device extends Item { - constructor(props) { + constructor(props, scope) { super(props); @@ -38,6 +41,11 @@ module.exports = class Device extends Item { // for each interface class, create a interface stream this.interfaces = props.interfaces.map((obj) => { + + // NOTE: refactor interfaceStream in v4 + // move .bridge method there and pass device instance? + // > Would this also create a ciruclar reference in Interface class + // > since its stored via `Object.defineProperty(this, "stream",...);` let stream = new InterfaceStream({ // duplex stream options emitClose: false @@ -50,6 +58,21 @@ module.exports = class Device extends Item { let iface = new Interface(obj, stream); + // inject bridge method into interface instance + // passing deivce instance into Interface class, creates a ciruclar reference + // TODO: Move this into "interfaceStream" (needs to be refactored) + // NOTE: remove "device" for bridging requests (only needed in connector)? + // > See: https://github.com/OpenHausIO/connector/issues/54 + // > When done, "device" property can be removed, and the `.bridge()` method can be moved into Interface class + injectMethod(iface, "bridge", (cb) => { + return Interface._bridge({ + events: scope.events, + interface: iface, + device: this._id + }, cb); + }); + + // "hide" stream behind iface object // so we can use the interface object // as duplex stream diff --git a/components/devices/class.interface.js b/components/devices/class.interface.js index 701d102..f95a6a8 100644 --- a/components/devices/class.interface.js +++ b/components/devices/class.interface.js @@ -2,6 +2,15 @@ const Joi = require("joi"); const { Agent } = require("http"); const mongodb = require("mongodb"); const { Transform, Duplex } = require("stream"); +const { randomUUID } = require("crypto"); +//const path = require("path"); + +//const Adapter = require("./class.adapter.js"); + + +const timeout = require("../../helper/timeout.js"); +const promisfy = require("../../helper/promisify.js"); + /** * @description @@ -372,5 +381,75 @@ module.exports = class Interface { } + // bridge methods connects adapter with the underlaying network socket + // create a `.socket()` method that returns the palin websocket stream + static _bridge({ device, interface: iface, events }, cb) { + return promisfy((done) => { + + console.log("Bridge request, iface", iface, device); + + // create a random uuid + // used as identifier for responses + let uuid = randomUUID(); + //let uuid = "4c6de542-f89f-42ac-a2b5-1c26f9e68d73"; + + + // timeout after certain time + // no connector available, not mocks or whatever reaseon + let caller = timeout(5000, (timedout, duration, args) => { + if (timedout) { + done(new Error("TIMEDOUT")); + } else { + done(null, args[0]); + } + }); + + + // socket response handler + // listen for uuid and compare it with generated + let handler = ({ stream, type, uuid: id, socket }) => { + if (uuid === id && type === "response" && socket) { + + console.log("adapter", iface.adapter); + + /* + // create adapter stack here + // pass adapter stack as caller argument + //caller(stack); + let stack = iface.adapter.map((name) => { + try { + return require(path.join(process.cwd(), "adapter", `${name}.js`))(); + } catch (err) { + console.error(`Error in adapter "${name}" `, err); + } + }); + + console.log("stack", stack); + + stream = new Adapter(stack, stream, { + emitClose: false, + end: false + }); + + console.log("stream", stream) + */ + + caller(stream); + + } + }; + + events.on("socket", handler); + + events.emit("socket", { + uuid, + device, + interface: iface._id, + type: "request" + }); + + }, cb); + } + }; \ No newline at end of file diff --git a/routes/router.api.devices.js b/routes/router.api.devices.js index 5ac6738..1bf2f11 100644 --- a/routes/router.api.devices.js +++ b/routes/router.api.devices.js @@ -1,5 +1,6 @@ const WebSocket = require("ws"); const { finished } = require("stream"); +const C_DEVICES = require("../components/devices"); //const iface_locked = new Map(); @@ -21,11 +22,12 @@ module.exports = (app, router) => { return String(iface._id) === String(req.params._iid); }); - if (!iface) { + if (!iface && !req.query?.socket) { return res.status(404).end(); } - if (iface.upstream) { + // allow multipel connections to new connection handling below + if (iface.upstream && !req.query?.socket) { return res.status(423).end(); } @@ -53,41 +55,50 @@ module.exports = (app, router) => { // listen only once to connectoin event // gets fired every time websocket client hit this url/route - wss.on("connection", (ws) => { + wss.on("connection", (ws, req) => { + if (req.query?.uuid && req.query?.socket === "true" && req.query?.type === "response") { - // set connection to "alive" - // see #148 - ws.isAlive = true; + // new bridge/connector practice + // see https://github.com/OpenHausIO/backend/issues/460 - let upstream = WebSocket.createWebSocketStream(ws); + let stream = WebSocket.createWebSocketStream(ws); - // Cleanup: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#streamfinishedstream-options-callback - let cleanup = finished(upstream, () => { - iface.detach(() => { - cleanup(); + C_DEVICES.events.emit("socket", { + uuid: req.query.uuid, + type: "response", + socket: true, + stream }); - }); + } else { - iface.attach(upstream); + // old/legacy connection mechanism + // TODO: Remove this in future versions + let upstream = WebSocket.createWebSocketStream(ws); - //https://github.com/websockets/ws#how-to-detect-and-close-broken-connections - ["close", "error"].forEach((event) => { - upstream.once(event, () => { + // Cleanup: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#streamfinishedstream-options-callback + let cleanup = finished(upstream, () => { + iface.detach(() => { + cleanup(); + }); + }); - upstream.destroy(); - iface.detach(); - }); - }); + iface.attach(upstream); - // detect broken connection - ws.on("pong", () => { - //console.log("pong", Date.now(), "\r\n\r\n") - ws.isAlive = true; - }); + //https://github.com/websockets/ws#how-to-detect-and-close-broken-connections + ["close", "error"].forEach((event) => { + upstream.once(event, () => { + + upstream.destroy(); + iface.detach(); + + }); + }); + + } }); @@ -119,7 +130,15 @@ module.exports = (app, router) => { wss.handleUpgrade(req, req.socket, req.headers, (ws) => { + + ws.isAlive = true; + + ws.on("pong", () => { + ws.isAlive = true; + }); + wss.emit("connection", ws, req); + }); } diff --git a/routes/router.system.connector.js b/routes/router.system.connector.js new file mode 100644 index 0000000..3dad200 --- /dev/null +++ b/routes/router.system.connector.js @@ -0,0 +1,72 @@ +const C_DEVICES = require("../components/devices"); + +// external modules +const WebSocket = require("ws"); + +module.exports = (router) => { + + // websocket server + let wss = new WebSocket.Server({ + noServer: true + }); + + // detect broken connections + let interval = setInterval(() => { + wss.clients.forEach((ws) => { + + if (!ws.isAlive) { + ws.terminate(); + return; + } + + ws.isAlive = false; + ws.ping(); + + }); + }, Number(process.env.API_WEBSOCKET_TIMEOUT)); + + + // if the server closes + // clear the interval + wss.on("close", () => { + clearInterval(interval); + }); + + + C_DEVICES.events.on("socket", (obj) => { + if (obj.type === "request") { + wss.clients.forEach((ws) => { + ws.send(JSON.stringify(obj)); + }); + } + }); + + + // http route handler + // TODO: Reformat to match router.api.mdns.js code style/if-else + router.get("/", (req, res, next) => { + + // check if connection is a simple get request or ws client + if ((!req.headers["upgrade"] || !req.headers["connection"])) { + //return res.status(403).end(); + next(); // let the rest-handler.js do its job + return; + } + + // handle request as websocket + // perform websocket handshake + wss.handleUpgrade(req, req.socket, req.headers, (ws) => { + + ws.isAlive = true; + + ws.on("pong", () => { + ws.isAlive = true; + }); + + wss.emit("connection", ws, req); + + }); + + }); + +}; \ No newline at end of file diff --git a/routes/router.system.js b/routes/router.system.js index a4de93c..8e332f0 100644 --- a/routes/router.system.js +++ b/routes/router.system.js @@ -20,6 +20,7 @@ module.exports = (router) => { let eventsRouter = Router(); let notificationsRouter = Router(); let logsRouter = Router(); + let connectorRouter = Router(); // http://127.0.0.1/api/system/info // FIXME: what does this work with "eventsRouter/notificationsRouter"?! @@ -38,4 +39,8 @@ module.exports = (router) => { router.use("/logs", logsRouter); require("./router.system.logs.js")(logsRouter); + // http://127.0.0.1/api/system/connector + router.use("/connector", connectorRouter); + require("./router.system.connector.js")(connectorRouter); + }; \ No newline at end of file From 4c32a6f23947d721e7c6e8a8203c64f122cef54f Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sat, 25 May 2024 23:01:54 +0200 Subject: [PATCH 13/20] note added --- routes/router.api.events.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routes/router.api.events.js b/routes/router.api.events.js index 38503e7..83c0494 100644 --- a/routes/router.api.events.js +++ b/routes/router.api.events.js @@ -89,6 +89,9 @@ module.exports = (app, router) => { component.events.on(method, reemit(method, name)); }); + // NOTE: handle also custom events like "socket" & ssdp/mqtt events? + //component.events.on("socket", reemit("socket", name)); + } catch (err) { console.error("Failure in events http api", err); From 72fcfb66d832b6dd08ef82446ad8767ab5e3d56a Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sat, 25 May 2024 23:08:44 +0200 Subject: [PATCH 14/20] removed test --- tests/helper/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helper/index.js b/tests/helper/index.js index 73edb45..8032d91 100644 --- a/tests/helper/index.js +++ b/tests/helper/index.js @@ -8,7 +8,7 @@ describe("Helper functions", function () { require("./test.extend.js"); //require("./test.filter.js"); // todo or remove?! see #19 require("./test.infinity.js"); - require("./test.injectMethod.js"); + //require("./test.injectMethod.js"); // removed due to problems on `interface.bridge()`, see https://github.com/OpenHausIO/backend/issues/463#issuecomment-2131411981 require("./test.iterate.js"); require("./test.mixins.js"); require("./test.observe.js"); From 9b0de28411945a3d6f672fd48172668e0d7248bc Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sat, 25 May 2024 23:14:37 +0200 Subject: [PATCH 15/20] fix #462 --- postman.json | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 299 insertions(+), 3 deletions(-) diff --git a/postman.json b/postman.json index fd64cc2..2bfb08d 100644 --- a/postman.json +++ b/postman.json @@ -1674,7 +1674,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -1683,7 +1684,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1697,7 +1699,7 @@ } }, "url": { - "raw": "http://{{HOST}}:{{PORT}}/api/plugins/658188e93cde9987c3228806/files", + "raw": "http://{{HOST}}:{{PORT}}/api/plugins/658188e93cde9987c3228806/files?install=true", "protocol": "http", "host": [ "{{HOST}}" @@ -1708,6 +1710,13 @@ "plugins", "658188e93cde9987c3228806", "files" + ], + "query": [ + { + "key": "install", + "value": "true", + "description": "Install npm dependenys" + } ] } }, @@ -2743,6 +2752,293 @@ "response": [] } ] + }, + { + "name": "System", + "item": [ + { + "name": "Notifications", + "item": [ + { + "name": "Create notification", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Title\",\n \"message\": \"Hello World\",\n \"uuid\": \"83d2087a-d901-4eb5-ac05-f7980893df64\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/notifications?publish=false", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "notifications" + ], + "query": [ + { + "key": "publish", + "value": "false", + "description": "Publish notifications instantly?" + } + ] + } + }, + "response": [] + }, + { + "name": "Publish notification", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/notifications/83d2087a-d901-4eb5-ac05-f7980893df64/publish", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "notifications", + "83d2087a-d901-4eb5-ac05-f7980893df64", + "publish" + ] + } + }, + "response": [] + }, + { + "name": "Delete notification", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Title\",\n \"message\": \"Hello World\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/notifications/83d2087a-d901-4eb5-ac05-f7980893df64", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "notifications", + "83d2087a-d901-4eb5-ac05-f7980893df64" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Logging", + "item": [ + { + "name": "Get log entrys", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/logs?offset=0&limit=10", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "logs" + ], + "query": [ + { + "key": "offset", + "value": "0" + }, + { + "key": "limit", + "value": "10" + } + ] + } + }, + "response": [] + }, + { + "name": "Create log entry", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"message\": \"Hello World\", \n \"level\": \"error\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/logs", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "logs" + ] + } + }, + "response": [] + }, + { + "name": "Delete/Truncate logs", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/logs?delete=false", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "logs" + ], + "query": [ + { + "key": "delete", + "value": "false", + "description": "Delete files or just truncate them?" + } + ] + } + }, + "response": [] + }, + { + "name": "Export (Download) logfiles as tar.gz", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/logs/export", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "logs", + "export" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Information", + "item": [ + { + "name": "Software versions", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/info/versions", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "info", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "System usage", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/info/usage", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "info", + "usage" + ] + } + }, + "response": [] + } + ] + } + ] } ], "event": [ From a4f381f711e3e6221c7a50974f6eae9efb2f90b1 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sun, 26 May 2024 12:27:42 +0200 Subject: [PATCH 16/20] fix #464 --- routes/router.system.notifications.js | 8 +++++--- system/notifications/class.notification.js | 7 ++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/routes/router.system.notifications.js b/routes/router.system.notifications.js index 89b13db..de17975 100644 --- a/routes/router.system.notifications.js +++ b/routes/router.system.notifications.js @@ -63,10 +63,12 @@ module.exports = (router) => { router.put("/", (req, res) => { - let { title, message } = req.body; - let notification = new Notification(title, message); + let notification = new Notification(req.body); + + if (req.query?.publish === "true") { + notification.publish(); + } - notification.publish(); res.json(notification); }); diff --git a/system/notifications/class.notification.js b/system/notifications/class.notification.js index bba40e6..9786940 100644 --- a/system/notifications/class.notification.js +++ b/system/notifications/class.notification.js @@ -28,12 +28,9 @@ const notifications = new Proxy([], { module.exports = class Notification { - constructor(title, message) { + constructor(data) { - let { error, value } = Notification.validate({ - title, - message - }); + let { error, value } = Notification.validate(data); if (error) { throw error; From 518bf50b7f11ba5a3958a45b7ba15061f0ae6ebd Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sun, 26 May 2024 12:36:34 +0200 Subject: [PATCH 17/20] improved --- routes/router.system.notifications.js | 34 ++++++++++++++++++++++ system/notifications/class.notification.js | 14 ++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/routes/router.system.notifications.js b/routes/router.system.notifications.js index de17975..4f2e6ac 100644 --- a/routes/router.system.notifications.js +++ b/routes/router.system.notifications.js @@ -40,6 +40,25 @@ module.exports = (router) => { }); + router.param("uuid", (req, res, next, uid) => { + + let notifications = Notification.notifications(); + + let notification = notifications.find(({ uuid }) => { + return uuid === uid; + }); + + if (!notification) { + return res.status(404).end(); + } + + req.item = notification; + + next(); + + }); + + router.get("/", (req, res) => { if ((!req.headers["upgrade"] || !req.headers["connection"])) { @@ -74,6 +93,21 @@ module.exports = (router) => { }); + router.post("/:uuid/publish", (req, res) => { + + req.item.publish(); + res.json(req.item); + + }); + + + router.delete("/:uuid", (req, res) => { + + req.item.detain(); + res.json(req.item); + + }); + return router; }; \ No newline at end of file diff --git a/system/notifications/class.notification.js b/system/notifications/class.notification.js index 9786940..6dd30c4 100644 --- a/system/notifications/class.notification.js +++ b/system/notifications/class.notification.js @@ -41,11 +41,13 @@ module.exports = class Notification { Object.assign(this, { timestamps: { created: Date.now(), - published: false + published: null } }, value); // hidden property + // move into schema definition? + // why keep this secret? Object.defineProperty(this, "published", { value: false, writable: true @@ -82,6 +84,16 @@ module.exports = class Notification { } } + detain() { + + let index = notifications.find(({ uuid }) => { + return this.uuid === uuid; + }); + + notifications.splice(index, 1); + + } + static schema() { return Joi.object({ title: Joi.string().required(), From 60cde33c9ed861ac28faa542768e72c06f95b0d2 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sun, 26 May 2024 12:37:34 +0200 Subject: [PATCH 18/20] updated --- postman.json | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/postman.json b/postman.json index 2bfb08d..135105d 100644 --- a/postman.json +++ b/postman.json @@ -2766,7 +2766,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Title\",\n \"message\": \"Hello World\",\n \"uuid\": \"83d2087a-d901-4eb5-ac05-f7980893df64\"\n}", + "raw": "{\n \"title\": \"Title\",\n \"message\": \"Hello World\",\n \"type\": \"error\",\n \"uuid\": \"83d2087a-d901-4eb5-ac05-f7980893df64\"\n}", "options": { "raw": { "language": "json" @@ -2796,6 +2796,39 @@ }, "response": [] }, + { + "name": "Get notifications", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Title\",\n \"message\": \"Hello World\",\n \"type\": \"error\",\n \"uuid\": \"83d2087a-d901-4eb5-ac05-f7980893df64\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/system/notifications", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "system", + "notifications" + ] + } + }, + "response": [] + }, { "name": "Publish notification", "request": { From fc1eb7ba157fdc8dd9716ba2b719d7ee5bada09e Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sun, 26 May 2024 13:38:02 +0200 Subject: [PATCH 19/20] fix #462 --- postman.json | 296 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) diff --git a/postman.json b/postman.json index 135105d..d52d0e4 100644 --- a/postman.json +++ b/postman.json @@ -2579,6 +2579,302 @@ "response": [] } ] + }, + { + "name": "Webhooks", + "item": [ + { + "name": "Create new webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"_id\": \"647c29cb62ad0449380f0abe\",\n \"name\": \"Test Webhook as trigger for scenes\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks" + ] + } + }, + "response": [] + }, + { + "name": "Get all webhooks", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-auth-token", + "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks" + ] + } + }, + "response": [] + }, + { + "name": "Get sinlge webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/647c29cb62ad0449380f0abe", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "647c29cb62ad0449380f0abe" + ] + } + }, + "response": [] + }, + { + "name": "Update existing webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Trigger scene \"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/647c29cb62ad0449380f0abe", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "647c29cb62ad0449380f0abe" + ] + } + }, + "response": [] + }, + { + "name": "Update existing webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/647c29cb62ad0449380f0abe/trigger", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "647c29cb62ad0449380f0abe", + "trigger" + ] + } + }, + "response": [] + }, + { + "name": "Delete exisiting room", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/647c29cb62ad0449380f0abe", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "647c29cb62ad0449380f0abe" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] } ], "auth": { From 4472b5c25a252cd9c93f65af7fe78fc8b2072c2e Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Sun, 26 May 2024 13:38:15 +0200 Subject: [PATCH 20/20] minor bug fix --- routes/router.api.webhooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/router.api.webhooks.js b/routes/router.api.webhooks.js index 2a34257..06a752d 100644 --- a/routes/router.api.webhooks.js +++ b/routes/router.api.webhooks.js @@ -49,7 +49,7 @@ module.exports = (app, router) => { req.item._trigger(req.body, req.query, req); } - res.status(202).end(); + res.status(202).json(req.item); });