diff --git a/package.json b/package.json index 7fb7a37..54be1e4 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,15 @@ "matrix-server-config": "src/matrix-server-config.js", "matrix-receive": "src/matrix-receive.js", "matrix-send-message": "src/matrix-send-message.js", + "matrix-typing": "src/matrix-typing.js", + "matrix-mark-read": "src/matrix-mark-read.js", "matrix-delete-event": "src/matrix-delete-event.js", "matrix-send-file": "src/matrix-send-file.js", "matrix-send-image": "src/matrix-send-image.js", "matrix-upload-file": "src/matrix-upload-file.js", "matrix-react": "src/matrix-react.js", + "matrix-user-settings": "src/matrix-user-settings.js", + "matrix-get-user": "src/matrix-get-user.js", "matrix-create-room": "src/matrix-create-room.js", "matrix-invite-room": "src/matrix-invite-room.js", "matrix-room-invite": "src/matrix-room-invite.js", @@ -45,9 +49,7 @@ "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-typing": "src/matrix-typing.js", - "matrix-user-settings": "src/matrix-user-settings.js", - "matrix-get-user": "src/matrix-get-user.js" + "matrix-paginate-room": "src/matrix-paginate-room.js" } }, "engines": { diff --git a/src/matrix-mark-read.html b/src/matrix-mark-read.html new file mode 100644 index 0000000..e74f899 --- /dev/null +++ b/src/matrix-mark-read.html @@ -0,0 +1,254 @@ + + + + + \ 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 @@