Compare commits

..

2 Commits

Author SHA1 Message Date
skylord123 db14bd96a7 - Upgraded to rust crypto but old libolm data will not migrate 2024-09-20 17:57:15 -06:00
skylord123 512b7320d5 - Upgrade matrix-js-sdk to ^34.5.0
- Change how matrix-js-sdk is loaded with it now being an ESM module
2024-09-20 09:24:15 -06:00
12 changed files with 5408 additions and 16529 deletions
+2 -6
View File
@@ -5,8 +5,6 @@
Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech) Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B51BM7C)
### Features ### Features
Supported functionality in this package includes: Supported functionality in this package includes:
@@ -61,9 +59,7 @@ Interested in helping? Contributions to finalize E2EE support are welcome!
This module includes a node to register users using the Synapse secret registration endpoint. It returns both an `access_token` and a `device_id`, perfect for setting up the bot. This module includes a node to register users using the Synapse secret registration endpoint. It returns both an `access_token` and a `device_id`, perfect for setting up the bot.
[Guide on registering a user via the web browser](https://skylar.tech/matrix-chat-bot-module-for-node-red/) [See how to register a user here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme).
[Guide on registering using shared secret registration](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) (for server owners)
### Other Packages ### Other Packages
@@ -73,4 +69,4 @@ This module includes a node to register users using the Synapse secret registrat
We welcome all contributions! Please submit a pull request if you add a feature so the whole community can benefit. We welcome all contributions! Please submit a pull request if you add a feature so the whole community can benefit.
**Sharing is caring!** **Sharing is caring!**
+4990 -16048
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,15 +1,16 @@
{ {
"name": "node-red-contrib-matrix-chat", "name": "node-red-contrib-matrix-chat",
"version": "0.9.2", "version": "0.8.0",
"description": "Matrix chat server client for Node-RED", "description": "Matrix chat server client for Node-RED",
"dependencies": { "dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.2.0-beta.1",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"got": "^12.0.2", "got": "^12.0.2",
"image-size": "^1.0.2", "image-size": "^1.0.2",
"isomorphic-webcrypto": "^2.3.8", "isomorphic-webcrypto": "^2.3.8",
"matrix-js-sdk": "34.11.1", "matrix-js-sdk": "^34.5.0",
"mime": "^3.0.0", "mime": "^3.0.0",
"node-fetch": "^3.3.0", "node-fetch": "^3.3.0",
"node-localstorage": "^2.2.1", "node-localstorage": "^2.2.1",
+8 -11
View File
@@ -1,5 +1,3 @@
const sdkPromise = import("matrix-js-sdk");
module.exports = function(RED) { module.exports = function(RED) {
function MatrixFetchRelations(n) { function MatrixFetchRelations(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
@@ -43,23 +41,22 @@ module.exports = function(RED) {
}); });
node.on("input", async function(msg) { node.on("input", async function(msg) {
const {Direction} = await import("matrix-js-sdk");
if (!node.server || !node.server.matrixClient) { if (!node.server || !node.server.matrixClient) {
node.error("No matrix server selected", msg); node.error("No matrix server selected", msg);
return; return;
} }
try { try {
const sdk = await sdkPromise;
const Direction = sdk.Direction;
function evaluateNodePropertySafe(value, type, node, msg) { function evaluateNodePropertySafe(value, type, node, msg) {
try { try {
return RED.util.evaluateNodeProperty(value, type, node, msg); return RED.util.evaluateNodeProperty(value, type, node, msg);
} catch (e) { } catch (e) {
if (e instanceof TypeError) { if (e instanceof TypeError) {
return undefined; return undefined; // Handle TypeError and return undefined
} }
throw e; throw e; // Re-throw other errors to prevent masking issues
} }
} }
@@ -74,16 +71,16 @@ module.exports = function(RED) {
to = evaluateNodePropertySafe(node.toValue, node.toType, node, msg); to = evaluateNodePropertySafe(node.toValue, node.toType, node, msg);
let opts = { dir: direction }; let opts = { dir: direction };
if (limit) { if(limit) {
opts.limit = limit; opts.limit = limit;
} }
if (recurse === true || recurse === false) { if(recurse === true || recurse === false) {
opts.recurse = recurse; opts.recurse = recurse;
} }
if (from) { if(from) {
opts.from = from; opts.from = from;
} }
if (to) { if(to) {
opts.to = to; opts.to = to;
} }
+18 -10
View File
@@ -19,18 +19,14 @@
}, },
oneditprepare: function() { oneditprepare: function() {
$("#node-input-roomId").typedInput({ $("#node-input-roomId").typedInput({
types: ['msg','flow','global','str'], type: this.roomIdType,
typeField: "#node-input-roomId" types:['msg','flow','global','str'],
}); }).typedInput('value', this.roomIdValue);
$("#node-input-roomId").typedInput("type", this.roomIdType || "msg");
$("#node-input-roomId").typedInput("value", this.roomIdValue || "topic");
$("#node-input-eventId").typedInput({ $("#node-input-eventId").typedInput({
types: ['msg','flow','global','str'], type: this.eventIdType,
typeField: "#node-input-eventId" types:['msg','flow','global','str'],
}); }).typedInput('value', this.eventIdValue);
$("#node-input-eventId").typedInput("type", this.eventIdType || "msg");
$("#node-input-eventId").typedInput("value", this.eventIdValue || "eventId");
}, },
oneditsave: function() { oneditsave: function() {
this.roomIdType = $("#node-input-roomId").typedInput('type'); this.roomIdType = $("#node-input-roomId").typedInput('type');
@@ -62,6 +58,18 @@
<label for="node-input-eventId"><i class="fa fa-file"></i> Event ID</label> <label for="node-input-eventId"><i class="fa fa-file"></i> Event ID</label>
<input type="text" id="node-input-eventId"> <input type="text" id="node-input-eventId">
</div> </div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-get-event"> <script type="text/html" data-help-name="matrix-get-event">
+11 -12
View File
@@ -1,8 +1,7 @@
const crypto = require('crypto');
module.exports = function(RED) { module.exports = function(RED) {
function MatrixMarkRead(n) { function MatrixReceiveMessage(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
let node = this; let node = this;
this.name = n.name; this.name = n.name;
this.server = RED.nodes.getNode(n.server); this.server = RED.nodes.getNode(n.server);
@@ -28,7 +27,7 @@ module.exports = function(RED) {
}); });
node.on("input", async function (msg) { node.on("input", async function (msg) {
if (!node.server || !node.server.matrixClient) { if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected", msg); node.error("No matrix server selected", msg);
return; return;
} }
@@ -37,15 +36,15 @@ module.exports = function(RED) {
let value = property; let value = property;
if (type === "msg") { if (type === "msg") {
value = RED.util.getMessageProperty(msg, property); value = RED.util.getMessageProperty(msg, property);
} else if (type === 'flow' || type === 'global') { } else if ((type === 'flow') || (type === 'global')) {
try { try {
value = RED.util.evaluateNodeProperty(property, type, node, msg); value = RED.util.evaluateNodeProperty(property, type, node, msg);
} catch (e2) { } catch(e2) {
throw new Error("Invalid value evaluation"); throw new Error("Invalid value evaluation");
} }
} else if (type === "bool") { } else if(type === "bool") {
value = (property === 'true'); value = (property === 'true');
} else if (type === "num") { } else if(type === "num") {
value = Number(property); value = Number(property);
} }
return value; return value;
@@ -65,9 +64,9 @@ module.exports = function(RED) {
throw new Error(`Event ${eventId} not found in room ${roomId}.`); throw new Error(`Event ${eventId} not found in room ${roomId}.`);
} }
await node.server.matrixClient.sendReceipt(event, "m.read"); await node.server.matrixClient.sendReceipt(event, "m.read")
node.send([msg, null]); node.send([msg, null]);
} catch (e) { } catch(e) {
msg.error = `Room pagination error: ${e}`; msg.error = `Room pagination error: ${e}`;
node.error(msg.error, msg); node.error(msg.error, msg);
node.send([null, msg]); node.send([null, msg]);
@@ -78,5 +77,5 @@ module.exports = function(RED) {
node.server.deregister(node); node.server.deregister(node);
}); });
} }
RED.nodes.registerType("matrix-mark-read", MatrixMarkRead); RED.nodes.registerType("matrix-mark-read", MatrixReceiveMessage);
} }
+34 -33
View File
@@ -1,8 +1,5 @@
const sdkPromise = import("matrix-js-sdk");
const crypto = require('crypto');
module.exports = function(RED) { module.exports = function(RED) {
function MatrixPaginateRoom(n) { function MatrixReceiveMessage(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
let node = this; let node = this;
@@ -35,7 +32,10 @@ module.exports = function(RED) {
}); });
node.on("input", async function (msg) { node.on("input", async function (msg) {
if (!node.server || !node.server.matrixClient) { const {TimelineWindow, RelationType, Filter} = await import("matrix-js-sdk");
const crypto = await import('crypto');
if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected", msg); node.error("No matrix server selected", msg);
return; return;
} }
@@ -44,47 +44,43 @@ module.exports = function(RED) {
let value = property; let value = property;
if (type === "msg") { if (type === "msg") {
value = RED.util.getMessageProperty(msg, property); value = RED.util.getMessageProperty(msg, property);
} else if (type === 'flow' || type === 'global') { } else if ((type === 'flow') || (type === 'global')) {
try { try {
value = RED.util.evaluateNodeProperty(property, type, node, msg); value = RED.util.evaluateNodeProperty(property, type, node, msg);
} catch(e2) { } catch(e2) {
throw new Error("Invalid value evaluation"); throw new Error("Invalid value evaluation");
} }
} else if (type === "bool") { } else if(type === "bool") {
value = (property === 'true'); value = (property === 'true');
} else if (type === "num") { } else if(type === "num") {
value = Number(property); value = Number(property);
} }
return value; return value;
} }
function setToValue(value, type, property) { function setToValue(value, type, property) {
if (type === 'global' || type === 'flow') { if(type === 'global' || type === 'flow') {
var contextKey = RED.util.parseContextStore(property); var contextKey = RED.util.parseContextStore(property);
if (/\[msg/.test(contextKey.key)) { if (/\[msg/.test(contextKey.key)) {
// The key has a nested msg. reference that must be evaluated first // The key has a nest msg. reference to evaluate first
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true); contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true)
} }
var target = node.context()[type]; var target = node.context()[type];
target.set(contextKey.key, value, contextKey.store, err => { var callback = err => {
if (err) { if (err) {
node.error(err, msg); node.error(err, msg);
getterErrors[rule.p] = err.message;
} }
}); }
} else if (type === 'msg') { target.set(contextKey.key, value, contextKey.store, callback);
} else if(type === 'msg') {
if (!RED.util.setMessageProperty(msg, property, value)) { if (!RED.util.setMessageProperty(msg, property, value)) {
node.warn(RED._("change.errors.no-override", { property: property })); node.warn(RED._("change.errors.no-override",{property:property}));
} }
} }
} }
try { try {
// Dynamically load the SDK
const sdk = await sdkPromise;
const TimelineWindow = sdk.TimelineWindow;
const RelationType = sdk.RelationType;
// (Filter was imported originally but is not used, so we omit it.)
let roomId = getToValue(msg, node.roomType, node.roomValue), let roomId = getToValue(msg, node.roomType, node.roomValue),
paginateBackwards = getToValue(msg, node.paginateBackwardsType, node.paginateBackwardsValue), paginateBackwards = getToValue(msg, node.paginateBackwardsType, node.paginateBackwardsValue),
pageSize = getToValue(msg, node.pageSizeType, node.pageSizeValue), pageSize = getToValue(msg, node.pageSizeType, node.pageSizeValue),
@@ -92,37 +88,42 @@ module.exports = function(RED) {
let room = node.server.matrixClient.getRoom(roomId); let room = node.server.matrixClient.getRoom(roomId);
if (!room) { if(!room) {
throw new Error(`Room ${roomId} does not exist`); throw new Error(`Room ${roomId} does not exist`);
} }
if (pageSize > node.server.initialSyncLimit) { if(pageSize > node.server.initialSyncLimit) {
throw new Error(`Page size=${pageSize} cannot exceed initialSyncLimit=${node.server.initialSyncLimit}`); throw new Error(`Page size=${pageSize} cannot exceed initialSyncLimit=${node.server.initialSyncLimit}`);
} }
if (!pageKey) { if(!pageKey) {
pageKey = crypto.randomUUID(); pageKey = crypto.randomUUID();
setToValue(pageKey, node.paginateKeyType, node.paginateKeyValue); setToValue(pageKey, node.paginateKeyType, node.paginateKeyValue);
} }
let timelineWindow = node.timelineWindows.get(pageKey), let timelineWindow = node.timelineWindows.get(pageKey),
moreMessages = true; moreMessages = true;
if (!timelineWindow) { if(!timelineWindow) {
let timelineSet = room.getUnfilteredTimelineSet(); let timelineSet = room.getUnfilteredTimelineSet();
// MatrixClient's option initialSyncLimit gets set to the filter we are using, // node.debug(JSON.stringify(timelineSet.getFilter()));
// so override that value with our pageSize.
// 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); timelineWindow = new TimelineWindow(node.server.matrixClient, timelineSet);
await timelineWindow.load(msg.eventId || null, pageSize); await timelineWindow.load(msg.eventId || null, pageSize);
node.timelineWindows.set(pageKey, timelineWindow); node.timelineWindows.set(pageKey, timelineWindow);
} else { } else {
moreMessages = await timelineWindow.paginate(paginateBackwards ? 'b' : 'f', pageSize); // 'b' for backwards, 'f' for forwards moreMessages = await timelineWindow.paginate(paginateBackwards ? 'b' : 'f', pageSize); // b for backwards f for forwards
if (moreMessages) { if(moreMessages) {
await timelineWindow.unpaginate(pageSize, !paginateBackwards); await timelineWindow.unpaginate(pageSize, !paginateBackwards);
} }
} }
// To avoid errors converting massive MatrixEvent objects to JSON, we omit them. // 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.payload = false;
msg.start = timelineWindow.getTimelineIndex('b')?.index; msg.start = timelineWindow.getTimelineIndex('b')?.index;
msg.end = timelineWindow.getTimelineIndex('f')?.index; msg.end = timelineWindow.getTimelineIndex('f')?.index;
if (moreMessages) { if(moreMessages) {
msg.payload = timelineWindow.getEvents().map(function(event) { msg.payload = timelineWindow.getEvents().map(function(event) {
return { return {
encrypted : event.isEncrypted(), encrypted : event.isEncrypted(),
@@ -152,5 +153,5 @@ module.exports = function(RED) {
node.server.deregister(node); node.server.deregister(node);
}); });
} }
RED.nodes.registerType("matrix-paginate-room", MatrixPaginateRoom); RED.nodes.registerType("matrix-paginate-room", MatrixReceiveMessage);
} }
-11
View File
@@ -13,7 +13,6 @@
acceptOwnEvents: {"value": false}, acceptOwnEvents: {"value": false},
acceptText: {"value": true}, acceptText: {"value": true},
acceptEmotes: {"value": true}, acceptEmotes: {"value": true},
acceptNotices: {"value": true},
acceptStickers: {"value": true}, acceptStickers: {"value": true},
acceptReactions: {"value": true}, acceptReactions: {"value": true},
acceptFiles: {"value": true}, acceptFiles: {"value": true},
@@ -67,16 +66,6 @@
Accept text <code style="text-transform: none;">m.text</code> Accept text <code style="text-transform: none;">m.text</code>
</label> </label>
</div> </div>
<div class="form-row" style="margin-bottom:0;">
<input
type="checkbox"
id="node-input-acceptNotices"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-input-acceptNotices" style="width: auto">
Accept notices <code style="text-transform: none;">m.notice</code>
</label>
</div>
<div class="form-row" style="margin-bottom:0;"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
-5
View File
@@ -9,7 +9,6 @@ module.exports = function(RED) {
this.acceptOwnEvents = n.acceptOwnEvents; this.acceptOwnEvents = n.acceptOwnEvents;
this.acceptText = n.acceptText; this.acceptText = n.acceptText;
this.acceptEmotes = n.acceptEmotes; this.acceptEmotes = n.acceptEmotes;
this.acceptNotices = n.acceptNotices;
this.acceptStickers = n.acceptStickers; this.acceptStickers = n.acceptStickers;
this.acceptReactions = n.acceptReactions; this.acceptReactions = n.acceptReactions;
this.acceptFiles = n.acceptFiles; this.acceptFiles = n.acceptFiles;
@@ -69,10 +68,6 @@ module.exports = function(RED) {
if (!node.acceptEmotes) return; if (!node.acceptEmotes) return;
break; break;
case 'm.notice':
if (!node.acceptNotices) return;
break;
case 'm.text': case 'm.text':
if (!node.acceptText) return; if (!node.acceptText) return;
break; break;
+20 -27
View File
@@ -1,8 +1,7 @@
const sdkPromise = import("matrix-js-sdk");
module.exports = function(RED) { module.exports = function(RED) {
function MatrixSendImage(n) { function MatrixSendImage(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
var node = this; var node = this;
this.name = n.name; this.name = n.name;
@@ -67,17 +66,13 @@ module.exports = function(RED) {
node.status({ fill: "green", shape: "ring", text: "connected" }); node.status({ fill: "green", shape: "ring", text: "connected" });
}); });
// Make the input handler async so we can await the dynamic import.
node.on("input", async function (msg) { node.on("input", async function (msg) {
// Await the SDK import and get the RelationType constant. const {RelationType} = await import("matrix-js-sdk");
const sdk = await sdkPromise;
const RelationType = sdk.RelationType;
function getToValue(msg, type, property) { function getToValue(msg, type, property) {
let value = property; let value = property;
if (type === "msg") { if (type === "msg") {
value = RED.util.getMessageProperty(msg, property); value = RED.util.getMessageProperty(msg, property);
} else if (type === 'flow' || type === 'global') { } else if ((type === 'flow') || (type === 'global')) {
try { try {
value = RED.util.evaluateNodeProperty(property, type, node, msg); value = RED.util.evaluateNodeProperty(property, type, node, msg);
} catch(e2) { } catch(e2) {
@@ -119,19 +114,19 @@ module.exports = function(RED) {
} }
let content = null; let content = null;
if (typeof payload === 'object') { if(typeof payload === 'object') {
content = payload; content = payload;
} else { } else {
if (msgType === 'msg.type') { if(msgType === 'msg.type') {
if (!msg.type) { if(!msg.type) {
node.error("msg.type is set to be passed in via msg.type but was not defined", msg); node.error("msg.type type is set to be passed in via msg.type but was not defined", msg);
return; return;
} }
msgType = msg.type; msgType = msg.type;
} }
if (msgFormat === 'msg.format') { if(msgFormat === 'msg.format') {
if (!Object.hasOwn(msg, 'format')) { if(!msg.format) {
node.error("Message format is set to be passed in via msg.format but was not defined", msg); node.error("Message format is set to be passed in via msg.format but was not defined", msg);
return; return;
} }
@@ -143,7 +138,7 @@ module.exports = function(RED) {
body: payload.toString() body: payload.toString()
}; };
if (msgFormat === 'html') { if(msgFormat === 'html') {
content.format = "org.matrix.custom.html"; content.format = "org.matrix.custom.html";
content.formatted_body = content.formatted_body =
(typeof msg.formatted_payload !== 'undefined' && msg.formatted_payload) (typeof msg.formatted_payload !== 'undefined' && msg.formatted_payload)
@@ -151,15 +146,15 @@ module.exports = function(RED) {
: payload.toString(); : payload.toString();
} }
if ((node.replaceMessage || msg.replace) && msg.eventId) { if((node.replaceMessage || msg.replace) && msg.eventId) {
content['m.new_content'] = { content['m.new_content'] = {
msgtype: content.msgtype, msgtype: content.msgtype,
body: content.body body: content.body
}; };
if ('format' in content) { if('format' in content) {
content['m.new_content']['format'] = content['format']; content['m.new_content']['format'] = content['format'];
} }
if ('formatted_body' in content) { if('formatted_body' in content) {
content['m.new_content']['formatted_body'] = content['formatted_body']; content['m.new_content']['formatted_body'] = content['formatted_body'];
} }
@@ -168,19 +163,17 @@ module.exports = function(RED) {
event_id: msg.eventId event_id: msg.eventId
}; };
content['body'] = ' * ' + content['body']; content['body'] = ' * ' + content['body'];
} else if (threadReply) { } else if(threadReply) {
// If incoming message is a reply to a thread we fetch the thread parent from m.relates_to, // if incoming message is a reply to a thread we fetch the thread parent from the m.relates_to property
// otherwise fallback to msg.eventId. // otherwise fallback to msg.eventId
let threadParent = (msg?.content?.['m.relates_to']?.rel_type === RelationType.Thread let threadParent = (msg?.content?.['m.relates_to']?.rel_type === RelationType.Thread ? msg?.content?.['m.relates_to']?.event_id : null) || msg.eventId;
? msg?.content?.['m.relates_to']?.event_id if(threadParent) {
: null) || msg.eventId;
if (threadParent) {
content["m.relates_to"] = { content["m.relates_to"] = {
"rel_type": RelationType.Thread, "rel_type": RelationType.Thread,
"event_id": threadParent, "event_id": threadParent,
"is_falling_back": true, "is_falling_back": true,
}; };
if (msg.eventId !== threadParent) { if(msg.eventId !== threadParent) {
content["m.relates_to"]["m.in_reply_to"] = { content["m.relates_to"]["m.in_reply_to"] = {
"event_id": msg.eventId "event_id": msg.eventId
}; };
@@ -208,4 +201,4 @@ module.exports = function(RED) {
}); });
} }
RED.nodes.registerType("matrix-send-message", MatrixSendImage); RED.nodes.registerType("matrix-send-message", MatrixSendImage);
}; }
+1 -16
View File
@@ -36,8 +36,7 @@
name: { value: null }, name: { value: null },
autoAcceptRoomInvites: { value: true }, autoAcceptRoomInvites: { value: true },
enableE2ee: { type: "checkbox", value: true }, enableE2ee: { type: "checkbox", value: true },
global: { type: "checkbox", value: true }, global: { type: "checkbox", value: true }
allowUnknownDevices: { type: "checkbox", value: false }
}, },
icon: "matrix.png", icon: "matrix.png",
label: function() { label: function() {
@@ -131,20 +130,6 @@
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code> <code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
</div> </div>
</div> </div>
<div class="form-row">
<input
type="checkbox"
id="node-config-input-allowUnknownDevices"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-config-input-allowUnknownDevices" style="width: auto">
Allow unverified devices in rooms
</label>
<div class="form-tips" style="margin-bottom: 12px;">
Allow sending messages to a room with unknown devices which have not been verified.
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
$("#matrix-login-btn").on("click", function() { $("#matrix-login-btn").on("click", function() {
function prettyPrintJson(json) { function prettyPrintJson(json) {
+321 -348
View File
@@ -1,52 +1,36 @@
global.Olm = require('olm'); global.Olm = require('olm');
const fs = require("fs-extra"); const fs = require("fs-extra");
let RelationType, sdk, LocalStorageCryptoStore, RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, MemoryStore;
(async () => {
const mod = await import("matrix-js-sdk");
RelationType = mod.RelationType;
// matrix-js-sdk doesn't export a default the top-level export is the same object:
sdk = mod;
RoomEvent = mod.RoomEvent;
RoomMemberEvent = mod.RoomMemberEvent;
HttpApiEvent = mod.HttpApiEvent;
ClientEvent = mod.ClientEvent;
MemoryStore = mod.MemoryStore;
// For LocalStorageCryptoStore, specify the file extension for Node 20+:
const cmod = await import("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store.js");
LocalStorageCryptoStore = cmod.LocalStorageCryptoStore;
})();
const { resolve } = require('path'); const { resolve } = require('path');
const { LocalStorage } = require('node-localstorage'); const { LocalStorage } = require('node-localstorage');
globalThis.crypto = require('crypto');
require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it
if (!globalThis.fetch) { if (!globalThis.fetch) {
// polyfill fetch if we don't have it // polyfill fetch if we don't have it
if (!globalThis.fetch) { if (!globalThis.fetch) {
import('node-fetch').then(({ default: fetch, Headers, Request, Response }) => { import('node-fetch').then(({ default: fetch, Headers, Request, Response }) => {
Object.assign(globalThis, { fetch, Headers, Request, Response }); Object.assign(globalThis, { fetch, Headers, Request, Response })
}); })
} }
} }
module.exports = function(RED) { module.exports = function(RED) {
// Prepare dynamic imports
const sdkPromise = import("matrix-js-sdk");
const LocalStorageCryptoStorePromise = import('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store.js');
const loggerPromise = import('matrix-js-sdk/lib/logger.js');
// disable logging if set to "off" // disable logging if set to "off"
let loggingSettings = RED.settings.get('logging'); // let loggingSettings = RED.settings.get('logging');
if ( // if(
typeof loggingSettings.console !== 'undefined' && // typeof loggingSettings.console !== 'undefined' &&
typeof loggingSettings.console.level !== 'undefined' && // typeof loggingSettings.console.level !== 'undefined' &&
['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0 // ['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0
) { // ) {
import('matrix-js-sdk/lib/logger.js') // loggerPromise.then(({ logger }) => {
.then(({ logger }) => { // logger.disableAll();
logger.disableAll(); // });
}) // }
.catch((err) => {
console.error("Error loading logger module:", err);
});
}
function MatrixFolderNameFromUserId(name) { function MatrixFolderNameFromUserId(name) {
return name.replace(/[^a-z0-9]/gi, '_').toLowerCase(); return name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
@@ -74,11 +58,10 @@ module.exports = function(RED) {
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
this.e2ee = n.enableE2ee || false; this.e2ee = n.enableE2ee || false;
this.globalAccess = n.global; this.globalAccess = n.global;
this.allowUnknownDevices = n.allowUnknownDevices || false;
this.initializedAt = new Date(); this.initializedAt = new Date();
node.initialSyncLimit = 25; node.initialSyncLimit = 25;
// Keep track of all consumers of this node to catch errors // Keep track of all consumers of this node to be able to catch errors
node.register = function(consumerNode) { node.register = function(consumerNode) {
node.users[consumerNode.id] = consumerNode; node.users[consumerNode.id] = consumerNode;
}; };
@@ -102,7 +85,7 @@ module.exports = function(RED) {
} else if(!this.url) { } 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 { } else {
node.setConnected = async function(connected, cb) { node.setConnected = function(connected, cb) {
if (node.connected !== connected) { if (node.connected !== connected) {
node.connected = connected; node.connected = connected;
if(typeof cb === 'function') { if(typeof cb === 'function') {
@@ -118,7 +101,7 @@ module.exports = function(RED) {
device_id = this.matrixClient.getDeviceId(); device_id = this.matrixClient.getDeviceId();
if(!device_id && node.enableE2ee) { 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 { } else {
if(!stored_device_id || stored_device_id !== device_id) { if(!stored_device_id || stored_device_id !== device_id) {
node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`); node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`);
@@ -135,7 +118,7 @@ module.exports = function(RED) {
node.matrixClient.setDeviceDetails(device_id, { node.matrixClient.setDeviceDetails(device_id, {
display_name: node.deviceLabel display_name: node.deviceLabel
}).then( }).then(
function() {}, function(response) {},
function(error) { function(error) {
node.error("Failed to set device label: " + error, {}); node.error("Failed to set device label: " + error, {});
} }
@@ -164,311 +147,294 @@ module.exports = function(RED) {
fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist
upgradeDirectoryIfNecessary(node, storageDir); upgradeDirectoryIfNecessary(node, storageDir);
node.matrixClient = sdk.createClient({
baseUrl: this.url,
accessToken: this.credentials.accessToken,
cryptoStore: new LocalStorageCryptoStore(localStorage),
store: new MemoryStore({
localStorage: localStorage,
}),
userId: this.userId,
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined
});
node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`); // Wait for the dynamic imports to resolve
Promise.all([sdkPromise, LocalStorageCryptoStorePromise]).then(([sdkModule, LocalStorageCryptoStoreModule]) => {
const sdk = sdkModule.default || sdkModule;
const { LocalStorageCryptoStore } = LocalStorageCryptoStoreModule;
// set globally if configured to do so node.matrixClient = sdk.createClient({
if(this.globalAccess) { baseUrl: this.url,
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient); accessToken: this.credentials.accessToken,
} cryptoStore: new LocalStorageCryptoStore(localStorage),
store: new sdk.MemoryStore({
function stopClient() { localStorage: localStorage,
if(node.matrixClient && node.matrixClient.clientRunning) { }),
node.matrixClient.stopClient(); userId: this.userId,
node.setConnected(false); deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined
} // verificationMethods: ["m.sas.v1"]
if(retryStartTimeout) {
clearTimeout(retryStartTimeout);
}
}
node.on('close', function(done) {
stopClient();
if(node.globalAccess) {
try {
node.context().global.set('matrixClient["'+node.userId+'"]', undefined);
} catch(e) {
node.error(e.message, {});
}
}
done();
});
node.isConnected = function() {
return node.connected;
};
node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result");
return;
}
if (!data || !data.liveEvent) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message");
return;
}
if(node.initializedAt > 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;
}
try {
await node.matrixClient.decryptEventIfNeeded(event);
} catch (error) {
node.error(error, {});
return;
}
const isDmRoom = (room) => {
// Find out if this is a direct message room.
let isDM = !!room.getDMInviter();
const allMembers = room.currentState.getMembers();
if (!isDM && allMembers.length <= 2) {
// if not a DM, but there are 2 users only
// double check DM (needed because getDMInviter works only if you were invited, not if you invite)
// hence why we check for each member
if (allMembers.some((m) => m.getDMInviter())) {
return true;
}
}
return allMembers.length <= 2 && isDM;
};
let msg = {
encrypted : event.isEncrypted(),
redacted : event.isRedacted(),
content : event.getContent(),
type : (event.getContent()['msgtype'] || event.getType()) || null,
payload : (event.getContent()['body'] || event.getContent()) || null,
isDM : isDmRoom(room),
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,
};
// remove keys from user property that start with an underscore
Object.keys(msg.user).forEach(function (key) {
if (/^_/.test(key)) {
delete msg.user[key];
}
}); });
node.log(`Received ${msg.encrypted ? 'encrypted ' : ''}timeline event [${msg.type}]: (${room.name}) ${event.getSender()} :: ${msg.content.body} ${toStartOfTimeline ? ' [PAGINATED]' : ''}`); node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`);
node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
});
/** // set globally if configured to do so
* Fires when we want to suggest to the user that they restore their megolm keys if(this.globalAccess) {
* from backup or by cross-signing the device. this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
*
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
*/
// node.matrixClient.on("crypto.suggestKeyRestore", function(){
//
// });
// node.matrixClient.on("RoomMember.typing", async function(event, member) {
// let isTyping = member.typing;
// let roomId = member.roomId;
// });
// node.matrixClient.on("RoomMember.powerLevel", async function(event, member) {
// let newPowerLevel = member.powerLevel;
// let newNormPowerLevel = member.powerLevelNorm;
// let roomId = member.roomId;
// });
// node.matrixClient.on("RoomMember.name", async function(event, member) {
// let newName = member.name;
// let roomId = member.roomId;
// });
// handle auto-joining rooms
node.matrixClient.on(RoomMemberEvent.Membership, async function(event, member) {
if(node.initializedAt > event.getDate()) {
return; // skip events that occurred before our client initialized
} }
if (member.membership === "invite" && member.userId === node.userId) { function stopClient() {
node.log("Got invite to join room " + member.roomId); if(node.matrixClient && node.matrixClient.clientRunning) {
if(node.autoAcceptRoomInvites) { node.matrixClient.stopClient();
node.matrixClient.joinRoom(member.roomId).then(function() { node.setConnected(false);
node.log("Automatically accepted invitation to join room " + member.roomId); }
}).catch(function(e) {
node.warn("Cannot join room (could be from being kicked/banned) " + member.roomId + ": " + e); if(retryStartTimeout) {
clearTimeout(retryStartTimeout);
}
}
node.on('close', function(done) {
stopClient();
if(node.globalAccess) {
try {
node.context().global.set('matrixClient["'+node.userId+'"]', undefined);
} catch(e){
node.error(e.message, {});
}
}
done();
});
node.isConnected = function() {
return node.connected;
};
const { RelationType, RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent } = sdk;
node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result");
return; // ignore paginated results
}
if (!data || !data.liveEvent) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message");
return; // ignore old message (we only want live events)
}
if(node.initializedAt > 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
}
try {
await node.matrixClient.decryptEventIfNeeded(event);
} catch (error) {
node.error(error, {});
return;
}
const isDmRoom = (room) => {
// Find out if this is a direct message room.
let isDM = !!room.getDMInviter();
const allMembers = room.currentState.getMembers();
if (!isDM && allMembers.length <= 2) {
// if not a DM, but there are 2 users only
// double check DM (needed because getDMInviter works only if you were invited, not if you invite)
// hence why we check for each member
if (allMembers.some((m) => m.getDMInviter())) {
return true;
}
}
return allMembers.length <= 2 && isDM;
};
let msg = {
encrypted : event.isEncrypted(),
redacted : event.isRedacted(),
content : event.getContent(),
type : (event.getContent()['msgtype'] || event.getType()) || null,
payload : (event.getContent()['body'] || event.getContent()) || null,
isDM : isDmRoom(room),
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,
};
// remove keys from user property that start with an underscore
if (msg.user) {
Object.keys(msg.user).forEach(function (key) {
if (/^_/.test(key)) {
delete msg.user[key];
}
}); });
} }
let room = node.matrixClient.getRoom(event.getRoomId()); node.log(`Received ${msg.encrypted ? 'encrypted ' : ''}timeline event [${msg.type}]: (${room.name}) ${event.getSender()} :: ${msg.content.body} ${toStartOfTimeline ? ' [PAGINATED]' : ''}`);
node.emit("Room.invite", { node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
type : 'm.room.member', });
userId : event.getSender(),
topic : event.getRoomId(),
topicName : (room ? room.name : null) || null,
event : event,
eventId : event.getId(),
});
}
});
node.matrixClient.on(ClientEvent.Sync, async function(state, prevState, data) { // handle auto-joining rooms
node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]");
if(prevState === null && state === "PREPARED" ) {
// Occurs when the initial sync is completed first time.
// This involves setting up filters and obtaining push rules.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} 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", {});
});
} else if(prevState === "ERROR" && state === "PREPARED") {
// Occurs when the initial sync succeeds
// after previously failing.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(prevState === "PREPARED" && state === "SYNCING") {
// Occurs immediately after transitioning to PREPARED.
// Starts listening for live updates rather than catching up.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(prevState === "SYNCING" && state === "RECONNECTING") {
// Occurs when the live update fails.
node.setConnected(false, function(){
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", {});
});
} else if(prevState === "RECONNECTING" && state === "ERROR") {
// Occurs when the keepalive call also fails
node.setConnected(false, function(){
node.error("Connection to Matrix server lost", {});
});
} else if(prevState === "ERROR" && state === "SYNCING") {
// Occurs when the client has performed a
// live update after having previously failed.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(prevState === "ERROR" && state === "ERROR") {
// 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", {});
});
} else if(prevState === "SYNCING" && state === "SYNCING") {
// Occurs when the client has performed a live update.
// This is called <i>after</i> processing.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(state === "STOPPED") {
// 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.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){ node.matrixClient.on(RoomMemberEvent.Membership, async function(event, member) {
// Example if user auth token incorrect: if(node.initializedAt > event.getDate()) {
// { return; // skip events that occurred before our client initialized
// errcode: 'M_UNKNOWN_TOKEN',
// data: {
// errcode: 'M_UNKNOWN_TOKEN',
// error: 'Invalid macaroon passed.',
// soft_logout: false
// },
// httpStatus: 401
// }
node.error("Authentication failure: " + errorObj, {});
stopClient();
});
async function run() {
try {
if(node.e2ee){
node.log("Initializing crypto...");
await node.matrixClient.initCrypto();
node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices
node.matrixClient.getCrypto().globalErrorOnUnknownDevices = !node.allowUnknownDevices;
} }
node.log("Connecting to Matrix server...");
await node.matrixClient.startClient({
initialSyncLimit: node.initialSyncLimit
});
} catch(error) {
node.error(error, {});
}
}
// do an authed request and only continue if we don't get an error if (member.membership === "invite" && member.userId === node.userId) {
// this prevent the matrix client from crashing Node-RED on invalid auth token node.log("Got invite to join room " + member.roomId);
(function checkAuthTokenThenStart() { if(node.autoAcceptRoomInvites) {
if(node.matrixClient.clientRunning) { node.matrixClient.joinRoom(member.roomId).then(function() {
return; node.log("Automatically accepted invitation to join room " + member.roomId);
} }).catch(function(e) {
node.warn("Cannot join room (could be from being kicked/banned) " + member.roomId + ": " + e);
/** });
* We do a /whoami request before starting for a few reasons:
* - validate our auth token
* - make sure auth token belongs to provided node.userId
* - fetch device_id if possible (only available on Synapse >= v1.40.0 under MSC2033)
*/
node.matrixClient.whoami()
.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.", {});
}
if('device_id' in data && data['device_id'] && !node.deviceId) {
// if we have no device_id configured lets use the one
// returned by /whoami for this access_token
node.matrixClient.deviceId = data['device_id'];
}
// 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']}`, {});
return;
}
run().catch((error) => node.error(error));
},
function(err) {
// 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, {});
}
} }
);
})(); let room = node.matrixClient.getRoom(event.getRoomId());
node.emit("Room.invite", {
type : 'm.room.member',
userId : event.getSender(),
topic : event.getRoomId(),
topicName : (room ? room.name : null) || null,
event : event,
eventId : event.getId(),
});
}
});
node.matrixClient.on(ClientEvent.Sync, async function(state, prevState, data) {
node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]");
if(prevState === null && state === "PREPARED" ) {
// Occurs when the initial sync is completed first time.
// This involves setting up filters and obtaining push rules.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} 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", {});
});
} else if(prevState === "ERROR" && state === "PREPARED") {
// Occurs when the initial sync succeeds
// after previously failing.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(prevState === "PREPARED" && state === "SYNCING") {
// Occurs immediately after transitioning to PREPARED.
// Starts listening for live updates rather than catching up.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(prevState === "SYNCING" && state === "RECONNECTING") {
// Occurs when the live update fails.
node.setConnected(false, function(){
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", {});
});
} else if(prevState === "RECONNECTING" && state === "ERROR") {
// Occurs when the keepalive call also fails
node.setConnected(false, function(){
node.error("Connection to Matrix server lost", {});
});
} else if(prevState === "ERROR" && state === "SYNCING") {
// Occurs when the client has performed a
// live update after having previously failed.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(prevState === "ERROR" && state === "ERROR") {
// 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", {});
});
} else if(prevState === "SYNCING" && state === "SYNCING") {
// Occurs when the client has performed a live update.
// This is called <i>after</i> processing.
node.setConnected(true, function(){
node.log("Matrix client connected");
});
} else if(state === "STOPPED") {
// 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.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){
node.error("Authentication failure: " + errorObj, {});
stopClient();
});
async function run() {
try {
if(node.e2ee){
node.matrixClient.on("crypto.legacyCryptoStoreMigrationProgress", function(progress, total){
node.log(`Migrating from legacy crypto to rust crypto. ${progress}/${total}`);
});
await node.matrixClient.initRustCrypto({
useIndexedDB: false
});
console.log(`CRYPTO VERSION: ${node.matrixClient.getCrypto()?.getVersion()}`);
node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices
}
node.log("Connecting to Matrix server...");
await node.matrixClient.startClient({
initialSyncLimit: node.initialSyncLimit
});
} catch(error) {
node.error(error);
node.error(error, {});
}
}
// do an authed request and only continue if we don't get an error
// this prevent the matrix client from crashing Node-RED on invalid auth token
(function checkAuthTokenThenStart() {
if(node.matrixClient.clientRunning) {
return;
}
/**
* We do a /whoami request before starting for a few reasons:
* - validate our auth token
* - make sure auth token belongs to provided node.userId
* - fetch device_id if possible (only available on Synapse >= v1.40.0 under MSC2033)
*/
node.matrixClient.whoami()
.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.", {});
}
if('device_id' in data && data['device_id'] && !node.deviceId) {
// if we have no device_id configured lets use the one
// returned by /whoami for this access_token
node.matrixClient.deviceId = data['device_id'];
}
// 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']}`, {});
return;
}
run().catch((error) => node.error(error));
},
function(err) {
// 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, {});
}
}
)
})();
}).catch((error) => {
node.error("Failed to load Matrix SDK modules: " + error, {});
});
} }
} }
@@ -492,26 +458,28 @@ module.exports = function(RED) {
deviceId = req.body.deviceId || undefined, deviceId = req.body.deviceId || undefined,
displayName = req.body.displayName || undefined; displayName = req.body.displayName || undefined;
(async () => { sdkPromise.then((sdkModule) => {
const mod = await import("matrix-js-sdk"); const sdk = sdkModule.default || sdk;
const matrixClient = mod.createClient({
const matrixClient = sdk.createClient({
baseUrl: baseUrl, baseUrl: baseUrl,
deviceId: deviceId, deviceId: deviceId,
timelineSupport: true, timelineSupport: true,
localTimeoutMs: '30000' localTimeoutMs: '30000',
request
}); });
matrixClient.timelineSupport = true; matrixClient.timelineSupport = true;
matrixClient.login('m.login.password', { matrixClient.login(
identifier: { 'm.login.password', {
type: 'm.id.user', identifier: {
user: userId, type: 'm.id.user',
}, user: userId,
password: password, },
initial_device_display_name: displayName password: password,
}) initial_device_display_name: displayName
})
.then( .then(
function(response) { function(response) {
res.json({ res.json({
@@ -528,11 +496,13 @@ module.exports = function(RED) {
}); });
} }
); );
})().catch(err => { }).catch((error) => {
res.json({ result: 'error', message: err.toString() }); res.json({
'result': 'error',
'message': "Failed to load Matrix SDK: " + error
});
}); });
} });
);
function upgradeDirectoryIfNecessary(node, storageDir) { function upgradeDirectoryIfNecessary(node, storageDir) {
let oldStorageDir = './matrix-local-storage', let oldStorageDir = './matrix-local-storage',
@@ -573,6 +543,9 @@ module.exports = function(RED) {
} }
} }
/**
* If a device ID is stored we will use that for the client
*/
function getStoredDeviceId(localStorage) { function getStoredDeviceId(localStorage) {
let deviceId = localStorage.getItem('my_device_id'); let deviceId = localStorage.getItem('my_device_id');
if(deviceId === "null" || !deviceId) { if(deviceId === "null" || !deviceId) {
@@ -588,4 +561,4 @@ module.exports = function(RED) {
localStorage.setItem('my_device_id', deviceId); localStorage.setItem('my_device_id', deviceId);
return true; return true;
} }
}; }