From 8ca11f36d8ebdc3cbb2e80b3a374bf15e5541630 Mon Sep 17 00:00:00 2001 From: bvmensvoort Date: Sun, 11 Jun 2023 10:42:38 +0200 Subject: [PATCH 01/29] Make errors of nodes catchable by a catch node --- src/matrix-create-room.js | 4 ++-- src/matrix-delete-event.js | 4 ++-- src/matrix-invite-room.js | 8 ++++---- src/matrix-join-room.js | 8 ++++---- src/matrix-leave-room.js | 10 +++++----- src/matrix-react.js | 10 +++++----- src/matrix-receive.js | 2 +- src/matrix-room-ban.js | 10 +++++----- src/matrix-room-invite.js | 2 +- src/matrix-room-kick.js | 10 +++++----- src/matrix-room-users.js | 4 ++-- src/matrix-send-file.js | 4 ++-- src/matrix-send-image.js | 4 ++-- src/matrix-send-message.js | 8 ++++---- 14 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/matrix-create-room.js b/src/matrix-create-room.js index d60d2b3..c954a0e 100644 --- a/src/matrix-create-room.js +++ b/src/matrix-create-room.js @@ -8,7 +8,7 @@ module.exports = function(RED) { this.server = RED.nodes.getNode(n.server); if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } @@ -41,7 +41,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } diff --git a/src/matrix-delete-event.js b/src/matrix-delete-event.js index cbb40d2..66b6afd 100644 --- a/src/matrix-delete-event.js +++ b/src/matrix-delete-event.js @@ -27,7 +27,7 @@ module.exports = function(RED) { node.on('input', function(msg) { if(!msg.eventId) { - node.error("eventId is missing"); + node.error("eventId is missing", {}); node.send([null, msg]) return; } @@ -38,7 +38,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); return; } diff --git a/src/matrix-invite-room.js b/src/matrix-invite-room.js index 334cb0c..e1c5553 100644 --- a/src/matrix-invite-room.js +++ b/src/matrix-invite-room.js @@ -9,7 +9,7 @@ module.exports = function(RED) { this.roomId = n.roomId; if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } @@ -37,18 +37,18 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected"); + node.error("No matrix server selected", {}); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("room must be defined in either msg.topic or in node config"); + node.error("room must be defined in either msg.topic or in node config", {}); return; } diff --git a/src/matrix-join-room.js b/src/matrix-join-room.js index bee81fd..0c4c4ba 100644 --- a/src/matrix-join-room.js +++ b/src/matrix-join-room.js @@ -24,17 +24,17 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected"); + node.error("No matrix server selected", {}); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } if(!msg.topic) { - node.error("Room must be specified in msg.topic"); + node.error("Room must be specified in msg.topic", {}); return; } @@ -46,7 +46,7 @@ module.exports = function(RED) { node.send([msg, null]); }) .catch(function(e){ - node.error("Error trying to join room " + msg.topic + ":" + e); + node.error("Error trying to join room " + msg.topic + ":" + e, {}); msg.error = e; node.send([null, msg]); }); diff --git a/src/matrix-leave-room.js b/src/matrix-leave-room.js index f69877d..ee5da62 100644 --- a/src/matrix-leave-room.js +++ b/src/matrix-leave-room.js @@ -11,7 +11,7 @@ module.exports = function(RED) { node.status({ fill: "red", shape: "ring", text: "disconnected" }); if (!node.server) { - node.error("No configuration node"); + node.error("No configuration node", {}); return; } @@ -25,17 +25,17 @@ module.exports = function(RED) { node.on('input', function(msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected"); + node.error("No matrix server selected", {}); return; } if(!msg.topic) { - node.error('No room provided in msg.topic'); + node.error('No room provided in msg.topic', {}); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } @@ -44,7 +44,7 @@ module.exports = function(RED) { node.server.matrixClient.leave(msg.topic); node.send([msg, null]); } catch(e) { - node.error("Failed to leave room " + msg.topic + ": " + e); + node.error("Failed to leave room " + msg.topic + ": " + e, {}); msg.payload = e; node.send([null, msg]); } diff --git a/src/matrix-react.js b/src/matrix-react.js index 8da269e..26ca4cc 100644 --- a/src/matrix-react.js +++ b/src/matrix-react.js @@ -26,30 +26,30 @@ module.exports = function(RED) { node.on("input", function (msg) { if (!node.server || !node.server.matrixClient) { - node.error("No matrix server selected"); + node.error("No matrix server selected", {}); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("Room must be specified in msg.topic or in configuration"); + node.error("Room must be specified in msg.topic or in configuration", {}); return; } let payload = n.reaction || msg.payload; if(!payload) { - node.error('msg.payload must be defined or the reaction configured on the node.'); + node.error('msg.payload must be defined or the reaction configured on the node.', {}); return; } let eventId = msg.referenceEventId || msg.eventId; if(!eventId) { - node.error('Either msg.referenceEventId or msg.eventId must be defined to react to a message.'); + node.error('Either msg.referenceEventId or msg.eventId must be defined to react to a message.', {}); return; } diff --git a/src/matrix-receive.js b/src/matrix-receive.js index 8e82f58..404a32f 100644 --- a/src/matrix-receive.js +++ b/src/matrix-receive.js @@ -21,7 +21,7 @@ module.exports = function(RED) { node.status({ fill: "red", shape: "ring", text: "disconnected" }); if (!node.server) { - node.error("No configuration node"); + node.error("No configuration node", {}); return; } diff --git a/src/matrix-room-ban.js b/src/matrix-room-ban.js index 0a26b74..6d6a71e 100644 --- a/src/matrix-room-ban.js +++ b/src/matrix-room-ban.js @@ -26,23 +26,23 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected"); + node.error("No matrix server selected", {}); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("Room must be specified in msg.topic or in configuration"); + node.error("Room must be specified in msg.topic or in configuration", {}); return; } if(!msg.userId) { - node.error("msg.userId was not set."); + node.error("msg.userId was not set.", {}); return; } @@ -53,7 +53,7 @@ module.exports = function(RED) { node.send([msg, null]); }) .catch(function(e){ - node.error("Error trying to ban " + msg.userId + " from " + msg.topic); + node.error("Error trying to ban " + msg.userId + " from " + msg.topic, {}); msg.error = e; node.send([null, msg]); }); diff --git a/src/matrix-room-invite.js b/src/matrix-room-invite.js index 9c7edf5..1492538 100644 --- a/src/matrix-room-invite.js +++ b/src/matrix-room-invite.js @@ -9,7 +9,7 @@ module.exports = function(RED) { this.roomId = n.roomId; if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } diff --git a/src/matrix-room-kick.js b/src/matrix-room-kick.js index 5fb9d6f..13007ca 100644 --- a/src/matrix-room-kick.js +++ b/src/matrix-room-kick.js @@ -26,23 +26,23 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected"); + node.error("No matrix server selected", {}); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("Room must be specified in msg.topic or in configuration"); + node.error("Room must be specified in msg.topic or in configuration", {}); return; } if(!msg.userId) { - node.error("msg.userId was not set."); + node.error("msg.userId was not set.", {}); return; } @@ -53,7 +53,7 @@ module.exports = function(RED) { node.send([msg, null]); }) .catch(function(e){ - node.error("Error trying to kick " + msg.userId + " from " + msg.topic); + node.error("Error trying to kick " + msg.userId + " from " + msg.topic, {}); msg.error = e; node.send([null, msg]); }); diff --git a/src/matrix-room-users.js b/src/matrix-room-users.js index 7d1da9b..07eb34f 100644 --- a/src/matrix-room-users.js +++ b/src/matrix-room-users.js @@ -31,13 +31,13 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } let roomId = node.roomId || msg.topic; if(!roomId) { - node.error("msg.topic is required. Specify in the input or configure the room ID on the node."); + node.error("msg.topic is required. Specify in the input or configure the room ID on the node.", {}); return; } diff --git a/src/matrix-send-file.js b/src/matrix-send-file.js index 92486c2..923f40c 100644 --- a/src/matrix-send-file.js +++ b/src/matrix-send-file.js @@ -31,7 +31,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } @@ -57,7 +57,7 @@ module.exports = function(RED) { } if(!msg.payload) { - node.error('msg.payload is required'); + node.error('msg.payload is required', {}); return; } diff --git a/src/matrix-send-image.js b/src/matrix-send-image.js index 9572e7a..6035310 100644 --- a/src/matrix-send-image.js +++ b/src/matrix-send-image.js @@ -31,7 +31,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } @@ -57,7 +57,7 @@ module.exports = function(RED) { } if(!msg.payload) { - node.error('msg.payload is required'); + node.error('msg.payload is required', {}); return; } diff --git a/src/matrix-send-message.js b/src/matrix-send-message.js index 30607f1..79494a7 100644 --- a/src/matrix-send-message.js +++ b/src/matrix-send-message.js @@ -71,7 +71,7 @@ module.exports = function(RED) { if(msgType === 'msg.type') { if(!msg.type) { - node.error("msg.type type is set to be passed in via msg.type but was not defined"); + node.error("msg.type type is set to be passed in via msg.type but was not defined", {}); return; } msgType = msg.type; @@ -79,7 +79,7 @@ module.exports = function(RED) { if(msgFormat === 'msg.format') { if(!msg.format) { - node.error("Message format is set to be passed in via msg.format but was not defined"); + node.error("Message format is set to be passed in via msg.format but was not defined", {}); return; } msgFormat = msg.format; @@ -91,7 +91,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); return; } @@ -104,7 +104,7 @@ module.exports = function(RED) { let payload = n.message || msg.payload; if(!payload) { - node.error('msg.payload must be defined or the message configured on the node.'); + node.error('msg.payload must be defined or the message configured on the node.', {}); return; } From 124a0cba342c06ce1827dc2528c69f9753ef43db Mon Sep 17 00:00:00 2001 From: bvmensvoort Date: Sun, 11 Jun 2023 10:45:13 +0200 Subject: [PATCH 02/29] (de)Register consumer nodes at config node In order for error messages to be catchable --- src/matrix-create-room.js | 5 +++++ src/matrix-delete-event.js | 5 +++++ src/matrix-invite-room.js | 5 +++++ src/matrix-join-room.js | 5 +++++ src/matrix-leave-room.js | 5 +++++ src/matrix-react.js | 5 +++++ src/matrix-receive.js | 5 +++++ src/matrix-room-ban.js | 5 +++++ src/matrix-room-invite.js | 5 +++++ src/matrix-room-kick.js | 5 +++++ src/matrix-room-users.js | 5 +++++ src/matrix-send-file.js | 5 +++++ src/matrix-send-image.js | 5 +++++ src/matrix-send-message.js | 5 +++++ src/matrix-server-config.js | 9 +++++++++ 15 files changed, 79 insertions(+) diff --git a/src/matrix-create-room.js b/src/matrix-create-room.js index c954a0e..d5b7320 100644 --- a/src/matrix-create-room.js +++ b/src/matrix-create-room.js @@ -11,6 +11,7 @@ module.exports = function(RED) { node.error('Server must be configured on the node.', {}); return; } + node.server.register(node); this.encodeUri = function(pathTemplate, variables) { for (const key in variables) { @@ -64,6 +65,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-create-room", MatrixCreateRoom); } \ No newline at end of file diff --git a/src/matrix-delete-event.js b/src/matrix-delete-event.js index 66b6afd..187eae3 100644 --- a/src/matrix-delete-event.js +++ b/src/matrix-delete-event.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -70,6 +71,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent); } diff --git a/src/matrix-invite-room.js b/src/matrix-invite-room.js index e1c5553..dcf0689 100644 --- a/src/matrix-invite-room.js +++ b/src/matrix-invite-room.js @@ -12,6 +12,7 @@ module.exports = function(RED) { node.error('Server must be configured on the node.', {}); return; } + node.server.register(node); this.encodeUri = function(pathTemplate, variables) { for (const key in variables) { @@ -64,6 +65,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-invite-room", MatrixInviteRoom); } \ No newline at end of file diff --git a/src/matrix-join-room.js b/src/matrix-join-room.js index 0c4c4ba..438a7af 100644 --- a/src/matrix-join-room.js +++ b/src/matrix-join-room.js @@ -11,6 +11,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -51,6 +52,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-join-room", MatrixJoinRoom); } \ No newline at end of file diff --git a/src/matrix-leave-room.js b/src/matrix-leave-room.js index ee5da62..02d04a2 100644 --- a/src/matrix-leave-room.js +++ b/src/matrix-leave-room.js @@ -14,6 +14,7 @@ module.exports = function(RED) { node.error("No configuration node", {}); return; } + node.server.register(node); node.server.on("disconnected", function(){ node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -49,6 +50,10 @@ module.exports = function(RED) { node.send([null, msg]); } }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-leave-room", MatrixLeaveRoom); } \ No newline at end of file diff --git a/src/matrix-react.js b/src/matrix-react.js index 26ca4cc..398da7f 100644 --- a/src/matrix-react.js +++ b/src/matrix-react.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -75,6 +76,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-react", MatrixReact); } \ No newline at end of file diff --git a/src/matrix-receive.js b/src/matrix-receive.js index 404a32f..cb8102c 100644 --- a/src/matrix-receive.js +++ b/src/matrix-receive.js @@ -24,6 +24,7 @@ module.exports = function(RED) { node.error("No configuration node", {}); return; } + node.server.register(node); node.server.on("disconnected", function(){ node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -149,6 +150,10 @@ module.exports = function(RED) { node.send(msg); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-receive", MatrixReceiveMessage); } \ No newline at end of file diff --git a/src/matrix-room-ban.js b/src/matrix-room-ban.js index 6d6a71e..0b3e0e5 100644 --- a/src/matrix-room-ban.js +++ b/src/matrix-room-ban.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -58,6 +59,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-room-ban", MatrixBan); } \ No newline at end of file diff --git a/src/matrix-room-invite.js b/src/matrix-room-invite.js index 1492538..1dbbfae 100644 --- a/src/matrix-room-invite.js +++ b/src/matrix-room-invite.js @@ -12,6 +12,7 @@ module.exports = function(RED) { node.error('Server must be configured on the node.', {}); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -26,6 +27,10 @@ module.exports = function(RED) { node.server.on("Room.invite", async function(msg) { node.send(msg); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-room-invite", MatrixRoomInvite); } \ No newline at end of file diff --git a/src/matrix-room-kick.js b/src/matrix-room-kick.js index 13007ca..f93befe 100644 --- a/src/matrix-room-kick.js +++ b/src/matrix-room-kick.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -58,6 +59,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-room-kick", MatrixKick); } \ No newline at end of file diff --git a/src/matrix-room-users.js b/src/matrix-room-users.js index 07eb34f..7df3c02 100644 --- a/src/matrix-room-users.js +++ b/src/matrix-room-users.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -65,6 +66,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-room-users", MatrixRoomUsers); } \ No newline at end of file diff --git a/src/matrix-send-file.js b/src/matrix-send-file.js index 923f40c..ffac909 100644 --- a/src/matrix-send-file.js +++ b/src/matrix-send-file.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -94,6 +95,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-send-file", MatrixSendFile); } \ No newline at end of file diff --git a/src/matrix-send-image.js b/src/matrix-send-image.js index 6035310..b309db6 100644 --- a/src/matrix-send-image.js +++ b/src/matrix-send-image.js @@ -13,6 +13,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -98,6 +99,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-send-image", MatrixSendImage); } \ No newline at end of file diff --git a/src/matrix-send-message.js b/src/matrix-send-message.js index 79494a7..6819f92 100644 --- a/src/matrix-send-message.js +++ b/src/matrix-send-message.js @@ -54,6 +54,7 @@ module.exports = function(RED) { node.warn("No configuration node"); return; } + node.server.register(node); node.status({ fill: "red", shape: "ring", text: "disconnected" }); @@ -152,6 +153,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-send-message", MatrixSendImage); } \ No newline at end of file diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 59d0cf9..b2efab9 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -44,6 +44,7 @@ module.exports = function(RED) { this.credentials = {}; } + this.users = {}; this.connected = null; this.name = n.name; this.userId = this.credentials.userId; @@ -423,6 +424,14 @@ module.exports = function(RED) { } ) })(); + + // Keep track of all consumers of this node to be able to catch errors + node.register = function(consumerNode) { + node.users[consumerNode.id] = consumerNode; + }; + node.deregister = function(consumerNode) { + delete node.users[consumerNode.id]; + }; } } From f48ba74a7273dff6fcee0f2b4b8852b0b1ec4190 Mon Sep 17 00:00:00 2001 From: bvmensvoort Date: Sun, 11 Jun 2023 10:46:30 +0200 Subject: [PATCH 03/29] Make errors of config node catchable via a catch node --- src/matrix-server-config.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index b2efab9..19da821 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -69,9 +69,9 @@ module.exports = function(RED) { let retryStartTimeout = null; if(!this.credentials.accessToken) { - node.error("Matrix connection failed: missing access token in configuration."); + node.error("Matrix connection failed: missing access token in configuration.", {}); } else if(!this.url) { - node.error("Matrix connection failed: missing server URL in configuration."); + node.error("Matrix connection failed: missing server URL in configuration.", {}); } else { node.setConnected = async function(connected, cb) { if (node.connected !== connected) { @@ -89,7 +89,7 @@ module.exports = function(RED) { device_id = this.matrixClient.getDeviceId(); if(!device_id && node.enableE2ee) { - node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.") + node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.", {}) } else { if(!stored_device_id || stored_device_id !== device_id) { node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`); @@ -108,13 +108,13 @@ module.exports = function(RED) { }).then( function(response) {}, function(error) { - node.error("Failed to set device label: " + error); + node.error("Failed to set device label: " + error, {}); } ); } }, function(error) { - node.error("Failed to fetch device: " + error); + node.error("Failed to fetch device: " + error, {}); } ); } @@ -190,7 +190,7 @@ module.exports = function(RED) { try { await node.matrixClient.decryptEventIfNeeded(event); } catch (error) { - node.error(error); + node.error(error, {}); return; } @@ -293,7 +293,7 @@ module.exports = function(RED) { } else if(prevState === null && state === "ERROR") { // Occurs when the initial sync failed first time. node.setConnected(false, function(){ - node.error("Failed to connect to Matrix server"); + node.error("Failed to connect to Matrix server", {}); }); } else if(prevState === "ERROR" && state === "PREPARED") { // Occurs when the initial sync succeeds @@ -310,18 +310,18 @@ module.exports = function(RED) { } else if(prevState === "SYNCING" && state === "RECONNECTING") { // Occurs when the live update fails. node.setConnected(false, function(){ - node.error("Connection to Matrix server lost"); + node.error("Connection to Matrix server lost", {}); }); } else if(prevState === "RECONNECTING" && state === "RECONNECTING") { // Can occur if the update calls continue to fail, // but the keepalive calls (to /versions) succeed. node.setConnected(false, function(){ - node.error("Connection to Matrix server lost"); + node.error("Connection to Matrix server lost", {}); }); } else if(prevState === "RECONNECTING" && state === "ERROR") { // Occurs when the keepalive call also fails node.setConnected(false, function(){ - node.error("Connection to Matrix server lost"); + node.error("Connection to Matrix server lost", {}); }); } else if(prevState === "ERROR" && state === "SYNCING") { // Occurs when the client has performed a @@ -333,7 +333,7 @@ module.exports = function(RED) { // Occurs when the client has failed to // keepalive for a second time or more. node.setConnected(false, function(){ - node.error("Connection to Matrix server lost"); + node.error("Connection to Matrix server lost", {}); }); } else if(prevState === "SYNCING" && state === "SYNCING") { // Occurs when the client has performed a live update. @@ -345,7 +345,7 @@ module.exports = function(RED) { // Occurs once the client has stopped syncing or // trying to sync after stopClient has been called. node.setConnected(false, function(){ - node.error("Connection to Matrix server lost"); + node.error("Connection to Matrix server lost", {}); }); } }); @@ -363,7 +363,7 @@ module.exports = function(RED) { // httpStatus: 401 // } - node.error("Authentication failure: " + errorObj); + node.error("Authentication failure: " + errorObj, {}); stopClient(); }); @@ -379,7 +379,7 @@ module.exports = function(RED) { initialSyncLimit: 8 }); } catch(error) { - node.error(error); + node.error(error, {}); } } @@ -400,7 +400,7 @@ module.exports = function(RED) { .then( function(data) { if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) { - node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched."); + node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched.", {}); } if('device_id' in data && data['device_id'] && !node.deviceId) { // if we have no device_id configured lets use the one @@ -410,7 +410,7 @@ module.exports = function(RED) { // make sure our userId matches the access token's if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) { - node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`); + node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`, {}); return; } run().catch((error) => node.error(error)); @@ -419,7 +419,7 @@ module.exports = function(RED) { // if the error isn't authentication related retry in a little bit if(err.code !== "M_UNKNOWN_TOKEN") { retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000); - node.error("Auth check failed: " + err); + node.error("Auth check failed: " + err, {}); } } ) @@ -503,7 +503,7 @@ module.exports = function(RED) { fs.copySync(oldStorageDir, dir); } } catch (err) { - node.error(err); + node.error(err, {}); } }); From 20c718251187ee5e755c51aa4e6a662fe04f66dc Mon Sep 17 00:00:00 2001 From: bvmensvoort Date: Sun, 11 Jun 2023 10:50:15 +0200 Subject: [PATCH 04/29] Make errors catchable and (de)register at config node of all nodes --- src/matrix-synapse-create-edit-user.js | 11 ++++++++--- src/matrix-synapse-deactivate-user.js | 11 ++++++++--- src/matrix-synapse-join-room.js | 13 ++++++++----- src/matrix-synapse-register.js | 16 +++++++++++----- src/matrix-synapse-users.js | 8 +++++++- src/matrix-whois-user.js | 12 +++++++++--- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/matrix-synapse-create-edit-user.js b/src/matrix-synapse-create-edit-user.js index 67dca0d..ed1925d 100644 --- a/src/matrix-synapse-create-edit-user.js +++ b/src/matrix-synapse-create-edit-user.js @@ -8,9 +8,10 @@ module.exports = function(RED) { this.server = RED.nodes.getNode(n.server); if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } + node.server.register(node); this.encodeUri = function(pathTemplate, variables) { for (const key in variables) { @@ -41,12 +42,12 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } if(!msg.userId) { - node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)"); + node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", {}); return; } @@ -69,6 +70,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser); } \ No newline at end of file diff --git a/src/matrix-synapse-deactivate-user.js b/src/matrix-synapse-deactivate-user.js index 939692b..a881057 100644 --- a/src/matrix-synapse-deactivate-user.js +++ b/src/matrix-synapse-deactivate-user.js @@ -8,9 +8,10 @@ module.exports = function(RED) { this.server = RED.nodes.getNode(n.server); if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } + node.server.register(node); this.encodeUri = function(pathTemplate, variables) { for (const key in variables) { @@ -41,12 +42,12 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } if(!msg.userId) { - node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)"); + node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", {}); return; } @@ -70,6 +71,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-synapse-deactivate-user", MatrixSynapseDeactivateUser); } \ No newline at end of file diff --git a/src/matrix-synapse-join-room.js b/src/matrix-synapse-join-room.js index d08d794..0cb0ce3 100644 --- a/src/matrix-synapse-join-room.js +++ b/src/matrix-synapse-join-room.js @@ -9,10 +9,9 @@ module.exports = function(RED) { this.roomId = n.roomId; if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } - this.encodeUri = function(pathTemplate, variables) { for (const key in variables) { if (!variables.hasOwnProperty(key)) { @@ -42,18 +41,18 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("room must be defined in either msg.topic or in node config"); + node.error("room must be defined in either msg.topic or in node config", {}); return; } if(!msg.userId) { - node.error("msg.userId is required to set user into a room"); + node.error("msg.userId is required to set user into a room", {}); return; } @@ -77,6 +76,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-synapse-join-room", MatrixJoinRoom); } \ No newline at end of file diff --git a/src/matrix-synapse-register.js b/src/matrix-synapse-register.js index 25cc3db..21223cf 100644 --- a/src/matrix-synapse-register.js +++ b/src/matrix-synapse-register.js @@ -12,25 +12,27 @@ module.exports = function(RED) { this.sharedSecret = this.credentials.sharedSecret; if(!this.server) { - node.error('Server URL must be configured on the node.'); + node.error('Server URL must be configured on the node.', {}); return; } if(!this.sharedSecret) { - node.error('Shared registration secret must be configured on the node.'); + node.error('Shared registration secret must be configured on the node.', {}); return; } + node.server.register(node); + node.on("input", async function (msg) { const { got } = await import('got'); if(!msg.payload.username) { - node.error("msg.payload.username is required"); + node.error("msg.payload.username is required", {}); return; } if(!msg.payload.password) { - node.error("msg.payload.password is required"); + node.error("msg.payload.password is required", {}); return; } @@ -50,7 +52,7 @@ module.exports = function(RED) { var nonce = response.body.nonce; if(!nonce) { - node.error('Could not get nonce from /_synapse/admin/v1/register'); + node.error('Could not get nonce from /_synapse/admin/v1/register', {}); return; } @@ -96,6 +98,10 @@ module.exports = function(RED) { node.send(msg); })(); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-synapse-register", MatrixSynapseRegister, { credentials: { diff --git a/src/matrix-synapse-users.js b/src/matrix-synapse-users.js index 9beb1b1..eb1975e 100644 --- a/src/matrix-synapse-users.js +++ b/src/matrix-synapse-users.js @@ -12,6 +12,8 @@ module.exports = function(RED) { return; } + node.server.register(node); + node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.server.on("disconnected", function(){ @@ -29,7 +31,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } @@ -62,6 +64,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-synapse-users", MatrixSynapseUsers); } \ No newline at end of file diff --git a/src/matrix-whois-user.js b/src/matrix-whois-user.js index 3a22feb..226b623 100644 --- a/src/matrix-whois-user.js +++ b/src/matrix-whois-user.js @@ -8,10 +8,12 @@ module.exports = function(RED) { this.server = RED.nodes.getNode(n.server); if(!this.server) { - node.error('Server must be configured on the node.'); + node.error('Server must be configured on the node.', {}); return; } + node.server.register(node); + this.encodeUri = function(pathTemplate, variables) { for (const key in variables) { if (!variables.hasOwnProperty(key)) { @@ -41,12 +43,12 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed"); + node.error("Matrix server connection is currently closed", {}); node.send([null, msg]); } if(!msg.userId) { - node.error("msg.userId must be set to get user whois data"); + node.error("msg.userId must be set to get user whois data", {}); return; } @@ -70,6 +72,10 @@ module.exports = function(RED) { node.send([null, msg]); }); }); + + node.on("close", function() { + node.server.deregister(node); + }); } RED.nodes.registerType("matrix-whois-user", MatrixWhoIsUser); } \ No newline at end of file From 9661922f782126837bbe7035e70c96130d7aa7f4 Mon Sep 17 00:00:00 2001 From: bvmensvoort Date: Wed, 14 Jun 2023 21:50:13 +0200 Subject: [PATCH 05/29] Pass msg object where possible As described on https://nodered.org/docs/user-guide/writing-functions#handling-errors --- src/matrix-create-room.js | 2 +- src/matrix-crypt-file.js | 8 ++++---- src/matrix-delete-event.js | 4 ++-- src/matrix-invite-room.js | 6 +++--- src/matrix-join-room.js | 8 ++++---- src/matrix-leave-room.js | 8 ++++---- src/matrix-react.js | 10 +++++----- src/matrix-room-ban.js | 10 +++++----- src/matrix-room-kick.js | 10 +++++----- src/matrix-room-users.js | 4 ++-- src/matrix-send-file.js | 4 ++-- src/matrix-send-image.js | 4 ++-- src/matrix-send-message.js | 8 ++++---- src/matrix-synapse-create-edit-user.js | 4 ++-- src/matrix-synapse-deactivate-user.js | 4 ++-- src/matrix-synapse-join-room.js | 6 +++--- src/matrix-synapse-register.js | 6 +++--- src/matrix-synapse-users.js | 2 +- src/matrix-whois-user.js | 4 ++-- 19 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/matrix-create-room.js b/src/matrix-create-room.js index d5b7320..f9b7fa2 100644 --- a/src/matrix-create-room.js +++ b/src/matrix-create-room.js @@ -42,7 +42,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } diff --git a/src/matrix-crypt-file.js b/src/matrix-crypt-file.js index e58086d..779a1c8 100644 --- a/src/matrix-crypt-file.js +++ b/src/matrix-crypt-file.js @@ -12,22 +12,22 @@ module.exports = function(RED) { const { got } = await import('got'); if(!msg.type) { - node.error('msg.type is required.'); + node.error('msg.type is required.', msg); return; } if(!msg.content) { - node.error('msg.content is required.'); + node.error('msg.content is required.', msg); return; } if(!msg.content.file) { - node.error('msg.content.file is required.'); + node.error('msg.content.file is required.', msg); return; } if(!msg.url) { - node.error('msg.url is required.'); + node.error('msg.url is required.', msg); return; } diff --git a/src/matrix-delete-event.js b/src/matrix-delete-event.js index 187eae3..132ba16 100644 --- a/src/matrix-delete-event.js +++ b/src/matrix-delete-event.js @@ -28,7 +28,7 @@ module.exports = function(RED) { node.on('input', function(msg) { if(!msg.eventId) { - node.error("eventId is missing", {}); + node.error("eventId is missing", msg); node.send([null, msg]) return; } @@ -39,7 +39,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); return; } diff --git a/src/matrix-invite-room.js b/src/matrix-invite-room.js index dcf0689..df6afac 100644 --- a/src/matrix-invite-room.js +++ b/src/matrix-invite-room.js @@ -38,18 +38,18 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected", {}); + node.error("No matrix server selected", msg); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("room must be defined in either msg.topic or in node config", {}); + node.error("room must be defined in either msg.topic or in node config", msg); return; } diff --git a/src/matrix-join-room.js b/src/matrix-join-room.js index 438a7af..a3b885e 100644 --- a/src/matrix-join-room.js +++ b/src/matrix-join-room.js @@ -25,17 +25,17 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected", {}); + node.error("No matrix server selected", msg); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } if(!msg.topic) { - node.error("Room must be specified in msg.topic", {}); + node.error("Room must be specified in msg.topic", msg); return; } @@ -47,7 +47,7 @@ module.exports = function(RED) { node.send([msg, null]); }) .catch(function(e){ - node.error("Error trying to join room " + msg.topic + ":" + e, {}); + node.error("Error trying to join room " + msg.topic + ":" + e, msg); msg.error = e; node.send([null, msg]); }); diff --git a/src/matrix-leave-room.js b/src/matrix-leave-room.js index 02d04a2..ecc171b 100644 --- a/src/matrix-leave-room.js +++ b/src/matrix-leave-room.js @@ -26,17 +26,17 @@ module.exports = function(RED) { node.on('input', function(msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected", {}); + node.error("No matrix server selected", msg); return; } if(!msg.topic) { - node.error('No room provided in msg.topic', {}); + node.error('No room provided in msg.topic', msg); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } @@ -45,7 +45,7 @@ module.exports = function(RED) { node.server.matrixClient.leave(msg.topic); node.send([msg, null]); } catch(e) { - node.error("Failed to leave room " + msg.topic + ": " + e, {}); + node.error("Failed to leave room " + msg.topic + ": " + e, msg); msg.payload = e; node.send([null, msg]); } diff --git a/src/matrix-react.js b/src/matrix-react.js index 398da7f..4ce6b3d 100644 --- a/src/matrix-react.js +++ b/src/matrix-react.js @@ -27,30 +27,30 @@ module.exports = function(RED) { node.on("input", function (msg) { if (!node.server || !node.server.matrixClient) { - node.error("No matrix server selected", {}); + node.error("No matrix server selected", msg); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("Room must be specified in msg.topic or in configuration", {}); + node.error("Room must be specified in msg.topic or in configuration", msg); return; } let payload = n.reaction || msg.payload; if(!payload) { - node.error('msg.payload must be defined or the reaction configured on the node.', {}); + node.error('msg.payload must be defined or the reaction configured on the node.', msg); return; } let eventId = msg.referenceEventId || msg.eventId; if(!eventId) { - node.error('Either msg.referenceEventId or msg.eventId must be defined to react to a message.', {}); + node.error('Either msg.referenceEventId or msg.eventId must be defined to react to a message.', msg); return; } diff --git a/src/matrix-room-ban.js b/src/matrix-room-ban.js index 0b3e0e5..a7d8435 100644 --- a/src/matrix-room-ban.js +++ b/src/matrix-room-ban.js @@ -27,23 +27,23 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected", {}); + node.error("No matrix server selected", msg); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("Room must be specified in msg.topic or in configuration", {}); + node.error("Room must be specified in msg.topic or in configuration", msg); return; } if(!msg.userId) { - node.error("msg.userId was not set.", {}); + node.error("msg.userId was not set.", msg); return; } @@ -54,7 +54,7 @@ module.exports = function(RED) { node.send([msg, null]); }) .catch(function(e){ - node.error("Error trying to ban " + msg.userId + " from " + msg.topic, {}); + node.error("Error trying to ban " + msg.userId + " from " + msg.topic, msg); msg.error = e; node.send([null, msg]); }); diff --git a/src/matrix-room-kick.js b/src/matrix-room-kick.js index f93befe..926311b 100644 --- a/src/matrix-room-kick.js +++ b/src/matrix-room-kick.js @@ -27,23 +27,23 @@ module.exports = function(RED) { node.on("input", function (msg) { if (! node.server || ! node.server.matrixClient) { - node.error("No matrix server selected", {}); + node.error("No matrix server selected", msg); return; } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("Room must be specified in msg.topic or in configuration", {}); + node.error("Room must be specified in msg.topic or in configuration", msg); return; } if(!msg.userId) { - node.error("msg.userId was not set.", {}); + node.error("msg.userId was not set.", msg); return; } @@ -54,7 +54,7 @@ module.exports = function(RED) { node.send([msg, null]); }) .catch(function(e){ - node.error("Error trying to kick " + msg.userId + " from " + msg.topic, {}); + node.error("Error trying to kick " + msg.userId + " from " + msg.topic, msg); msg.error = e; node.send([null, msg]); }); diff --git a/src/matrix-room-users.js b/src/matrix-room-users.js index 7df3c02..1a5347e 100644 --- a/src/matrix-room-users.js +++ b/src/matrix-room-users.js @@ -32,13 +32,13 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } let roomId = node.roomId || msg.topic; if(!roomId) { - node.error("msg.topic is required. Specify in the input or configure the room ID on the node.", {}); + node.error("msg.topic is required. Specify in the input or configure the room ID on the node.", msg); return; } diff --git a/src/matrix-send-file.js b/src/matrix-send-file.js index ffac909..ad89615 100644 --- a/src/matrix-send-file.js +++ b/src/matrix-send-file.js @@ -32,7 +32,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } @@ -58,7 +58,7 @@ module.exports = function(RED) { } if(!msg.payload) { - node.error('msg.payload is required', {}); + node.error('msg.payload is required', msg); return; } diff --git a/src/matrix-send-image.js b/src/matrix-send-image.js index b309db6..2e15e8f 100644 --- a/src/matrix-send-image.js +++ b/src/matrix-send-image.js @@ -32,7 +32,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } @@ -58,7 +58,7 @@ module.exports = function(RED) { } if(!msg.payload) { - node.error('msg.payload is required', {}); + node.error('msg.payload is required', msg); return; } diff --git a/src/matrix-send-message.js b/src/matrix-send-message.js index 6819f92..632ace5 100644 --- a/src/matrix-send-message.js +++ b/src/matrix-send-message.js @@ -72,7 +72,7 @@ module.exports = function(RED) { if(msgType === 'msg.type') { if(!msg.type) { - node.error("msg.type type is set to be passed in via msg.type but was not defined", {}); + node.error("msg.type type is set to be passed in via msg.type but was not defined", msg); return; } msgType = msg.type; @@ -80,7 +80,7 @@ module.exports = function(RED) { if(msgFormat === 'msg.format') { if(!msg.format) { - node.error("Message format is set to be passed in via msg.format but was not defined", {}); + node.error("Message format is set to be passed in via msg.format but was not defined", msg); return; } msgFormat = msg.format; @@ -92,7 +92,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); return; } @@ -105,7 +105,7 @@ module.exports = function(RED) { let payload = n.message || msg.payload; if(!payload) { - node.error('msg.payload must be defined or the message configured on the node.', {}); + node.error('msg.payload must be defined or the message configured on the node.', msg); return; } diff --git a/src/matrix-synapse-create-edit-user.js b/src/matrix-synapse-create-edit-user.js index ed1925d..43be104 100644 --- a/src/matrix-synapse-create-edit-user.js +++ b/src/matrix-synapse-create-edit-user.js @@ -42,12 +42,12 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } if(!msg.userId) { - node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", {}); + node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", msg); return; } diff --git a/src/matrix-synapse-deactivate-user.js b/src/matrix-synapse-deactivate-user.js index a881057..b6e909a 100644 --- a/src/matrix-synapse-deactivate-user.js +++ b/src/matrix-synapse-deactivate-user.js @@ -42,12 +42,12 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } if(!msg.userId) { - node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", {}); + node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", msg); return; } diff --git a/src/matrix-synapse-join-room.js b/src/matrix-synapse-join-room.js index 0cb0ce3..3fd51c2 100644 --- a/src/matrix-synapse-join-room.js +++ b/src/matrix-synapse-join-room.js @@ -41,18 +41,18 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } msg.topic = node.roomId || msg.topic; if(!msg.topic) { - node.error("room must be defined in either msg.topic or in node config", {}); + node.error("room must be defined in either msg.topic or in node config", msg); return; } if(!msg.userId) { - node.error("msg.userId is required to set user into a room", {}); + node.error("msg.userId is required to set user into a room", msg); return; } diff --git a/src/matrix-synapse-register.js b/src/matrix-synapse-register.js index 21223cf..bb6f9d7 100644 --- a/src/matrix-synapse-register.js +++ b/src/matrix-synapse-register.js @@ -27,12 +27,12 @@ module.exports = function(RED) { const { got } = await import('got'); if(!msg.payload.username) { - node.error("msg.payload.username is required", {}); + node.error("msg.payload.username is required", msg); return; } if(!msg.payload.password) { - node.error("msg.payload.password is required", {}); + node.error("msg.payload.password is required", msg); return; } @@ -52,7 +52,7 @@ module.exports = function(RED) { var nonce = response.body.nonce; if(!nonce) { - node.error('Could not get nonce from /_synapse/admin/v1/register', {}); + node.error('Could not get nonce from /_synapse/admin/v1/register', msg); return; } diff --git a/src/matrix-synapse-users.js b/src/matrix-synapse-users.js index eb1975e..f2079c6 100644 --- a/src/matrix-synapse-users.js +++ b/src/matrix-synapse-users.js @@ -31,7 +31,7 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } diff --git a/src/matrix-whois-user.js b/src/matrix-whois-user.js index 226b623..15de6d8 100644 --- a/src/matrix-whois-user.js +++ b/src/matrix-whois-user.js @@ -43,12 +43,12 @@ module.exports = function(RED) { } if(!node.server.isConnected()) { - node.error("Matrix server connection is currently closed", {}); + node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); } if(!msg.userId) { - node.error("msg.userId must be set to get user whois data", {}); + node.error("msg.userId must be set to get user whois data", msg); return; } From c920dd12cb86f717a0d48c776623a59d64da3907 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 14 Oct 2023 22:26:29 -0600 Subject: [PATCH 06/29] - Fix error with matrix-synapse-register node - Ensure matrix-server-config's register/deregister methods are always available --- src/matrix-server-config.js | 17 +++++++++-------- src/matrix-synapse-register.js | 2 -- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 19da821..19b3b12 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -17,6 +17,7 @@ if (!globalThis.fetch) { } module.exports = function(RED) { + console.log(RED.settings.contextStorage); // disable logging if set to "off" let loggingSettings = RED.settings.get('logging'); if( @@ -56,6 +57,14 @@ module.exports = function(RED) { this.globalAccess = n.global; this.initializedAt = new Date(); + + // Keep track of all consumers of this node to be able to catch errors + node.register = function(consumerNode) { + node.users[consumerNode.id] = consumerNode; + }; + node.deregister = function(consumerNode) { + delete node.users[consumerNode.id]; + }; if(!this.userId) { node.log("Matrix connection failed: missing user ID in configuration."); @@ -424,14 +433,6 @@ module.exports = function(RED) { } ) })(); - - // Keep track of all consumers of this node to be able to catch errors - node.register = function(consumerNode) { - node.users[consumerNode.id] = consumerNode; - }; - node.deregister = function(consumerNode) { - delete node.users[consumerNode.id]; - }; } } diff --git a/src/matrix-synapse-register.js b/src/matrix-synapse-register.js index bb6f9d7..2135821 100644 --- a/src/matrix-synapse-register.js +++ b/src/matrix-synapse-register.js @@ -21,8 +21,6 @@ module.exports = function(RED) { return; } - node.server.register(node); - node.on("input", async function (msg) { const { got } = await import('got'); From 0e755bc350e1f02d62ff10ca2233a8ef371ac5d4 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 14 Oct 2023 23:22:38 -0600 Subject: [PATCH 07/29] - Remove console.log usage --- src/matrix-server-config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 19b3b12..81ea4cf 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -17,7 +17,6 @@ if (!globalThis.fetch) { } module.exports = function(RED) { - console.log(RED.settings.contextStorage); // disable logging if set to "off" let loggingSettings = RED.settings.get('logging'); if( @@ -270,7 +269,6 @@ module.exports = function(RED) { if (member.membership === "invite" && member.userId === node.userId) { node.log("Got invite to join room " + member.roomId); - console.log(event); if(node.autoAcceptRoomInvites) { node.matrixClient.joinRoom(member.roomId).then(function() { node.log("Automatically accepted invitation to join room " + member.roomId); From c833a40a84721c816aaf8c213b18e642f39a141f Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 15 Oct 2023 04:17:12 -0600 Subject: [PATCH 08/29] Issue #97 Room Settings - Can set room name, topic, and avatar - Can get name, topic, avatar, encrypted, power_levels, aliases, guest_access, join_rule, and join_allow_rules --- package.json | 5 +- src/matrix-room-settings.html | 115 +++++++++++++++++++++++++++++++++ src/matrix-room-settings.js | 116 ++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/matrix-room-settings.html create mode 100644 src/matrix-room-settings.js diff --git a/package.json b/package.json index 6aeb583..d997df7 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,14 @@ "matrix-crypt-file": "src/matrix-crypt-file.js", "matrix-room-kick": "src/matrix-room-kick.js", "matrix-room-ban": "src/matrix-room-ban.js", + "matrix-room-users": "src/matrix-room-users.js", + "matrix-room-settings": "src/matrix-room-settings.js", "matrix-synapse-users": "src/matrix-synapse-users.js", "matrix-synapse-register": "src/matrix-synapse-register.js", "matrix-synapse-create-edit-user": "src/matrix-synapse-create-edit-user.js", "matrix-synapse-deactivate-user": "src/matrix-synapse-deactivate-user.js", "matrix-synapse-join-room": "src/matrix-synapse-join-room.js", - "matrix-whois-user": "src/matrix-whois-user.js", - "matrix-room-users": "src/matrix-room-users.js" + "matrix-whois-user": "src/matrix-whois-user.js" } }, "engines": { diff --git a/src/matrix-room-settings.html b/src/matrix-room-settings.html new file mode 100644 index 0000000..02e0d89 --- /dev/null +++ b/src/matrix-room-settings.html @@ -0,0 +1,115 @@ + + + + + + \ No newline at end of file diff --git a/src/matrix-room-settings.js b/src/matrix-room-settings.js new file mode 100644 index 0000000..153dc97 --- /dev/null +++ b/src/matrix-room-settings.js @@ -0,0 +1,116 @@ +module.exports = function(RED) { + function MatrixRoomSettings(n) { + RED.nodes.createNode(this, n); + + var node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomId = n.roomId; + this.returnValues = n.returnValues; + + if (!node.server) { + node.warn("No configuration node"); + return; + } + node.server.register(node); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on("input", async function (msg) { + if (! node.server || ! node.server.matrixClient) { + msg.error = "No matrix server selected"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + if(!node.server.isConnected()) { + msg.error = "Matrix server connection is currently closed"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + msg.topic = node.roomId || msg.topic; + if(!msg.topic) { + msg.error = "Room must be specified in msg.topic or in configuration"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + let errors = {}, + payload = {}; + + if(msg.payload?.name) { + try { + await node.server.matrixClient.setRoomName(msg.topic, msg.payload.name); + } catch(e) { + node.error("Set room name failed: " + e.message, msg); + errors.name = e.message; + } + } + + if(msg.payload?.topic) { + try { + await node.server.matrixClient.setRoomTopic(msg.topic, msg.payload.topic); + } catch(e) { + node.error("Set room topic failed: " + e.message, msg); + errors.topic = e.message; + } + } + + if(msg.payload?.avatar) { + try { + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.avatar", + { + "info": msg.payload?.avatar_info || undefined, + "url": msg.payload.avatar + }, + ""); + } catch(e) { + node.error("Set room avatar failed: " + e.message, msg); + errors.topic = e.message; + } + } + + if(Object.keys(errors).length) { + msg.errors = errors; + } + + if(node.returnValues) { + // return current settings + let join_rules = await node.server.matrixClient.getStateEvent(msg.topic, "m.room.join_rules", ""); + msg.payload = { + "name": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.name", ""))?.name, + "topic": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.topic", ""))?.topic, + "avatar": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.avatar", ""))?.url, + "encrypted": node.server.matrixClient.isRoomEncrypted(msg.topic), + "power_levels": await node.server.matrixClient.getStateEvent(msg.topic, "m.room.power_levels", ""), + "aliases": (await node.server.matrixClient.getLocalAliases(msg.topic))?.aliases, + "guest_access": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.guest_access", ""))?.guest_access, + "join_rule": join_rules?.join_rule, + "join_allow_rules": join_rules?.allow_rules + }; + } + + node.send([msg, null]); + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-room-settings", MatrixRoomSettings); +} \ No newline at end of file From 9d050a0d44de430e120c77136d3fd27d030b842d Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 15 Oct 2023 14:45:16 -0600 Subject: [PATCH 09/29] Issue #97 Room Settings - fix join_allow_rules for Room Setting node not getting correctly - update Room Setting docs --- src/matrix-room-settings.html | 2 +- src/matrix-room-settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix-room-settings.html b/src/matrix-room-settings.html index 02e0d89..edf1c84 100644 --- a/src/matrix-room-settings.html +++ b/src/matrix-room-settings.html @@ -44,7 +44,7 @@ Return current name, topic, and avatar in msg.payload
- You can set the payload to null, undefined, or false to just get the current settings without changing them. + You can set the payload to null, undefined, false, or an object that doesn't contain the keys needed to apply a change so you can just get the current settings without changing them.
- - - - - \ No newline at end of file diff --git a/src/matrix-room-settings.js b/src/matrix-room-settings.js deleted file mode 100644 index 1d952b5..0000000 --- a/src/matrix-room-settings.js +++ /dev/null @@ -1,116 +0,0 @@ -module.exports = function(RED) { - function MatrixRoomSettings(n) { - RED.nodes.createNode(this, n); - - var node = this; - - this.name = n.name; - this.server = RED.nodes.getNode(n.server); - this.roomId = n.roomId; - this.returnValues = n.returnValues; - - if (!node.server) { - node.warn("No configuration node"); - return; - } - node.server.register(node); - - node.status({ fill: "red", shape: "ring", text: "disconnected" }); - - node.server.on("disconnected", function(){ - node.status({ fill: "red", shape: "ring", text: "disconnected" }); - }); - - node.server.on("connected", function() { - node.status({ fill: "green", shape: "ring", text: "connected" }); - }); - - node.on("input", async function (msg) { - if (! node.server || ! node.server.matrixClient) { - msg.error = "No matrix server selected"; - node.error(msg.error, msg); - node.send([null, msg]); - return; - } - - if(!node.server.isConnected()) { - msg.error = "Matrix server connection is currently closed"; - node.error(msg.error, msg); - node.send([null, msg]); - return; - } - - msg.topic = node.roomId || msg.topic; - if(!msg.topic) { - msg.error = "Room must be specified in msg.topic or in configuration"; - node.error(msg.error, msg); - node.send([null, msg]); - return; - } - - let errors = {}, - payload = {}; - - if(msg.payload?.name) { - try { - await node.server.matrixClient.setRoomName(msg.topic, msg.payload.name); - } catch(e) { - node.error("Set room name failed: " + e.message, msg); - errors.name = e.message; - } - } - - if(msg.payload?.topic) { - try { - await node.server.matrixClient.setRoomTopic(msg.topic, msg.payload.topic); - } catch(e) { - node.error("Set room topic failed: " + e.message, msg); - errors.topic = e.message; - } - } - - if(msg.payload?.avatar) { - try { - await node.server.matrixClient.sendStateEvent( - msg.topic, - "m.room.avatar", - { - "info": msg.payload?.avatar_info || undefined, - "url": msg.payload.avatar - }, - ""); - } catch(e) { - node.error("Set room avatar failed: " + e.message, msg); - errors.topic = e.message; - } - } - - if(Object.keys(errors).length) { - msg.errors = errors; - } - - if(node.returnValues) { - // return current settings - let join_rules = await node.server.matrixClient.getStateEvent(msg.topic, "m.room.join_rules", ""); - msg.payload = { - "name": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.name", ""))?.name, - "topic": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.topic", ""))?.topic, - "avatar": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.avatar", ""))?.url, - "encrypted": node.server.matrixClient.isRoomEncrypted(msg.topic), - "power_levels": await node.server.matrixClient.getStateEvent(msg.topic, "m.room.power_levels", ""), - "aliases": (await node.server.matrixClient.getLocalAliases(msg.topic))?.aliases, - "guest_access": (await node.server.matrixClient.getStateEvent(msg.topic, "m.room.guest_access", ""))?.guest_access, - "join_rule": join_rules?.join_rule, - "join_allow_rules": join_rules?.allow - }; - } - - node.send([msg, null]); - }); - - node.on("close", function() { - node.server.deregister(node); - }); - } - RED.nodes.registerType("matrix-room-settings", MatrixRoomSettings); -} \ No newline at end of file diff --git a/src/matrix-room-state-events.html b/src/matrix-room-state-events.html new file mode 100644 index 0000000..51a9a15 --- /dev/null +++ b/src/matrix-room-state-events.html @@ -0,0 +1,356 @@ + + + + + + \ No newline at end of file diff --git a/src/matrix-room-state-events.js b/src/matrix-room-state-events.js new file mode 100644 index 0000000..e824ae7 --- /dev/null +++ b/src/matrix-room-state-events.js @@ -0,0 +1,273 @@ +module.exports = function(RED) { + function MatrixRoomSettings(n) { + RED.nodes.createNode(this, n); + + var node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomId = n.roomId; + this.returnValues = n.returnValues; + this.rules = n.rules; + + if (!node.server) { + node.warn("No configuration node"); + return; + } + node.server.register(node); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on("input", async function (msg) { + if (! node.server || ! node.server.matrixClient) { + msg.error = "No matrix server selected"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + if(!node.server.isConnected()) { + msg.error = "Matrix server connection is currently closed"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + msg.topic = node.roomId || msg.topic; + if(!msg.topic) { + msg.error = "Room must be specified in msg.topic or in configuration"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + let getterErrors = {}, + setterErrors = {}; + + if(!Array.isArray(node.rules) || !node.rules.length) { + node.warn("No rules configured, skipping", msg); + return msg; + } + + function getToValue(msg, rule) { + var value = rule.to; + if (rule.tot === 'json') { + try { + value = JSON.parse(rule.to); + } catch(e) { + throw new Error("Invalid JSON"); + } + } else if (rule.tot === 'bin') { + try { + value = Buffer.from(JSON.parse(rule.to)) + } catch(e) { + throw new Error("Invalid Binary"); + } + } + if (rule.tot === "msg") { + value = RED.util.getMessageProperty(msg,rule.to); + } else if ((rule.tot === 'flow') || (rule.tot === 'global')) { + RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg, (err,value) => { + if (err) { + throw new Error("Invalid value evaluation"); + } else { + return value; + } + }); + return + } else if (rule.tot === 'date') { + value = Date.now(); + } else if (rule.tot === 'jsonata') { + RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => { + if (err) { + throw new Error("Invalid expression"); + } else { + return value; + } + }); + return; + } + return value; + } + + function setToValue(value, rule) { + if(rule.tot === 'global' || rule.tot === 'flow') { + var contextKey = RED.util.parseContextStore(rule.to); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true) + } + var target = node.context()[rule.tot]; + var callback = err => { + if (err) { + node.error(err, msg); + getterErrors[rule.p] = err.message; + } + } + target.set(contextKey.key, value, contextKey.store, callback); + } else if(rule.tot === 'msg') { + if (!RED.util.setMessageProperty(msg, rule.to, value)) { + node.warn(RED._("change.errors.no-override",{property:rule.to})); + } + } + } + + for(let rule of node.rules) { + // [ + // { + // "t": "set", + // "p": "m.room.topic", + // "to": "asdf", + // "tot": "str" + // }, ... + // ] + + let cachedGetters = {}; + if(rule.t === 'set') { + let value; + try { + value = getToValue(msg, rule); + switch(rule.p) { + case "m.room.name": + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.name", + typeof value === "string" + ? { name: value } + : value); + break; + case "m.room.topic": + if(typeof value === "string") { + await node.server.matrixClient.setRoomTopic(msg.topic, value); + } else { + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.topic", + value + ); + } + break; + case "m.room.avatar": + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.avatar", + typeof value === "string" + ? { "url": value } + : value, + ""); + break; + case "m.room.power_levels": + if(typeof value !== 'object') { + setterErrors[rule.p] = "m.room.power_levels content must be object"; + } else { + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.power_levels", + value, + ""); + } + break; + case "m.room.guest_access": + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.guest_access", + typeof value === "string" + ? { "guest_access": value } + : value, + ""); + break; + case "m.room.join_rules": + if(typeof value !== 'object') { + setterErrors[rule.p] = "m.room.join_rules content must be object"; + } else { + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.join_rules", + value, + ""); + } + break; + case "m.room.canonical_alias": + if(typeof value !== 'object') { + setterErrors[rule.p] = "m.room.canonical_alias content must be object"; + } else { + await node.server.matrixClient.sendStateEvent( + msg.topic, + "m.room.canonical_alias", + value, + ""); + } + break; + default: + if(typeof value !== 'object') { + setterErrors[rule.p] = "Custom event content must be object"; + } else { + await node.server.matrixClient.sendStateEvent( + msg.topic, + rule.p, + value, + ""); + } + break; + } + } catch(e) { + setterErrors[rule.p] = e.message; + } + } else if(rule.t === 'get') { + let value; + if(cachedGetters.hasOwnProperty(rule.p)) { + value = cachedGetters[rule.p]; + } else { + try { + // we may want to fetch from local storage in the future, this is how to do that + // const room = this.getRoom(roomId); + // const ev = room.currentState.getStateEvents(EventType.RoomEncryption, ""); + value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, ""); + switch(rule.p) { + case "m.room.name": + value = value?.name + break; + case "m.room.topic": + value = value?.topic + break; + case "m.room.avatar": + value = value?.url + break; + case "m.room.guest_access": + value = value?.guest_access; + break; + } + setToValue(value, rule); + } catch(e) { + getterErrors[rule.p] = e; + } + } + + } + } + + if(Object.keys(setterErrors).length) { + msg.setter_errors = setterErrors; + } + + if(Object.keys(getterErrors).length) { + msg.getter_errors = getterErrors; + } + + node.send([msg, null]); + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-room-state-events", MatrixRoomSettings); +} \ No newline at end of file diff --git a/src/matrix-room-users.js b/src/matrix-room-users.js index 1a5347e..8a801c6 100644 --- a/src/matrix-room-users.js +++ b/src/matrix-room-users.js @@ -34,6 +34,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } let roomId = node.roomId || msg.topic; diff --git a/src/matrix-send-file.js b/src/matrix-send-file.js index ad89615..65e9344 100644 --- a/src/matrix-send-file.js +++ b/src/matrix-send-file.js @@ -34,6 +34,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } msg.topic = node.roomId || msg.topic; diff --git a/src/matrix-send-image.js b/src/matrix-send-image.js index 2e15e8f..29e2173 100644 --- a/src/matrix-send-image.js +++ b/src/matrix-send-image.js @@ -34,6 +34,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } msg.topic = node.roomId || msg.topic; diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 81ea4cf..920f3af 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -379,7 +379,7 @@ module.exports = function(RED) { if(node.e2ee){ node.log("Initializing crypto..."); await node.matrixClient.initCrypto(); - node.matrixClient.setGlobalErrorOnUnknownDevices(false); + node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices } node.log("Connecting to Matrix server..."); await node.matrixClient.startClient({ diff --git a/src/matrix-synapse-create-edit-user.js b/src/matrix-synapse-create-edit-user.js index 43be104..e13aa7f 100644 --- a/src/matrix-synapse-create-edit-user.js +++ b/src/matrix-synapse-create-edit-user.js @@ -44,6 +44,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } if(!msg.userId) { diff --git a/src/matrix-synapse-deactivate-user.js b/src/matrix-synapse-deactivate-user.js index b6e909a..62aaa49 100644 --- a/src/matrix-synapse-deactivate-user.js +++ b/src/matrix-synapse-deactivate-user.js @@ -44,6 +44,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } if(!msg.userId) { diff --git a/src/matrix-synapse-join-room.js b/src/matrix-synapse-join-room.js index 3fd51c2..9c683b3 100644 --- a/src/matrix-synapse-join-room.js +++ b/src/matrix-synapse-join-room.js @@ -43,6 +43,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } msg.topic = node.roomId || msg.topic; diff --git a/src/matrix-synapse-users.js b/src/matrix-synapse-users.js index f2079c6..e49ff24 100644 --- a/src/matrix-synapse-users.js +++ b/src/matrix-synapse-users.js @@ -33,6 +33,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } let queryParams = { diff --git a/src/matrix-whois-user.js b/src/matrix-whois-user.js index 15de6d8..70e335d 100644 --- a/src/matrix-whois-user.js +++ b/src/matrix-whois-user.js @@ -45,6 +45,7 @@ module.exports = function(RED) { if(!node.server.isConnected()) { node.error("Matrix server connection is currently closed", msg); node.send([null, msg]); + return; } if(!msg.userId) { From e7e0f2967b668eb55307773eb62531ff95346d28 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 21 Oct 2023 19:39:32 -0600 Subject: [PATCH 11/29] Issue #97 Room Settings - Remove unused returnValues config option for Room State Events node --- src/matrix-room-state-events.html | 14 -------------- src/matrix-room-state-events.js | 1 - 2 files changed, 15 deletions(-) diff --git a/src/matrix-room-state-events.html b/src/matrix-room-state-events.html index 51a9a15..f778a47 100644 --- a/src/matrix-room-state-events.html +++ b/src/matrix-room-state-events.html @@ -12,19 +12,6 @@ -
- - -
- You can set the payload to null, undefined, false, or an object that doesn't contain the keys needed to apply a change so you can just get the current settings without changing them. -
-
@@ -134,7 +121,6 @@ server: { value: "", type: "matrix-server-config" }, roomId: { value: null }, reason: { value: null }, - returnValues: { value: true }, rules: { value: defaultRules, validate: function(rules, opt) { diff --git a/src/matrix-room-state-events.js b/src/matrix-room-state-events.js index e824ae7..e8f2a1f 100644 --- a/src/matrix-room-state-events.js +++ b/src/matrix-room-state-events.js @@ -7,7 +7,6 @@ module.exports = function(RED) { this.name = n.name; this.server = RED.nodes.getNode(n.server); this.roomId = n.roomId; - this.returnValues = n.returnValues; this.rules = n.rules; if (!node.server) { From fd605005d1826fd2eb928374592535232bd3ee4b Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 21 Oct 2023 19:47:12 -0600 Subject: [PATCH 12/29] Closes #99 - matrix-server-config now auto populates with first option --- src/matrix-create-room.html | 2 +- src/matrix-delete-event.html | 2 +- src/matrix-invite-room.html | 2 +- src/matrix-join-room.html | 2 +- src/matrix-leave-room.html | 2 +- src/matrix-react.html | 2 +- src/matrix-receive.html | 2 +- src/matrix-room-ban.html | 2 +- src/matrix-room-invite.html | 2 +- src/matrix-room-kick.html | 2 +- src/matrix-room-state-events.html | 2 +- src/matrix-room-users.html | 2 +- src/matrix-send-file.html | 2 +- src/matrix-send-image.html | 2 +- src/matrix-send-message.html | 2 +- src/matrix-synapse-create-edit-user.html | 2 +- src/matrix-synapse-deactivate-user.html | 2 +- src/matrix-synapse-join-room.html | 2 +- src/matrix-synapse-users.html | 2 +- src/matrix-whois-user.html | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/matrix-create-room.html b/src/matrix-create-room.html index d57ae59..ee7926d 100644 --- a/src/matrix-create-room.html +++ b/src/matrix-create-room.html @@ -8,7 +8,7 @@ outputs: 2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" } + server: { type: "matrix-server-config" } }, label: function() { return this.name || "Create Room"; diff --git a/src/matrix-delete-event.html b/src/matrix-delete-event.html index 899ac22..acbff0e 100644 --- a/src/matrix-delete-event.html +++ b/src/matrix-delete-event.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, reason: { value: "" }, }, diff --git a/src/matrix-invite-room.html b/src/matrix-invite-room.html index 20f49d1..dd5cb0d 100644 --- a/src/matrix-invite-room.html +++ b/src/matrix-invite-room.html @@ -8,7 +8,7 @@ outputs: 2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, }, label: function() { diff --git a/src/matrix-join-room.html b/src/matrix-join-room.html index c3c75f5..81e403e 100644 --- a/src/matrix-join-room.html +++ b/src/matrix-join-room.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" } + server: { type: "matrix-server-config" } }, label: function() { return this.name || "Join Room"; diff --git a/src/matrix-leave-room.html b/src/matrix-leave-room.html index 4795c66..7b14b68 100644 --- a/src/matrix-leave-room.html +++ b/src/matrix-leave-room.html @@ -8,7 +8,7 @@ outputs: 2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, }, label: function() { diff --git a/src/matrix-react.html b/src/matrix-react.html index b552980..e9706e2 100644 --- a/src/matrix-react.html +++ b/src/matrix-react.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, reaction: { value: null } }, diff --git a/src/matrix-receive.html b/src/matrix-receive.html index cfde1e7..a9c9d13 100644 --- a/src/matrix-receive.html +++ b/src/matrix-receive.html @@ -8,7 +8,7 @@ outputs:1, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: {"value": null}, acceptText: {"value": true}, acceptEmotes: {"value": true}, diff --git a/src/matrix-room-ban.html b/src/matrix-room-ban.html index 32bb931..1bb6f58 100644 --- a/src/matrix-room-ban.html +++ b/src/matrix-room-ban.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, reason: { value: null } }, diff --git a/src/matrix-room-invite.html b/src/matrix-room-invite.html index 79c9c39..a2999ee 100644 --- a/src/matrix-room-invite.html +++ b/src/matrix-room-invite.html @@ -8,7 +8,7 @@ outputs: 1, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, }, label: function() { diff --git a/src/matrix-room-kick.html b/src/matrix-room-kick.html index f0bf0e1..287f74b 100644 --- a/src/matrix-room-kick.html +++ b/src/matrix-room-kick.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, reason: { value: null } }, diff --git a/src/matrix-room-state-events.html b/src/matrix-room-state-events.html index f778a47..26c7be1 100644 --- a/src/matrix-room-state-events.html +++ b/src/matrix-room-state-events.html @@ -118,7 +118,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, reason: { value: null }, rules: { diff --git a/src/matrix-room-users.html b/src/matrix-room-users.html index 34e6452..c0fc2fa 100644 --- a/src/matrix-room-users.html +++ b/src/matrix-room-users.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: "" } }, label: function() { diff --git a/src/matrix-send-file.html b/src/matrix-send-file.html index e3edac5..86258e8 100644 --- a/src/matrix-send-file.html +++ b/src/matrix-send-file.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, contentType: { value: null } }, diff --git a/src/matrix-send-image.html b/src/matrix-send-image.html index 6d87fee..31f1dc1 100644 --- a/src/matrix-send-image.html +++ b/src/matrix-send-image.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, contentType: { value: null } }, diff --git a/src/matrix-send-message.html b/src/matrix-send-message.html index 9babaab..5c714ac 100644 --- a/src/matrix-send-message.html +++ b/src/matrix-send-message.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, message: { value: null }, messageType: { value: 'm.text' }, diff --git a/src/matrix-synapse-create-edit-user.html b/src/matrix-synapse-create-edit-user.html index 110aa9d..171f811 100644 --- a/src/matrix-synapse-create-edit-user.html +++ b/src/matrix-synapse-create-edit-user.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, }, label: function() { return this.name || "Synapse Add/Edit User"; diff --git a/src/matrix-synapse-deactivate-user.html b/src/matrix-synapse-deactivate-user.html index b6ba282..4b0404c 100644 --- a/src/matrix-synapse-deactivate-user.html +++ b/src/matrix-synapse-deactivate-user.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, }, label: function() { return this.name || "Synapse Deactivate User"; diff --git a/src/matrix-synapse-join-room.html b/src/matrix-synapse-join-room.html index bfdef39..e914ca8 100644 --- a/src/matrix-synapse-join-room.html +++ b/src/matrix-synapse-join-room.html @@ -8,7 +8,7 @@ outputs: 2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, roomId: { value: null }, }, label: function() { diff --git a/src/matrix-synapse-users.html b/src/matrix-synapse-users.html index 1d61e21..a115811 100644 --- a/src/matrix-synapse-users.html +++ b/src/matrix-synapse-users.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" } + server: { type: "matrix-server-config" } }, label: function() { return this.name || "Synapse User List"; diff --git a/src/matrix-whois-user.html b/src/matrix-whois-user.html index 72d761f..008437d 100644 --- a/src/matrix-whois-user.html +++ b/src/matrix-whois-user.html @@ -8,7 +8,7 @@ outputs:2, defaults: { name: { value: null }, - server: { value: "", type: "matrix-server-config" }, + server: { type: "matrix-server-config" }, }, label: function() { return this.name || "WhoIs User"; From 18596961226edd5fe2c7cdac66ffebde850dafd0 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 22 Oct 2023 00:29:12 -0600 Subject: [PATCH 13/29] - #97 added option to fetch state event from local storage and fallback to server if necessary (allows for faster lookups and gives the full event object with information about when/who created it, etc) - #97 remove num, bool, bin, and data from being options you can set to a state event (currently only objects and sometimes strings are allowed) - Updated Leave Room node so it deletes the room from local storage - Updated server config node so it deletes the matrix client from storage during shutdown (possibly solution to #94) --- src/matrix-leave-room.js | 1 + src/matrix-room-state-events.html | 30 +++++++++++++++++---- src/matrix-room-state-events.js | 44 +++++++++++++++++++------------ src/matrix-server-config.js | 7 +++++ 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/matrix-leave-room.js b/src/matrix-leave-room.js index daebb33..b46f8d7 100644 --- a/src/matrix-leave-room.js +++ b/src/matrix-leave-room.js @@ -44,6 +44,7 @@ module.exports = function(RED) { try { node.log("Leaving room " + msg.topic); node.server.matrixClient.leave(msg.topic); + node.server.matrixClient.store.removeRoom(msg.topic); node.send([msg, null]); } catch(e) { node.error("Failed to leave room " + msg.topic + ": " + e, msg); diff --git a/src/matrix-room-state-events.html b/src/matrix-room-state-events.html index 26c7be1..08395dc 100644 --- a/src/matrix-room-state-events.html +++ b/src/matrix-room-state-events.html @@ -69,7 +69,7 @@
dynamic string|object
-
You configure what room state events to output in the node configuration. m.room.name, m.room.avatar, and m.room.guest_access will come back as strings otherwise you will get the full content object of the event (find this by referencing the Matrix Client-Server docs)
+
You configure what room state events to output in the node configuration. m.room.name, m.room.avatar, and m.room.guest_access will come back as strings otherwise you will get the full content object of the event (find this by referencing the Matrix Client-Server docs). Additionally there is a setting when configuring a getter called "Fetch from local storage" that if enabled will search the local storage for the room and try to fetch the state event that way and fallback to hitting the server if that isn't possible.
@@ -156,15 +156,17 @@ .appendTo(row2_1) .typedInput({ default: defaultType || (type === 'set' ? 'str' : 'msg'), - types: (type === 'set' ? ['msg','flow','global','str','num','bool','json','bin','date','jsonata'] : ['msg', 'flow', 'global']) + types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global']) }); - var dcLabel = $('').appendTo(row2_2); + var lsLabel = $('').appendTo(row2_2); + var localStorageEl = $('').appendTo(lsLabel); + $('').text("Fetch from local storage").appendTo(lsLabel); propValInput.on("change", function(evt,type,val) { row2_2.toggle(type === "msg" || type === "flow" || type === "global" || type === "env"); }) - return [propValInput]; + return [propValInput, localStorageEl]; } $('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({ @@ -259,6 +261,7 @@ .appendTo(row4); let propertyValue = null; + let localStorageEl = null; let fromValue = null; let toValue = null; @@ -277,12 +280,18 @@ if (!propertyValue) { var parts = createPropertyValue(row2_1, row2_2, type); propertyValue = parts[0]; + localStorageEl = parts[1]; } else { - propertyValue.typedInput('types', (type === 'set' ? ['msg','flow','global','str','num','bool','json','bin','date','jsonata','env'] : ['msg', 'flow', 'global'])); + propertyValue.typedInput('types', (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])); } propertyValue.typedInput('show'); row2.show(); + if(type === 'get') { + localStorageEl.parent().show(); + } else { + localStorageEl.parent().hide(); + } row3.hide(); row4.hide(); }); @@ -292,7 +301,14 @@ if (rule.t === "set" || rule.t === "get") { var parts = createPropertyValue(row2_1, row2_2, rule.t, rule.tot); propertyValue = parts[0]; + localStorageEl = parts[1]; propertyValue.typedInput('value',rule.to); + localStorageEl.prop("checked", !!rule.ls); + if(rule.t === 'get') { + localStorageEl.show(); + } else { + localStorageEl.hide(); + } } selectField.change(); container[0].appendChild(fragment); @@ -319,6 +335,10 @@ to:rule.find(".node-input-rule-property-value").typedInput('value'), tot:rule.find(".node-input-rule-property-value").typedInput('type') }; + + if (r.t === "get" && rule.find(".node-input-rule-property-localStorage").prop("checked")) { + r.ls = true; + } node.rules.push(r); }); }, diff --git a/src/matrix-room-state-events.js b/src/matrix-room-state-events.js index e8f2a1f..bd6025c 100644 --- a/src/matrix-room-state-events.js +++ b/src/matrix-room-state-events.js @@ -226,23 +226,33 @@ module.exports = function(RED) { value = cachedGetters[rule.p]; } else { try { - // we may want to fetch from local storage in the future, this is how to do that - // const room = this.getRoom(roomId); - // const ev = room.currentState.getStateEvents(EventType.RoomEncryption, ""); - value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, ""); - switch(rule.p) { - case "m.room.name": - value = value?.name - break; - case "m.room.topic": - value = value?.topic - break; - case "m.room.avatar": - value = value?.url - break; - case "m.room.guest_access": - value = value?.guest_access; - break; + if(rule.ls) { + // we opted to lookup from local storage, will fallback to server if necessary + let room = node.server.matrixClient.getRoom(msg.topic); + if(room) { + value = await room.getLiveTimeline().getState("f").getStateEvents(rule.p, ""); + } + } + if(!value) { + // fetch the latest state event by type from server + value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, ""); + if(value) { + // normalize some simpler events for easier access + switch(rule.p) { + case "m.room.name": + value = value?.name + break; + case "m.room.topic": + value = value?.topic + break; + case "m.room.avatar": + value = value?.url + break; + case "m.room.guest_access": + value = value?.guest_access; + break; + } + } } setToValue(value, rule); } catch(e) { diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 920f3af..59c9e2a 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -174,6 +174,13 @@ module.exports = function(RED) { node.on('close', function(done) { stopClient(); + if(node.globalAccess) { + try { + node.context().global.delete('matrixClient["'+node.userId+'"]'); + } catch(e){ + node.error(e.message, {}); + } + } done(); }); From 2e9633e113b70df7eb3bdea8ef679f58533d5e09 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 22 Oct 2023 03:01:33 -0600 Subject: [PATCH 14/29] - #97 msg.state_key is now allowed as an input to Room State Events node for events that support it (required for m.space.child and m.space.parent) - #97 added support for m.room.history_visibility, m.room.server_acl, m.room.pinned_events, m.space.child, and m.space.parent - #97 fix issue with checkbox being hidden on config page when adding new setters/getters on config page --- src/matrix-room-state-events.html | 28 ++++++++----- src/matrix-room-state-events.js | 65 +++++++++++++++++++------------ 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/matrix-room-state-events.html b/src/matrix-room-state-events.html index 08395dc..1789769 100644 --- a/src/matrix-room-state-events.html +++ b/src/matrix-room-state-events.html @@ -49,6 +49,11 @@ string|object
You configure what room state events in the node configuration. m.room.name, m.room.avatar, and m.room.guest_access allow you to pass a string to set their value but all other room state events will require the full content object (find this by referencing the Matrix Client-Server docs)
+ +
msg.state_key + string +
+
Required for some events such as m.space.parent and m.room.child to set the referenced child/parent room

Outputs

@@ -77,13 +82,18 @@ + + + + \ No newline at end of file diff --git a/src/matrix-typing.js b/src/matrix-typing.js new file mode 100644 index 0000000..3facb78 --- /dev/null +++ b/src/matrix-typing.js @@ -0,0 +1,86 @@ +module.exports = function(RED) { + function MatrixTyping(n) { + RED.nodes.createNode(this, n); + + let node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomId = n.roomId; + this.roomType = n.roomType; + this.roomValue = n.roomValue; + this.typingType = n.typingType; + this.typingValue = n.typingValue; + this.timeoutMsType = n.timeoutMsType; + this.timeoutMsValue = n.timeoutMsValue; + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + if (!node.server) { + node.error("No configuration node", {}); + return; + } + node.server.register(node); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on('input', async function(msg) { + if (! node.server || ! node.server.matrixClient) { + node.error("No matrix server selected", msg); + return; + } + + if(!node.server.isConnected()) { + node.error("Matrix server connection is currently closed", msg); + node.send([null, msg]); + return; + } + + function getToValue(msg, type, property) { + let value = property; + if (type === "msg") { + value = RED.util.getMessageProperty(msg, property); + } else if ((type === 'flow') || (type === 'global')) { + try { + value = RED.util.evaluateNodeProperty(property, type, node, msg); + } catch(e2) { + throw new Error("Invalid value evaluation"); + } + } else if(type === "bool") { + value = (property === 'true'); + } + return value; + } + + try { + let roomId = getToValue(msg, node.roomType, node.roomValue), + typing = getToValue(msg, node.typingType, node.typingValue), + timeoutMs = getToValue(msg, node.timeoutMsType, node.timeoutMsValue); + + if(!roomId) { + node.error('No room provided in msg.topic', msg); + return; + } + + console.log("sending typing",roomId, typing, timeoutMs); + await node.server.matrixClient.sendTyping(roomId, typing, timeoutMs); + node.send([msg, null]); + } catch(e) { + node.error("Failed to send typing event " + msg.topic + ": " + e, msg); + msg.payload = e; + node.send([null, msg]); + } + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-typing", MatrixTyping); +} \ No newline at end of file From d7c4bc26bbe4b7dd8f6ce93ebbf4c253e261e83f Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 22 Oct 2023 16:56:49 -0600 Subject: [PATCH 16/29] - #100 add documentation for new typing node and update README - #100 fix typing event causing server error due to number being string - Mention using pantalaimon as an alternative for E2EE support in README --- README.md | 2 + examples/README.md | 10 +++ examples/send-typing-events.json | 127 +++++++++++++++++++++++++++++++ examples/send-typing-events.png | Bin 0 -> 18957 bytes src/matrix-typing.html | 6 +- src/matrix-typing.js | 4 +- 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 examples/send-typing-events.json create mode 100644 examples/send-typing-events.png diff --git a/README.md b/README.md index 32d723b..2bddaa6 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ The following is supported from this package: - End-to-end encryption - [Currently a WIP](#end-to-end-encryption-notes) + - Can also use [pantalaimon](https://github.com/matrix-org/pantalaimon) as an alternative solution to E2EE (if you need multiple sessions synced up with keys) - Receive events from a room (messages, reactions, images, audio, locations, and files) whether encrypted or not - Send Images/Files (sending files to e2ee room doesn't currently encrypt them yet) - Edit messages +- Send typing events (Bot is typing ...) - Delete events (messages, reactions, etc) - Decrypt files in e2ee rooms - Send HTML/Plain Text Message/Notice diff --git a/examples/README.md b/examples/README.md index 5eaf8cf..9385a2e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,7 @@ Build something cool with these nodes? Feel free to submit a pull request to sha - [Respond to "rooms " with user's rooms (list server's rooms if is left blank)](#respond-to-rooms-user_id-with-users-rooms-list-servers-rooms-if-user_id-is-left-blank) - [Respond to "whois " with information about the user's session](#respond-to-whois-user_id-with-information-about-the-users-session) - [Respond to "room_users" with current room's users](#respond-to-room_users-with-current-rooms-users) +- [Sending typing events to a room](#sending-typing-events-to-a-room) - [Download & store all received files/images](#download--store-all-received-filesimages) - [Kick/Ban user from room](#kickban-user-from-room) - [Deactivate user](#deactivate-user) @@ -221,6 +222,15 @@ Note: You may need to edit the storage directory for this to work. Default actio ![store-received-files.png](store-received-files.png) +### Sending typing events to a room + +[View JSON](send-typing-events.json) + +You can tell a room that Node-RED is writing a message and also cancel the writing event. This can be useful for making bots feel more interactive (show typing while requesting API endpoint for example). + +![store-received-files.png](send-typing-events.png) + + ### Kick/Ban user from room [View JSON](room-kick-ban.json) diff --git a/examples/send-typing-events.json b/examples/send-typing-events.json new file mode 100644 index 0000000..e73f7b7 --- /dev/null +++ b/examples/send-typing-events.json @@ -0,0 +1,127 @@ +[ + { + "id": "541dbfc3f04220cf", + "type": "matrix-typing", + "z": "f025a8b9fbd1b054", + "name": "", + "server": null, + "roomType": "msg", + "roomValue": "topic", + "typingType": "msg", + "typingValue": "typing", + "timeoutMsType": "num", + "timeoutMsValue": "20000", + "x": 1090, + "y": 220, + "wires": [ + [ + "febf8236f3683963" + ], + [ + "9db9819ebb6343c8" + ] + ] + }, + { + "id": "d7c3714c657bfe4f", + "type": "inject", + "z": "f025a8b9fbd1b054", + "name": "Start Typing", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "typing", + "v": "true", + "vt": "bool" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "!mohVKgDFYUubJQHcuX:skylar.tech", + "x": 910, + "y": 200, + "wires": [ + [ + "541dbfc3f04220cf" + ] + ] + }, + { + "id": "783121ff1a6bd833", + "type": "inject", + "z": "f025a8b9fbd1b054", + "name": "Stop Typing", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "typing", + "v": "false", + "vt": "bool" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "!mohVKgDFYUubJQHcuX:skylar.tech", + "x": 910, + "y": 240, + "wires": [ + [ + "541dbfc3f04220cf" + ] + ] + }, + { + "id": "9db9819ebb6343c8", + "type": "debug", + "z": "f025a8b9fbd1b054", + "name": "Failure msg", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1270, + "y": 240, + "wires": [] + }, + { + "id": "febf8236f3683963", + "type": "debug", + "z": "f025a8b9fbd1b054", + "name": "Success msg", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1270, + "y": 200, + "wires": [] + }, + { + "id": "01e8c4c6303af390", + "type": "comment", + "z": "f025a8b9fbd1b054", + "name": "Send typing events to a room", + "info": "", + "x": 940, + "y": 160, + "wires": [] + } +] \ No newline at end of file diff --git a/examples/send-typing-events.png b/examples/send-typing-events.png new file mode 100644 index 0000000000000000000000000000000000000000..703bc2d3626e8e712f2767104d6a2bffa9169bbc GIT binary patch literal 18957 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 literal 0 HcmV?d00001 diff --git a/src/matrix-typing.html b/src/matrix-typing.html index 0be8db8..b426357 100644 --- a/src/matrix-typing.html +++ b/src/matrix-typing.html @@ -12,7 +12,7 @@ roomType: { value: "msg" }, roomValue: { value: "topic" }, typingType: { value: "bool" }, - typingValue: { value: true }, + typingValue: { value: "true" }, timeoutMsType: { value: "num" }, timeoutMsValue: { value: 20000 }, }, @@ -74,6 +74,10 @@ + +
+ Timeout MS is how many milliseconds the server should show the user typing for. +
diff --git a/src/matrix-upload-file.html b/src/matrix-upload-file.html new file mode 100644 index 0000000..e0aa396 --- /dev/null +++ b/src/matrix-upload-file.html @@ -0,0 +1,163 @@ + + + + + + \ No newline at end of file diff --git a/src/matrix-upload-file.js b/src/matrix-upload-file.js new file mode 100644 index 0000000..a203615 --- /dev/null +++ b/src/matrix-upload-file.js @@ -0,0 +1,471 @@ +const crypto = require("isomorphic-webcrypto"); +const ffmpeg = require('fluent-ffmpeg'); +const getImageSize = require('image-size'); +const tmp = require('tmp'); +const fs = require('fs'); +const path = require('path'); +module.exports = function(RED) { + function MatrixUploadFile(n) { + RED.nodes.createNode(this, n); + + let node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.inputType = n.inputType; + this.inputValue = n.inputValue; + this.fileNameType = n.fileNameType; + this.fileNameValue = n.fileNameValue; + this.contentType = n.contentType; + this.generateThumbnails = n.generateThumbnails; + + if (!node.server) { + node.warn("No configuration node"); + return; + } + node.server.register(node); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + async function detectFileType(filename, bufferOrPath) + { + const Mime = require('mime'); + let file = Buffer.isBuffer(bufferOrPath) ? filename : bufferOrPath; + + if(file) + { + let type = Mime.getType(file); + let ext = Mime.getExtension(file); + if(type) { + return {ext: ext, mime: type} + } + } + } + + function getFileBuffer(data) + { + if(Buffer.isBuffer(data)) { + return data; + } + + if (data && RED.settings.fileWorkingDirectory && !path.isAbsolute(data)) { + return fs.readFileSync(path.resolve(path.join(RED.settings.fileWorkingDirectory,data))); + } + return fs.readFileSync(data); + } + + function getToValue(msg, type, property) { + let value = property; + if (type === "msg") { + value = RED.util.getMessageProperty(msg, property); + } else if ((type === 'flow') || (type === 'global')) { + try { + value = RED.util.evaluateNodeProperty(property, type, node, msg); + } catch(e2) { + throw new Error("Invalid value evaluation"); + } + } else if(type === "bool") { + value = (property === 'true'); + } else if(type === "num") { + value = Number(property); + } + return value; + } + + node.on("input", onInput); + async function onInput(msg) + { + if (! node.server || ! node.server.matrixClient) { + node.warn("No matrix server selected"); + return; + } + + if(!node.server.isConnected()) { + node.error("Matrix server connection is currently closed", msg); + msg.error = "Matrix server connection is currently closed"; + node.send([null, msg]); + return; + } + + let bufferOrPath = getToValue(msg, node.inputType, node.inputValue); + if(!bufferOrPath) { + node.error('Missing file path/buffer input', msg); + msg.error = 'Missing file path/buffer input'; + node.send([null, msg]); + return; + } + + let filename = getToValue(msg, node.fileNameType, node.fileNameValue); + if(!filename || typeof filename !== 'string') { + if(!Buffer.isBuffer(bufferOrPath)) { + filename = path.basename(bufferOrPath); + } else { + node.error('Missing filename, this is required if input is a file buffer', msg); + msg.error = 'Missing filename, this is required if input is a file buffer'; + node.send([null, msg]); + return; + } + } + + msg.contentType = node.contentType || msg.contentType || null; + let detectedFileType = await detectFileType(filename, bufferOrPath); + node.log("Detected file type " + JSON.stringify(detectedFileType) + " for " + (Buffer.isBuffer(bufferOrPath) ? 'buffer' : `file ${bufferOrPath}`), msg); + + let contentType = msg.contentType || detectedFileType?.mime || null, + msgtype = msg.msgtype || null; + if(!contentType) { + node.warn("Content-type failed to detect, falling back to text/plain", msg); + contentType = 'text/plain'; + } + if(!msgtype) { + msgtype = autoDetectMatrixMessageType(detectedFileType); + } + + let encryptedFile = null; + if(msg.encrypted) { + encryptedFile = await encryptAttachment(getFileBuffer(bufferOrPath)); + } + + node.log("Uploading file ", msg); + let file; + try { + file = await node.server.matrixClient.uploadContent( + encryptedFile?.data || getFileBuffer(bufferOrPath), + { + name: filename, // Name to give the file on the server. + rawResponse: false, // Return the raw body, rather than parsing the JSON. + type: contentType, // Content-type for the upload. Defaults to file.type, or applicaton/octet-stream. + onlyContentUri: false // Just return the content URI, rather than the whole body. Defaults to false. Ignored if opts.rawResponse is true. + }); + } catch(e) { + node.error("Upload content error " + e); + msg.error = e; + node.send([null, msg]); + return; + } + + // we call this method when we need a file and cannot use the buffer + // so if we get passed a buffer we write it to a tmp file and return that + // otherwise we just return the string because it's already a file + let tempFile = null; + function getFile(bufferOrFile) { + if(!Buffer.isBuffer(bufferOrFile)) { + return bufferOrFile; // already a file + } + + if(tempFile) { + return tempFile; + } + + // write buffer to tmp file and return path + let tmpObj = tmp.fileSync({ postfix: `.${detectedFileType.ext}` }); + fs.writeFileSync(tmpObj.name, bufferOrFile); + tempFile = tmpObj.name; + return tmpObj.name; + } + + function deleteTempFile() { + if(!tempFile) return null; + fs.rmSync(tempFile); + } + + // get size of a buffer or file in bytes + function getFileSize(bufferOrPath) { + if(Buffer.isBuffer(bufferOrPath)) { + return Buffer.byteLength(bufferOrPath); + } + + return fs.statSync(bufferOrPath).size; + } + + async function addThumbnail(buffer) { + let imageSize = getImageSize(Buffer.isBuffer(buffer) ? buffer : buffer.data); + msg.payload.info.thumbnail_info = { + w: imageSize.width, + h: imageSize.height, + size: getFileSize(Buffer.isBuffer(buffer) ? buffer : buffer.data) + } + let uploadedThumbnail = await node.server.matrixClient.uploadContent( + Buffer.isBuffer(buffer) ? buffer : buffer.data, + { + name: "thumbnail.png", // Name to give the file on the server. + rawResponse: false, // Return the raw body, rather than parsing the JSON. + type: "image/png", // Content-type for the upload. Defaults to file.type, or applicaton/octet-stream. + onlyContentUri: false // Just return the content URI, rather than the whole body. Defaults to false. Ignored if opts.rawResponse is true. + }); + // delete local file + if(msg.encrypted) { + msg.payload.info.thumbnail_file.url = uploadedThumbnail.content_uri; + } else { + msg.payload.info.thumbnail_url = uploadedThumbnail.content_uri; + } + } + + function _ffmpegVideoThumbnail(filepath){ + return new Promise((resolve,reject) => { + let filename = `${msg._msgid}-screenshot.png`; + ffmpeg(filepath) + .on('end', async function() { + let path = `/tmp/${filename}`; + let buffer = getFileBuffer(path); + let encryptedThumbnail = null; + if(msg.encrypted) { + encryptedThumbnail = await encryptAttachment(buffer); + msg.payload.info.thumbnail_file = encryptedFile.info; + } + try { + await addThumbnail(encryptedThumbnail || buffer); + fs.rmSync(path); // delete temporary thumbnail file + resolve(); + } catch(e) { + return reject(new Error("Thumbnail upload failure: " + e)); + } + }) + .on('error', function(err) { + return reject(err); + }) + .screenshots({ + timestamps: [0], + filename: filename, + folder: '/tmp', + size: '320x?' + }); + }); + } + + msg.payload = {}; + if(msg.encrypted) { + msg.payload.file = encryptedFile?.info || {}; + msg.payload.file.url = file.content_uri; + } else { + msg.payload.url = file.content_uri; + } + msg.payload.msgtype = msgtype; + msg.payload.body = msg.body || msg.filename || ""; + msg.payload.info = { + "mimetype": contentType, + "size": getFileSize(bufferOrPath), + }; + if(msgtype === 'm.image') { + // detect size of image + try { + let imageSize = getImageSize(buffer); + msg.payload.info.h = imageSize.height; + msg.payload.info.w = imageSize.width; + } catch(e) { + node.error("Failed to get image size: " + e, msg); + } + } else if(msgtype === 'm.audio' && detectedFileType) { + try { + // detect duration of audio clip + let filepath = getFile(bufferOrPath); + let metadata = await _ffprobe(filepath); + let audioStream = metadata?.streams.filter(function(stream){return stream.codec_type === "audio" || false;})[0]; + if(audioStream?.duration) { + msg.payload.info.duration = audioStream?.duration * 1000; + } + } catch(e) { + node.error(e, msg); + } + deleteTempFile(); + } else if(msgtype === 'm.video' && detectedFileType) { + let filepath = getFile(bufferOrPath); + + try { + // detect duration & width/height of video clip + let metadata = await _ffprobe(filepath); + let videoStream = metadata?.streams.filter(function(stream){return stream.codec_type === "video" || false;})[0]; + if(videoStream) { + msg.payload.info.duration = videoStream.duration * 1000; + msg.payload.info.w = videoStream.width; + msg.payload.info.h = videoStream.height; + } + } catch(e) { + node.error("ffprobe error: " + e); + } + + if(node.generateThumbnails) { + try { + await _ffmpegVideoThumbnail(filepath); + } catch(e) { + node.error("Screenshot generation error: " + e); + } + } + deleteTempFile(); + } + + node.send(msg, null); + } + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-upload-file", MatrixUploadFile); + + // the following was taken & modified from https://github.com/matrix-org/browser-encrypt-attachment/blob/master/index.js + /** + * Encrypt an attachment. + * @param {ArrayBuffer} plaintextBuffer The attachment data buffer. + * @return {Promise} A promise that resolves with an object when the attachment is encrypted. + * The object has a "data" key with an ArrayBuffer of encrypted data and an "info" key + * with an object containing the info needed to decrypt the data. + */ + function encryptAttachment(plaintextBuffer) { + let cryptoKey; // The AES key object. + let exportedKey; // The AES key exported as JWK. + let ciphertextBuffer; // ArrayBuffer of encrypted data. + let sha256Buffer; // ArrayBuffer of digest. + let ivArray; // Uint8Array of AES IV + // Generate an IV where the first 8 bytes are random and the high 8 bytes + // are zero. We set the counter low bits to 0 since it makes it unlikely + // that the 64 bit counter will overflow. + ivArray = new Uint8Array(16); + crypto.getRandomValues(ivArray.subarray(0,8)); + // Load the encryption key. + return crypto.subtle.generateKey( + {"name": "AES-CTR", length: 256}, true, ["encrypt", "decrypt"] + ).then(function(generateKeyResult) { + cryptoKey = generateKeyResult; + // Export the Key as JWK. + return crypto.subtle.exportKey("jwk", cryptoKey); + }).then(function(exportKeyResult) { + exportedKey = exportKeyResult; + // Encrypt the input ArrayBuffer. + // Use half of the iv as the counter by setting the "length" to 64. + return crypto.subtle.encrypt( + {name: "AES-CTR", counter: ivArray, length: 64}, cryptoKey, plaintextBuffer + ); + }).then(function(encryptResult) { + ciphertextBuffer = encryptResult; + // SHA-256 the encrypted data. + return crypto.subtle.digest("SHA-256", ciphertextBuffer); + }).then(function (digestResult) { + sha256Buffer = digestResult; + + return { + data: ciphertextBuffer, + info: { + v: "v2", + key: exportedKey, + iv: encodeBase64(ivArray), + hashes: { + sha256: encodeBase64(new Uint8Array(sha256Buffer)), + }, + }, + }; + }); + } + + /** + * Decrypt an attachment. + * @param {ArrayBuffer} ciphertextBuffer The encrypted attachment data buffer. + * @param {Object} info The information needed to decrypt the attachment. + * @param {Object} info.key AES-CTR JWK key object. + * @param {string} info.iv Base64 encoded 16 byte AES-CTR IV. + * @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext. + * @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted. + */ + function decryptAttachment(ciphertextBuffer, info) { + + if (info === undefined || info.key === undefined || info.iv === undefined + || info.hashes === undefined || info.hashes.sha256 === undefined) { + throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key"); + } + + let cryptoKey; // The AES key object. + let ivArray = decodeBase64(info.iv); + let expectedSha256base64 = info.hashes.sha256; + // Load the AES from the "key" key of the info object. + return crypto.subtle.importKey( + "jwk", info.key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"] + ).then(function (importKeyResult) { + cryptoKey = importKeyResult; + // Check the sha256 hash + return crypto.subtle.digest("SHA-256", ciphertextBuffer); + }).then(function (digestResult) { + if (encodeBase64(new Uint8Array(digestResult)) !== expectedSha256base64) { + throw new Error("Mismatched SHA-256 digest (expected: " + encodeBase64(new Uint8Array(digestResult)) + ") got (" + expectedSha256base64 + ")"); + } + let counterLength; + if (info.v.toLowerCase() === "v1" || info.v.toLowerCase() === "v2") { + // Version 1 and 2 use a 64 bit counter. + counterLength = 64; + } else { + // Version 0 uses a 128 bit counter. + counterLength = 128; + } + return crypto.subtle.decrypt( + {name: "AES-CTR", counter: ivArray, length: counterLength}, cryptoKey, ciphertextBuffer + ); + }); + } + + /** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64 without padding. + */ + function encodeBase64(uint8Array) { + // Misinterpt the Uint8Array as Latin-1. + // window.btoa expects a unicode string with codepoints in the range 0-255. + // var latin1String = String.fromCharCode.apply(null, uint8Array); + // Use the builtin base64 encoder. + var paddedBase64 = btoa(uint8Array); + // Calculate the unpadded length. + var inputLength = uint8Array.length; + var outputLength = 4 * Math.floor((inputLength + 2) / 3) + (inputLength + 2) % 3 - 2; + // Return the unpadded base64. + return paddedBase64.slice(0, outputLength); + } + + /** + * Decode a base64 string to a typed array of uint8. + * This will decode unpadded base64, but will also accept base64 with padding. + * @param {string} base64 The unpadded base64 to decode. + * @return {Uint8Array} The decoded data. + */ + function decodeBase64(base64) { + // Pad the base64 up to the next multiple of 4. + var paddedBase64 = base64 + "===".slice(0, (4 - base64.length % 4) % 4); + // Decode the base64 as a misinterpreted Latin-1 string. + // window.atob returns a unicode string with codepoints in the range 0-255. + var latin1String = atob(paddedBase64); + // Encode the string as a Uint8Array as Latin-1. + var uint8Array = new Uint8Array(latin1String.length); + for (var i = 0; i < latin1String.length; i++) { + uint8Array[i] = latin1String.charCodeAt(i); + } + return uint8Array; + } + + function autoDetectMatrixMessageType(fileType) { + switch(fileType ? fileType.mime.split('/')[0].toLowerCase() : undefined) { + case 'video': return 'm.video'; + case 'image': return 'm.image'; + case 'audio': return 'm.audio'; + default: return 'm.file'; + } + } + + // ffprobe method for getting metadata from a file wrapped in a promise + function _ffprobe(filepath){ + return new Promise((resolve,reject) => { + ffmpeg.ffprobe(filepath, function(err, metadata) { + if(err) { + return reject(new Error(err)); + } + + resolve(metadata); + }); + }); + } +} \ No newline at end of file From 000c28e3b890ba5ab072bc7f2b98967226f5ccb5 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 25 Nov 2023 05:38:09 -0700 Subject: [PATCH 18/29] - Reduce margin between checkboxes for matrix-receive node - matrix-send-message node can now be used to reply in a thread (closes #104) - matrix-receive node now returns msg.mentions for easier access to who was mentioned in message - matrix-receive node now returns boolean msg.isThread based on whether the message is a thread reply or not --- src/matrix-receive.html | 18 ++++++++-------- src/matrix-receive.js | 1 + src/matrix-send-message.html | 22 +++++++++++++++++-- src/matrix-send-message.js | 42 +++++++++++++++++++++++++++++++++++- src/matrix-server-config.js | 24 ++++++++++++--------- 5 files changed, 85 insertions(+), 22 deletions(-) diff --git a/src/matrix-receive.html b/src/matrix-receive.html index a9c9d13..a605039 100644 --- a/src/matrix-receive.html +++ b/src/matrix-receive.html @@ -45,7 +45,7 @@
Timeline event filters
-
+
m.text
-
+
m.emote
-
+
m.sticker
-
+
m.reaction
-
+
m.file
-
+
m.audio
-
+
m.image
-
+
m.video
-
+
@@ -72,6 +85,11 @@ It's recommended to use m.notice for bots because the message will render in a lighter text (at least in Element client) for users to distinguish bot and real user messages.
+
+ + +
+
- +
- +
diff --git a/src/matrix-user-settings.html b/src/matrix-user-settings.html index fb4ea04..0ec217b 100644 --- a/src/matrix-user-settings.html +++ b/src/matrix-user-settings.html @@ -105,7 +105,6 @@ name: { value: null }, server: { type: "matrix-server-config" }, roomId: { value: null }, - reason: { value: null }, rules: { value: defaultRules, validate: function(rules, opt) { From a3e1381d5313c9ccb36202f7fb52c8576f323406 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Fri, 15 Dec 2023 03:56:32 -0700 Subject: [PATCH 22/29] - Update room state events node so topic property can be dynamically configured --- src/matrix-room-state-events.html | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/matrix-room-state-events.html b/src/matrix-room-state-events.html index 35e558f..6eaccb5 100644 --- a/src/matrix-room-state-events.html +++ b/src/matrix-room-state-events.html @@ -8,9 +8,8 @@
- - - + +
@@ -18,18 +17,6 @@
    - - + + + + \ No newline at end of file diff --git a/src/matrix-mark-read.js b/src/matrix-mark-read.js new file mode 100644 index 0000000..f9559ca --- /dev/null +++ b/src/matrix-mark-read.js @@ -0,0 +1,73 @@ +const {TimelineWindow, RelationType, Filter} = require("matrix-js-sdk"); +const crypto = require('crypto'); +module.exports = function(RED) { + function MatrixReceiveMessage(n) { + RED.nodes.createNode(this, n); + + let node = this; + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomType = n.roomType; + this.roomValue = n.roomValue; + this.eventIdType = n.eventIdType; + this.eventIdValue = n.eventIdValue; + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + if (!node.server) { + node.error("No configuration node", {}); + return; + } + node.server.register(node); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on("input", async function (msg) { + if (! node.server || ! node.server.matrixClient) { + node.error("No matrix server selected", msg); + return; + } + + function getToValue(msg, type, property) { + let value = property; + if (type === "msg") { + value = RED.util.getMessageProperty(msg, property); + } else if ((type === 'flow') || (type === 'global')) { + try { + value = RED.util.evaluateNodeProperty(property, type, node, msg); + } catch(e2) { + throw new Error("Invalid value evaluation"); + } + } else if(type === "bool") { + value = (property === 'true'); + } else if(type === "num") { + value = Number(property); + } + return value; + } + + try { + let roomId = getToValue(msg, node.roomType, node.roomValue), + eventId = getToValue(msg, node.eventIdType, node.eventIdValue); + + msg.payload = await node.server.matrixClient.setRoomReadMarkers(roomId, eventId); + node.send([msg, null]); + } catch(e) { + msg.error = `Room pagination error: ${e}`; + node.error(msg.error, msg); + node.send([null, msg]); + } + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-mark-read", MatrixReceiveMessage); +} \ No newline at end of file diff --git a/src/matrix-paginate-room.html b/src/matrix-paginate-room.html new file mode 100644 index 0000000..f5fdbe5 --- /dev/null +++ b/src/matrix-paginate-room.html @@ -0,0 +1,284 @@ + + + + + \ No newline at end of file diff --git a/src/matrix-paginate-room.js b/src/matrix-paginate-room.js new file mode 100644 index 0000000..8798e6c --- /dev/null +++ b/src/matrix-paginate-room.js @@ -0,0 +1,155 @@ +const {TimelineWindow, RelationType, Filter} = require("matrix-js-sdk"); +const crypto = require('crypto'); +module.exports = function(RED) { + function MatrixReceiveMessage(n) { + RED.nodes.createNode(this, n); + + let node = this; + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomType = n.roomType; + this.roomValue = n.roomValue; + this.paginateBackwardsType = n.paginateBackwardsType; + this.paginateBackwardsValue = n.paginateBackwardsValue; + this.paginateKeyType = n.paginateKeyType; + this.paginateKeyValue = n.paginateKeyValue; + this.pageSizeType = n.pageSizeType; + this.pageSizeValue = n.pageSizeValue; + this.timelineWindows = new Map(); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + if (!node.server) { + node.error("No configuration node", {}); + return; + } + node.server.register(node); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on("input", async function (msg) { + if (! node.server || ! node.server.matrixClient) { + node.error("No matrix server selected", msg); + return; + } + + function getToValue(msg, type, property) { + let value = property; + if (type === "msg") { + value = RED.util.getMessageProperty(msg, property); + } else if ((type === 'flow') || (type === 'global')) { + try { + value = RED.util.evaluateNodeProperty(property, type, node, msg); + } catch(e2) { + throw new Error("Invalid value evaluation"); + } + } else if(type === "bool") { + value = (property === 'true'); + } else if(type === "num") { + value = Number(property); + } + return value; + } + + function setToValue(value, type, property) { + if(type === 'global' || type === 'flow') { + var contextKey = RED.util.parseContextStore(property); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true) + } + var target = node.context()[type]; + var callback = err => { + if (err) { + node.error(err, msg); + getterErrors[rule.p] = err.message; + } + } + target.set(contextKey.key, value, contextKey.store, callback); + } else if(type === 'msg') { + if (!RED.util.setMessageProperty(msg, property, value)) { + node.warn(RED._("change.errors.no-override",{property:property})); + } + } + } + + try { + let roomId = getToValue(msg, node.roomType, node.roomValue), + paginateBackwards = getToValue(msg, node.paginateBackwardsType, node.paginateBackwardsValue), + pageSize = getToValue(msg, node.pageSizeType, node.pageSizeValue), + pageKey = getToValue(msg, node.paginateKeyType, node.paginateKeyValue); + + let room = node.server.matrixClient.getRoom(roomId); + + if(!room) { + throw new Error(`Room ${roomId} does not exist`); + } + if(pageSize > node.server.initialSyncLimit) { + throw new Error(`Page size=${pageSize} cannot exceed initialSyncLimit=${node.server.initialSyncLimit}`); + } + if(!pageKey) { + pageKey = crypto.randomUUID(); + setToValue(pageKey, node.paginateKeyType, node.paginateKeyValue); + } + let timelineWindow = node.timelineWindows.get(pageKey), + moreMessages = true; + if(!timelineWindow) { + let timelineSet = room.getUnfilteredTimelineSet(); + node.debug(JSON.stringify(timelineSet.getFilter())); + // MatrixClient's option initialSyncLimit gets set to the filter we are using + // so override that value with our pageSize + timelineWindow = new TimelineWindow(node.server.matrixClient, timelineSet); + await timelineWindow.load(msg.eventId || null, pageSize); + node.timelineWindows.set(pageKey, timelineWindow); + } else { + moreMessages = await timelineWindow.paginate(paginateBackwards ? 'b' : 'f', pageSize); // b for backwards f for forwards + if(moreMessages) { + await timelineWindow.unpaginate(pageSize, !paginateBackwards); + } + } + + // MatrixEvent objects are massive so this throws an encode error for the string being too long + // since msg objects convert to JSON + // msg.payload = moreMessages ? timelineWindow.getEvents() : false; + + msg.payload = false; + msg.start = timelineWindow.getTimelineIndex('b')?.index; + msg.end = timelineWindow.getTimelineIndex('f')?.index; + if(moreMessages) { + msg.payload = timelineWindow.getEvents().map(function(event) { + return { + encrypted : event.isEncrypted(), + redacted : event.isRedacted(), + content : event.getContent(), + type : (event.getContent()['msgtype'] || event.getType()) || null, + payload : (event.getContent()['body'] || event.getContent()) || null, + isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread, + mentions : event.getContent()["m.mentions"] || null, + userId : event.getSender(), + // user : node.matrixClient.getUser(event.getSender()), + topic : event.getRoomId(), + eventId : event.getId(), + event : event, + }; + }); + } + node.send([msg, null]); + } catch(e) { + msg.error = `Room pagination error: ${e}`; + node.error(msg.error, msg); + node.send([null, msg]); + } + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-paginate-room", MatrixReceiveMessage); +} \ No newline at end of file diff --git a/src/matrix-receive.html b/src/matrix-receive.html index a605039..285ced2 100644 --- a/src/matrix-receive.html +++ b/src/matrix-receive.html @@ -10,6 +10,7 @@ name: { value: null }, server: { type: "matrix-server-config" }, roomId: {"value": null}, + acceptOwnEvents: {"value": false}, acceptText: {"value": true}, acceptEmotes: {"value": true}, acceptStickers: {"value": true}, @@ -45,6 +46,16 @@
    Timeline event filters
    +
    + + +
    event.getDate()) { + node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message before init"); return; // skip events that occurred before our client initialized } @@ -249,7 +251,7 @@ module.exports = function(RED) { } }); - node.log("Received" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getSender() + " :: " + msg.content.body + (toStartOfTimeline ? ' [PAGINATED]' : '')); + node.log(`Received ${msg.encrypted ? 'encrypted ' : ''}timeline event [${msg.type}]: (${room.name}) ${event.getSender()} :: ${msg.content.body} ${toStartOfTimeline ? ' [PAGINATED]' : ''}`); node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg); }); @@ -402,7 +404,7 @@ module.exports = function(RED) { } node.log("Connecting to Matrix server..."); await node.matrixClient.startClient({ - initialSyncLimit: 8 + initialSyncLimit: node.initialSyncLimit }); } catch(error) { node.error(error, {}); @@ -476,10 +478,15 @@ module.exports = function(RED) { const matrixClient = sdk.createClient({ baseUrl: baseUrl, deviceId: deviceId, + timelineSupport: true, localTimeoutMs: '30000', request }); + new TimelineWindow() + + matrixClient.timelineSupport = true; + matrixClient.login( 'm.login.password', { user: userId, diff --git a/src/matrix-upload-file.html b/src/matrix-upload-file.html index e0aa396..df6f336 100644 --- a/src/matrix-upload-file.html +++ b/src/matrix-upload-file.html @@ -105,7 +105,7 @@