Merge pull request #96 from Skylar-Tech/dev

Release v0.8.0
This commit is contained in:
Skylar Sadlier 2024-09-03 21:03:18 -06:00 committed by GitHub
commit 5de1274def
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 4880 additions and 416 deletions

View File

@ -11,9 +11,11 @@ The following is supported from this package:
- End-to-end encryption - End-to-end encryption
- [Currently a WIP](#end-to-end-encryption-notes) - [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 - 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) - Send Images/Files (sending files to e2ee room doesn't currently encrypt them yet)
- Edit messages - Edit messages
- Send typing events (Bot is typing ...)
- Delete events (messages, reactions, etc) - Delete events (messages, reactions, etc)
- Decrypt files in e2ee rooms - Decrypt files in e2ee rooms
- Send HTML/Plain Text Message/Notice - Send HTML/Plain Text Message/Notice

View File

@ -24,6 +24,7 @@ Build something cool with these nodes? Feel free to submit a pull request to sha
- [Respond to "rooms <user_id>" with user's rooms (list server's rooms if <user_id> is left blank)](#respond-to-rooms-user_id-with-users-rooms-list-servers-rooms-if-user_id-is-left-blank) - [Respond to "rooms <user_id>" with user's rooms (list server's rooms if <user_id> is left blank)](#respond-to-rooms-user_id-with-users-rooms-list-servers-rooms-if-user_id-is-left-blank)
- [Respond to "whois <user_id>" with information about the user's session](#respond-to-whois-user_id-with-information-about-the-users-session) - [Respond to "whois <user_id>" 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) - [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) - [Download & store all received files/images](#download--store-all-received-filesimages)
- [Kick/Ban user from room](#kickban-user-from-room) - [Kick/Ban user from room](#kickban-user-from-room)
- [Deactivate user](#deactivate-user) - [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) ![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 ### Kick/Ban user from room
[View JSON](room-kick-ban.json) [View JSON](room-kick-ban.json)

View File

@ -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": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

1060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,22 @@
{ {
"name": "node-red-contrib-matrix-chat", "name": "node-red-contrib-matrix-chat",
"version": "0.7.1", "version": "0.8.0",
"description": "Matrix chat server client for Node-RED", "description": "Matrix chat server client for Node-RED",
"dependencies": { "dependencies": {
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"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",
"isomorphic-webcrypto": "^2.3.8", "isomorphic-webcrypto": "^2.3.8",
"matrix-js-sdk": "^28.0.0", "matrix-js-sdk": "^28.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",
"olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download", "olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download",
"request": "^2.88.2", "request": "^2.88.2",
"sharp": "^0.33.4",
"tmp": "^0.2.1",
"utf8": "^3.0.0" "utf8": "^3.0.0"
}, },
"node-red": { "node-red": {
@ -20,10 +25,15 @@
"matrix-server-config": "src/matrix-server-config.js", "matrix-server-config": "src/matrix-server-config.js",
"matrix-receive": "src/matrix-receive.js", "matrix-receive": "src/matrix-receive.js",
"matrix-send-message": "src/matrix-send-message.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-delete-event": "src/matrix-delete-event.js",
"matrix-send-file": "src/matrix-send-file.js", "matrix-send-file": "src/matrix-send-file.js",
"matrix-send-image": "src/matrix-send-image.js", "matrix-send-image": "src/matrix-send-image.js",
"matrix-upload-file": "src/matrix-upload-file.js",
"matrix-react": "src/matrix-react.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-create-room": "src/matrix-create-room.js",
"matrix-invite-room": "src/matrix-invite-room.js", "matrix-invite-room": "src/matrix-invite-room.js",
"matrix-room-invite": "src/matrix-room-invite.js", "matrix-room-invite": "src/matrix-room-invite.js",
@ -32,13 +42,15 @@
"matrix-crypt-file": "src/matrix-crypt-file.js", "matrix-crypt-file": "src/matrix-crypt-file.js",
"matrix-room-kick": "src/matrix-room-kick.js", "matrix-room-kick": "src/matrix-room-kick.js",
"matrix-room-ban": "src/matrix-room-ban.js", "matrix-room-ban": "src/matrix-room-ban.js",
"matrix-room-users": "src/matrix-room-users.js",
"matrix-room-state-events": "src/matrix-room-state-events.js",
"matrix-synapse-users": "src/matrix-synapse-users.js", "matrix-synapse-users": "src/matrix-synapse-users.js",
"matrix-synapse-register": "src/matrix-synapse-register.js", "matrix-synapse-register": "src/matrix-synapse-register.js",
"matrix-synapse-create-edit-user": "src/matrix-synapse-create-edit-user.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-deactivate-user": "src/matrix-synapse-deactivate-user.js",
"matrix-synapse-join-room": "src/matrix-synapse-join-room.js", "matrix-synapse-join-room": "src/matrix-synapse-join-room.js",
"matrix-whois-user": "src/matrix-whois-user.js", "matrix-whois-user": "src/matrix-whois-user.js",
"matrix-room-users": "src/matrix-room-users.js" "matrix-paginate-room": "src/matrix-paginate-room.js"
} }
}, },
"engines": { "engines": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -8,7 +8,7 @@
outputs: 2, outputs: 2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" } server: { type: "matrix-server-config" }
}, },
label: function() { label: function() {
return this.name || "Create Room"; return this.name || "Create Room";

View File

@ -8,9 +8,10 @@ module.exports = function(RED) {
this.server = RED.nodes.getNode(n.server); this.server = RED.nodes.getNode(n.server);
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
node.server.register(node);
this.encodeUri = function(pathTemplate, variables) { this.encodeUri = function(pathTemplate, variables) {
for (const key in variables) { for (const key in variables) {
@ -41,8 +42,9 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
if(!msg.payload) { if(!msg.payload) {
@ -64,6 +66,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-create-room", MatrixCreateRoom); RED.nodes.registerType("matrix-create-room", MatrixCreateRoom);
} }

View File

@ -12,22 +12,22 @@ module.exports = function(RED) {
const { got } = await import('got'); const { got } = await import('got');
if(!msg.type) { if(!msg.type) {
node.error('msg.type is required.'); node.error('msg.type is required.', msg);
return; return;
} }
if(!msg.content) { if(!msg.content) {
node.error('msg.content is required.'); node.error('msg.content is required.', msg);
return; return;
} }
if(!msg.content.file) { if(!msg.content.file) {
node.error('msg.content.file is required.'); node.error('msg.content.file is required.', msg);
return; return;
} }
if(!msg.url) { if(!msg.url) {
node.error('msg.url is required.'); node.error('msg.url is required.', msg);
return; return;
} }

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
reason: { value: "" }, reason: { value: "" },
}, },

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -27,7 +28,7 @@ module.exports = function(RED) {
node.on('input', function(msg) { node.on('input', function(msg) {
if(!msg.eventId) { if(!msg.eventId) {
node.error("eventId is missing"); node.error("eventId is missing", msg);
node.send([null, msg]) node.send([null, msg])
return; return;
} }
@ -38,7 +39,7 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return; return;
} }
@ -70,6 +71,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent); RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent);
} }

0
src/matrix-file-crypt.js Normal file
View File

352
src/matrix-get-user.html Normal file
View File

@ -0,0 +1,352 @@
<script type="text/html" data-template-name="matrix-get-user">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-user"><i class="fa fa-user"></i> User ID</label>
<input type="text" id="node-input-user">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-user"></i> Property</label>
<input type="text" id="node-input-property">
</div>
<div class="form-row form-tips">
This is the property the user data object will be set to
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
</div>
<div class="form-row node-input-rule-container-row">
<ol id="node-input-rule-container"></ol>
</div>
</script>
<script type="text/html" data-help-name="matrix-get-user">
<h3>Details</h3>
<p>
Get data for a user. Data includes display name, avatar URL, presence, last active, currently active, and latest user events.
</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">msg.userId | dynamic
<span class="property-type">string</span>
</dt>
<dd> The user to get details for.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg <span class="property-type">object</span></dt>
<dd>Original message object with modifications based on config.</dd>
</dl>
<dl class="message-properties">
<dt>msg.error <span class="property-type">undefined|object</span></dt>
<dd>Returned if there was an error getting the user</dd>
</dl>
<dt class="optional">dynamic
<span class="property-type">string|object</span>
</dt>
<dd> You configure what to return on the node.</dd>
</li>
</ol>
</script>
<script type="text/javascript">
(function(){
var roomEventTypeOptions = [
{ value: "m.room.name", label: "m.room.name" },
{ value: "m.room.topic", label: "m.room.topic" },
{ value: "m.room.avatar", label: "m.room.avatar" },
{ value: "m.room.power_levels", label: "m.room.power_levels" },
{ value: "m.room.guest_access", label: "m.room.guest_access" },
{ value: "m.room.join_rules", label: "m.room.join_rules" },
{ value: "m.room.canonical_alias", label: "m.room.canonical_alias" },
{ value: "m.room.history_visibility", label: "m.room.history_visibility" },
{ value: "m.room.server_acl", label: "m.room.server_acl" },
{ value: "m.room.pinned_events", label: "m.room.pinned_events"},
{ value: "m.space.child", label: "m.space.child" },
{ value: "m.space.parent", label: "m.space.parent" },
];
var defaultRules = [{
t: "set",
p: roomEventTypeOptions[0].value,
to: "payload",
tot: "msg"
}];
function isInvalidProperty(v,vt) {
if (/msg|flow|global/.test(vt)) {
if (!RED.utils.validatePropertyExpression(v)) {
return "Invalid property: " + v;
}
} else if (vt === "jsonata") {
try{ jsonata(v); } catch(e) {
return "Invalid expression: " + e.message;
}
} else if (vt === "json") {
try{ JSON.parse(v); } catch(e) {
return "Invalid JSON data: " + e.message;
}
}
return false;
}
RED.nodes.registerType('matrix-get-user',{
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["success", "error"],
inputs:1,
outputs:2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
userType: { value: "msg" },
userValue: { value: "userId" },
propertyType: { value: "msg" },
propertyValue: { value: "user" },
},
oneditprepare: function() {
$("#node-input-user").typedInput({
type: this.roomType,
types:['msg','flow','global','str'],
})
.typedInput('value', this.userValue)
.typedInput('type', this.userType);
$("#node-input-property").typedInput({
type: this.roomType,
types:['msg','flow','global','str'],
})
.typedInput('value', this.propertyValue)
.typedInput('type', this.propertyType);
var set = "Set";
var to = "to the value";
var toValueLabel = "to the property";
var search = this._("change.action.search");
var replace = this._("change.action.replace");
var regex = this._("change.label.regex");
function createPropertyValue(row2_1, row2_2, type, defaultType) {
var propValInput = $('<input/>',{class:"node-input-rule-property-value",type:"text"})
.appendTo(row2_1)
.typedInput({
default: defaultType || (type === 'set' ? 'str' : 'msg'),
types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])
});
var lsLabel = $('<label style="padding-left: 130px;"></label>').appendTo(row2_2);
var localStorageEl = $('<input type="checkbox" class="node-input-rule-property-localStorage" style="width: auto; margin: 0 6px 0 0">').appendTo(lsLabel);
$('<span>').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, localStorageEl];
}
$('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var rule = opt;
if (!rule.hasOwnProperty('t')) {
rule = {t:"set",p:roomEventTypeOptions[0].value,to:"payload",tot:"msg"};
}
if (rule.t === "set" && !rule.tot) {
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
rule.to = rule.to.substring(4);
rule.tot = "msg";
} else {
rule.tot = "str";
}
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
let fragment = document.createDocumentFragment();
var row1 = $('<div/>',{style:"display:flex; align-items: center"}).appendTo(fragment);
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row4 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(fragment);
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width:110px; margin-right:10px;"}).appendTo(row1);
var selectOptions = [
{v:"set",l:"Set"},
{v:"get",l:"Get"}
];
for (var x=0; x<selectOptions.length; x++) {
selectField.append($("<option></option>").val(selectOptions[x].v).text(selectOptions[x].l));
}
var propertyName = $('<input/>',{class:"node-input-rule-property-name",type:"text"})
.appendTo(row1)
.autoComplete({
minLength:0,
search: function(val) {
if(!val) {
return roomEventTypeOptions.sort(function(A,B){return A.i-B.i});
}
var matches = [];
roomEventTypeOptions.map((x) => x.value).forEach(v => {
var i = v.toLowerCase().indexOf(val.toLowerCase());
if (i > -1) {
matches.push({
value: v,
label: v,
i: i
})
}
});
matches.sort(function(A,B){return A.i-B.i})
return matches
}
})
.on('focus', function(evt){
// following is a fix so autocomplete will show list on focus
if(!evt.isTrigger) {
evt.stopPropagation();
$(this).trigger("keyup.red-ui-autoComplete");
}
}).on("keyup", function(evt) {
// following allows autocomplete to display even when backspace/delete is used
if (evt.keyCode === 8 || evt.keyCode === 46) {
evt.stopPropagation();
$(this).trigger("keyup.red-ui-autoComplete");
}
});
var row2_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row2);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(toValueLabel)
.appendTo(row2_1);
var row2_2 = $('<div/>', {style:"margin-top: 4px;"}).appendTo(row2);
var row3_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row3);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(search)
.appendTo(row3_1);
var row3_2 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(row3);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(replace)
.appendTo(row3_2);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(to)
.appendTo(row4);
let propertyValue = null;
let localStorageEl = null;
let fromValue = null;
let toValue = null;
selectField.on("change", function() {
var type = $(this).val();
if (propertyValue) {
propertyValue.typedInput('hide');
}
if (fromValue) {
fromValue.typedInput('hide');
}
if (toValue) {
toValue.typedInput('hide');
}
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','json','jsonata'] : ['msg', 'flow', 'global']));
}
propertyValue.typedInput('show');
row2.show();
if(type === 'get') {
localStorageEl.parent().show();
} else {
localStorageEl.parent().hide();
}
row3.hide();
row4.hide();
});
selectField.val(rule.t);
propertyName.val(rule.p);
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.parent().show();
} else {
localStorageEl.parent().hide();
}
}
selectField.change();
container[0].appendChild(fragment);
},
removable: true,
sortable: true
});
for (var i=0; i<this.rules.length; i++) {
var rule = this.rules[i];
$("#node-input-rule-container").editableList('addItem',rule);
}
},
oneditsave: function() {
this.userType = $("#node-input-user").typedInput('type');
this.userValue = $("#node-input-user").typedInput('value');
this.propertyType = $("#node-input-property").typedInput('type');
this.propertyValue = $("#node-input-property").typedInput('value');
var rules = $("#node-input-rule-container").editableList('items');
var node = this;
node.rules= [];
rules.each(function(i) {
var rule = $(this);
var type = rule.find(".node-input-rule-type").val();
var r = {
t:type,
p:rule.find(".node-input-rule-property-name").val(),
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);
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = size.height;
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
height += 16;
$("#node-input-rule-container").editableList('height',height);
},
label: function() {
return this.name || "Get User";
},
paletteLabel: 'Get User'
});
})();
</script>

145
src/matrix-get-user.js Normal file
View File

@ -0,0 +1,145 @@
module.exports = function(RED) {
function MatrixGetUser(n) {
RED.nodes.createNode(this, n);
var node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
this.userType = n.userType || "msg";
this.userValue = n.userValue || "userId";
this.propertyType = n.propertyType || "msg";
this.propertyValue = n.propertyValue || "user";
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) {
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}));
}
}
}
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;
}
let userId = getToValue(msg, node.userType, node.userValue);
if(!userId) {
msg.error = "Missing userId";
node.error(msg.error, msg);
node.send([null, msg]);
return;
}
let user = null;
try {
user = node.server.matrixClient.getUser(userId);
} catch(e) {
msg.error = "Failed getting user: " + e.message;
node.error(msg.error, msg);
node.send([null, msg]);
return;
}
if(!user) {
// failed to fetch from local storage, try to fetch data from server
let user2 = {};
try {
let profileInfo = node.server.matrixClient.getProfileInfo(userId);
if(Object.keys(profileInfo).length > 0) {
user2.displayName = profileInfo.displayname;
user2.avatarUrl = profileInfo.avatar_url;
}
let presence = node.server.matrixClient.getPresence(userId);
if(Object.keys(presence).length > 0) {
user2.currentlyActive = presence.currently_active;
user2.lastActiveAgo = presence.last_active_ago;
user2.presenceStatusMsg = presence.presence_status_msg;
user2.presence = presence.presence;
}
if(Object.keys(user2).length > 0) {
setToValue(user2, node.propertyType, node.propertyValue);
node.send([msg, null]);
return;
}
} catch(e) {
msg.error = "Failed getting user: " + e.message;
node.error(msg.error, msg);
node.send([null, msg]);
return;
}
}
setToValue(user, node.propertyType, node.propertyValue);
node.send([msg, null]);
});
node.on("close", function() {
node.server.deregister(node);
});
}
RED.nodes.registerType("matrix-get-user", MatrixGetUser);
}

View File

@ -8,7 +8,7 @@
outputs: 2, outputs: 2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
}, },
label: function() { label: function() {

View File

@ -9,9 +9,10 @@ module.exports = function(RED) {
this.roomId = n.roomId; this.roomId = n.roomId;
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
node.server.register(node);
this.encodeUri = function(pathTemplate, variables) { this.encodeUri = function(pathTemplate, variables) {
for (const key in variables) { for (const key in variables) {
@ -37,18 +38,19 @@ module.exports = function(RED) {
node.on("input", function (msg) { node.on("input", function (msg) {
if (! node.server || ! node.server.matrixClient) { if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected"); node.error("No matrix server selected", msg);
return; return;
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
if(!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; return;
} }
@ -64,6 +66,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-invite-room", MatrixInviteRoom); RED.nodes.registerType("matrix-invite-room", MatrixInviteRoom);
} }

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" } server: { type: "matrix-server-config" }
}, },
label: function() { label: function() {
return this.name || "Join Room"; return this.name || "Join Room";

View File

@ -11,6 +11,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -24,17 +25,18 @@ module.exports = function(RED) {
node.on("input", function (msg) { node.on("input", function (msg) {
if (! node.server || ! node.server.matrixClient) { if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected"); node.error("No matrix server selected", msg);
return; return;
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
if(!msg.topic) { if(!msg.topic) {
node.error("Room must be specified in msg.topic"); node.error("Room must be specified in msg.topic", msg);
return; return;
} }
@ -46,11 +48,15 @@ module.exports = function(RED) {
node.send([msg, null]); node.send([msg, null]);
}) })
.catch(function(e){ .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; msg.error = e;
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-join-room", MatrixJoinRoom); RED.nodes.registerType("matrix-join-room", MatrixJoinRoom);
} }

View File

@ -8,7 +8,7 @@
outputs: 2, outputs: 2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
}, },
label: function() { label: function() {

View File

@ -11,9 +11,10 @@ module.exports = function(RED) {
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
if (!node.server) { if (!node.server) {
node.error("No configuration node"); node.error("No configuration node", {});
return; return;
} }
node.server.register(node);
node.server.on("disconnected", function(){ node.server.on("disconnected", function(){
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -25,30 +26,36 @@ module.exports = function(RED) {
node.on('input', function(msg) { node.on('input', function(msg) {
if (! node.server || ! node.server.matrixClient) { if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected"); node.error("No matrix server selected", msg);
return; return;
} }
if(!msg.topic) { if(!msg.topic) {
node.error('No room provided in msg.topic'); node.error('No room provided in msg.topic', msg);
return; return;
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
try { try {
node.log("Leaving room " + msg.topic); node.log("Leaving room " + msg.topic);
node.server.matrixClient.leave(msg.topic); node.server.matrixClient.leave(msg.topic);
node.server.matrixClient.store.removeRoom(msg.topic);
node.send([msg, null]); node.send([msg, null]);
} catch(e) { } catch(e) {
node.error("Failed to leave room " + msg.topic + ": " + e); node.error("Failed to leave room " + msg.topic + ": " + e, msg);
msg.payload = e; msg.payload = e;
node.send([null, msg]); node.send([null, msg]);
} }
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-leave-room", MatrixLeaveRoom); RED.nodes.registerType("matrix-leave-room", MatrixLeaveRoom);
} }

254
src/matrix-mark-read.html Normal file
View File

@ -0,0 +1,254 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-mark-read',{
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["message"],
inputs: 1,
outputs: 2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
roomType: { value: "msg" },
roomValue: { value: "topic" },
eventIdType: { value: "msg" },
eventIdValue: { value: "eventId" }
},
label: function() {
return this.name || "Mark Read";
},
paletteLabel: 'Mark Read',
oneditprepare: function() {
$("#node-input-room").typedInput({
type: this.roomType,
types:['msg','flow','global','str'],
})
.typedInput('value', this.roomValue)
.typedInput('type', this.roomType);
$("#node-input-eventId").typedInput({
types:['msg','flow','global','bool'],
})
.typedInput('value', this.eventIdValue)
.typedInput('type', this.eventIdType);
},
oneditsave: function() {
this.roomType = $("#node-input-room").typedInput('type');
this.roomValue = $("#node-input-room").typedInput('value');
this.eventIdType = $("#node-input-eventId").typedInput('type');
this.eventIdValue = $("#node-input-eventId").typedInput('value');
}
});
</script>
<script type="text/html" data-template-name="matrix-mark-read">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-server"></i> Matrix Server</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
<input type="text" id="node-input-room">
</div>
<div class="form-row">
<label for="node-input-eventId"><i class="fa fa-comments"></i> Event Id</label>
<input type="text" id="node-input-eventId">
</div>
</script>
<script type="text/html" data-help-name="matrix-mark-read">
<p>Mark an event in a room as read.</p>
<h3>Outputs</h3>
<ul class="node-ports">
<li>Always Returned
<dl class="message-properties">
<dt>msg.type <span class="property-type">string</span></dt>
<dd>
the message type. For example this will be either m.text, m.reaction, m.file, m.image, etc
</dd>
</dl>
<dl class="message-properties">
<dt>msg.isDM <span class="property-type">bool</span></dt>
<dd> returns true if message is from a direct message room.</dd>
</dl>
<dl class="message-properties">
<dt>msg.encrypted <span class="property-type">bool</span></dt>
<dd> returns true if message was encrypted (e2ee).</dd>
</dl>
<dl class="message-properties">
<dt>msg.redacted <span class="property-type">bool</span></dt>
<dd> returns true if the message was redacted (such as deleted by the user).</dd>
</dl>
<dl class="message-properties">
<dt>msg.payload <span class="property-type">string</span></dt>
<dd>the body from the message's content.</dd>
</dl>
<dl class="message-properties">
<dt>msg.userId <span class="property-type">string</span></dt>
<dd>the User ID of the message sender. Example: @john:matrix.org</dd>
</dl>
<dl class="message-properties">
<dt>msg.topic <span class="property-type">string</span></dt>
<dd>the ID of the room. Example: !OGEhHVWSdvArJzumhm:matrix.org</dd>
</dl>
<dl class="message-properties">
<dt>msg.event <span class="property-type">object</span></dt>
<dd>the event object returned by the Matrix server</dd>
</dl>
<dl class="message-properties">
<dt>msg.eventId <span class="property-type">object</span></dt>
<dd>The event ID, e.g. $143350589368169JsLZx:localhost</dd>
</dl>
<dl class="message-properties">
<dt>msg.content <span class="property-type">object</span></dt>
<dd>the message's content object</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.text</strong>'
<div class="form-tips" style="margin-bottom: 12px;">
Doesn't return anything extra
</div>
</li>
<li><code>msg.type</code> == '<strong>m.reaction</strong>'
<dl class="message-properties">
<dt>msg.referenceEventId <span class="property-type">string</span></dt>
<dd>the message that the reaction relates to</dd>
</dl>
<dl class="message-properties">
<dt>msg.payload <span class="property-type">string</span></dt>
<dd>the key of the reaction's content</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.emote</strong>'
<div class="form-tips" style="margin-bottom: 12px;">
Doesn't return anything extra
</div>
</li>
<li><code>msg.type</code> == '<strong>m.sticker</strong>'
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>URL to the sticker image</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>Matrix URL to the sticker image</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_url <span class="property-type">string</span></dt>
<dd>URL to the thumbnail of the sticker</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_mxc_url <span class="property-type">string</span></dt>
<dd>Matrix URL to the thumbnail of the sticker</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.file</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the file's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the file's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the file's Matrix URL</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.audio</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the image's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.mimetype <span class="property-type">string</span></dt>
<dd>audio file mimetype (ex: audio/ogg)</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the file's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the file's Matrix URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.duration <span class="property-type">integer</span></dt>
<dd>duration of audio file in milliseconds</dd>
</dl>
<dl class="message-properties">
<dt>msg.waveform <span class="property-type">array[int]</span></dt>
<dd>waveform of the audio clip</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.image</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the image's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the image's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the image's Matrix URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_url <span class="property-type">string</span></dt>
<dd>the image's thumbnail URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_mxc_url <span class="property-type">string</span></dt>
<dd>the image's thumbnail Matrix URL</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.location</strong>'
<dl class="message-properties">
<dt>msg.geo_uri <span class="property-type">string</span></dt>
<dd>URI format of the geolocation</dd>
</dl>
</li>
</ul>
</script>

73
src/matrix-mark-read.js Normal file
View File

@ -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);
}

View File

@ -0,0 +1,284 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-paginate-room',{
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["message"],
inputs: 1,
outputs: 2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
roomType: { value: "msg" },
roomValue: { value: "topic" },
paginateKeyType: { value: "msg" },
paginateKeyValue: { value: "paginationKey" },
paginateBackwardsType: { value: "bool" },
paginateBackwardsValue: { value: "true" },
pageSizeType: { value: "num" },
pageSizeValue: { value: "25" }
},
label: function() {
return this.name || "Paginate Room";
},
paletteLabel: 'Paginate Room',
oneditprepare: function() {
$("#node-input-room").typedInput({
type: this.roomType,
types:['msg','flow','global','str'],
})
.typedInput('value', this.roomValue)
.typedInput('type', this.roomType);
$("#node-input-paginateBackwards").typedInput({
types:['msg','flow','global','bool'],
})
.typedInput('value', this.paginateBackwardsValue)
.typedInput('type', this.paginateBackwardsType);
$("#node-input-paginateKey").typedInput({
types:['msg','flow','global'],
})
.typedInput('value', this.paginateKeyValue)
.typedInput('type', this.paginateKeyType);
$("#node-input-pageSize").typedInput({
types:['msg','flow','global','num'],
})
.typedInput('value', this.pageSizeValue)
.typedInput('type', this.pageSizeType);
},
oneditsave: function() {
this.roomType = $("#node-input-room").typedInput('type');
this.roomValue = $("#node-input-room").typedInput('value');
this.paginateBackwardsType = $("#node-input-paginateBackwards").typedInput('type');
this.paginateBackwardsValue = $("#node-input-paginateBackwards").typedInput('value');
this.paginateKeyType = $("#node-input-paginateKey").typedInput('type');
this.paginateKeyValue = $("#node-input-paginateKey").typedInput('value');
this.pageSizeType = $("#node-input-pageSize").typedInput('type');
this.pageSizeValue = $("#node-input-pageSize").typedInput('value');
}
});
</script>
<script type="text/html" data-template-name="matrix-paginate-room">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-server"></i> Matrix Server</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
<input type="text" id="node-input-room">
</div>
<div class="form-row">
<label for="node-input-paginateKey"><i class="fa fa-comments"></i> Pagination Key</label>
<input type="text" id="node-input-paginateKey">
</div>
<div class="form-row">
<label for="node-input-paginateBackwards"><i class="fa fa-commenting-o"></i> Paginate Backwards</label>
<input type="text" id="node-input-paginateBackwards">
</div>
<div class="form-row">
<label for="node-input-pageSize"><i class="fa fa-commenting-o"></i> Page Size</label>
<input type="text" id="node-input-pageSize">
</div>
</script>
<script type="text/html" data-help-name="matrix-paginate-room">
<p>Receive events from Matrix.</p>
<h3>Outputs</h3>
<ul class="node-ports">
<li>Always Returned
<dl class="message-properties">
<dt>msg.type <span class="property-type">string</span></dt>
<dd>
the message type. For example this will be either m.text, m.reaction, m.file, m.image, etc
</dd>
</dl>
<dl class="message-properties">
<dt>msg.isDM <span class="property-type">bool</span></dt>
<dd> returns true if message is from a direct message room.</dd>
</dl>
<dl class="message-properties">
<dt>msg.encrypted <span class="property-type">bool</span></dt>
<dd> returns true if message was encrypted (e2ee).</dd>
</dl>
<dl class="message-properties">
<dt>msg.redacted <span class="property-type">bool</span></dt>
<dd> returns true if the message was redacted (such as deleted by the user).</dd>
</dl>
<dl class="message-properties">
<dt>msg.payload <span class="property-type">string</span></dt>
<dd>the body from the message's content.</dd>
</dl>
<dl class="message-properties">
<dt>msg.userId <span class="property-type">string</span></dt>
<dd>the User ID of the message sender. Example: @john:matrix.org</dd>
</dl>
<dl class="message-properties">
<dt>msg.topic <span class="property-type">string</span></dt>
<dd>the ID of the room. Example: !OGEhHVWSdvArJzumhm:matrix.org</dd>
</dl>
<dl class="message-properties">
<dt>msg.event <span class="property-type">object</span></dt>
<dd>the event object returned by the Matrix server</dd>
</dl>
<dl class="message-properties">
<dt>msg.eventId <span class="property-type">object</span></dt>
<dd>The event ID, e.g. $143350589368169JsLZx:localhost</dd>
</dl>
<dl class="message-properties">
<dt>msg.content <span class="property-type">object</span></dt>
<dd>the message's content object</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.text</strong>'
<div class="form-tips" style="margin-bottom: 12px;">
Doesn't return anything extra
</div>
</li>
<li><code>msg.type</code> == '<strong>m.reaction</strong>'
<dl class="message-properties">
<dt>msg.referenceEventId <span class="property-type">string</span></dt>
<dd>the message that the reaction relates to</dd>
</dl>
<dl class="message-properties">
<dt>msg.payload <span class="property-type">string</span></dt>
<dd>the key of the reaction's content</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.emote</strong>'
<div class="form-tips" style="margin-bottom: 12px;">
Doesn't return anything extra
</div>
</li>
<li><code>msg.type</code> == '<strong>m.sticker</strong>'
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>URL to the sticker image</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>Matrix URL to the sticker image</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_url <span class="property-type">string</span></dt>
<dd>URL to the thumbnail of the sticker</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_mxc_url <span class="property-type">string</span></dt>
<dd>Matrix URL to the thumbnail of the sticker</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.file</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the file's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the file's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the file's Matrix URL</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.audio</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the image's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.mimetype <span class="property-type">string</span></dt>
<dd>audio file mimetype (ex: audio/ogg)</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the file's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the file's Matrix URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.duration <span class="property-type">integer</span></dt>
<dd>duration of audio file in milliseconds</dd>
</dl>
<dl class="message-properties">
<dt>msg.waveform <span class="property-type">array[int]</span></dt>
<dd>waveform of the audio clip</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.image</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the image's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the image's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the image's Matrix URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_url <span class="property-type">string</span></dt>
<dd>the image's thumbnail URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.thumbnail_mxc_url <span class="property-type">string</span></dt>
<dd>the image's thumbnail Matrix URL</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.location</strong>'
<dl class="message-properties">
<dt>msg.geo_uri <span class="property-type">string</span></dt>
<dd>URI format of the geolocation</dd>
</dl>
</li>
</ul>
</script>

155
src/matrix-paginate-room.js Normal file
View File

@ -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);
}

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
reaction: { value: null } reaction: { value: null }
}, },

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -26,30 +27,31 @@ module.exports = function(RED) {
node.on("input", function (msg) { node.on("input", function (msg) {
if (!node.server || !node.server.matrixClient) { if (!node.server || !node.server.matrixClient) {
node.error("No matrix server selected"); node.error("No matrix server selected", msg);
return; return;
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
if(!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; return;
} }
let payload = n.reaction || msg.payload; let payload = n.reaction || msg.payload;
if(!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; return;
} }
let eventId = msg.referenceEventId || msg.eventId; let eventId = msg.referenceEventId || msg.eventId;
if(!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; return;
} }
@ -75,6 +77,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-react", MatrixReact); RED.nodes.registerType("matrix-react", MatrixReact);
} }

View File

@ -8,8 +8,9 @@
outputs:1, outputs:1,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: {"value": null}, roomId: {"value": null},
acceptOwnEvents: {"value": false},
acceptText: {"value": true}, acceptText: {"value": true},
acceptEmotes: {"value": true}, acceptEmotes: {"value": true},
acceptStickers: {"value": true}, acceptStickers: {"value": true},
@ -45,7 +46,17 @@
<div class="form-row" style="margin-left: 100px;margin-top:10px;font-weight:bold;"> <div class="form-row" style="margin-left: 100px;margin-top:10px;font-weight:bold;">
Timeline event filters Timeline event filters
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input
type="checkbox"
id="node-input-acceptOwnEvents"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-input-acceptOwnEvents" style="width: auto">
Receive own events
</label>
</div>
<div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptText" id="node-input-acceptText"
@ -55,7 +66,7 @@
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"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptEmotes" id="node-input-acceptEmotes"
@ -65,7 +76,7 @@
Accept emotes <code style="text-transform: none;">m.emote</code> Accept emotes <code style="text-transform: none;">m.emote</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptStickers" id="node-input-acceptStickers"
@ -75,7 +86,7 @@
Accept stickers <code style="text-transform: none;">m.sticker</code> Accept stickers <code style="text-transform: none;">m.sticker</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptReactions" id="node-input-acceptReactions"
@ -85,7 +96,7 @@
Accept reactions <code style="text-transform: none;">m.reaction</code> Accept reactions <code style="text-transform: none;">m.reaction</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptFiles" id="node-input-acceptFiles"
@ -95,7 +106,7 @@
Accept files <code style="text-transform: none;">m.file</code> Accept files <code style="text-transform: none;">m.file</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptAudio" id="node-input-acceptAudio"
@ -105,7 +116,7 @@
Accept files <code style="text-transform: none;">m.audio</code> Accept files <code style="text-transform: none;">m.audio</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptImages" id="node-input-acceptImages"
@ -115,7 +126,7 @@
Accept images <code style="text-transform: none;">m.image</code> Accept images <code style="text-transform: none;">m.image</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptVideos" id="node-input-acceptVideos"
@ -125,7 +136,7 @@
Accept videos <code style="text-transform: none;">m.video</code> Accept videos <code style="text-transform: none;">m.video</code>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptLocations" id="node-input-acceptLocations"

View File

@ -1,3 +1,4 @@
const {RelationType} = require("matrix-js-sdk");
module.exports = function(RED) { module.exports = function(RED) {
function MatrixReceiveMessage(n) { function MatrixReceiveMessage(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
@ -6,6 +7,7 @@ module.exports = function(RED) {
this.name = n.name; this.name = n.name;
this.server = RED.nodes.getNode(n.server); this.server = RED.nodes.getNode(n.server);
this.acceptOwnEvents = n.acceptOwnEvents;
this.acceptText = n.acceptText; this.acceptText = n.acceptText;
this.acceptEmotes = n.acceptEmotes; this.acceptEmotes = n.acceptEmotes;
this.acceptStickers = n.acceptStickers; this.acceptStickers = n.acceptStickers;
@ -16,14 +18,15 @@ module.exports = function(RED) {
this.acceptVideos = n.acceptVideos; this.acceptVideos = n.acceptVideos;
this.acceptLocations = n.acceptLocations; this.acceptLocations = n.acceptLocations;
this.roomId = n.roomId; this.roomId = n.roomId;
this.roomIds = this.roomId ? this.roomId.split(',') : []; this.roomIds = this.roomId ? this.roomId.split(',').map(s => s.trim()) : [];
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
if (!node.server) { if (!node.server) {
node.error("No configuration node"); node.error("No configuration node", {});
return; return;
} }
node.server.register(node);
node.server.on("disconnected", function(){ node.server.on("disconnected", function(){
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -35,120 +38,103 @@ module.exports = function(RED) {
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) { node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
// if node has a room ID set we only listen on that room // if node has a room ID set we only listen on that room
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) { if (node.roomIds.length && !node.roomIds.includes(room.roomId)) {
return; return;
} }
switch(msg.type) { if (!node.acceptOwnEvents && (!event.getSender() || event.getSender().toLowerCase() === node.server.matrixClient.getUserId().toLowerCase())) {
node.log(`Ignoring${msg.encrypted ? ' encrypted' : ''} timeline event [${msg.type}]: (${room.name}) ${event.getId()} for reason: own event`);
return;
}
const setUrls = (urlKey, encryptedKey) => {
const url = msg.encrypted ? msg.content[encryptedKey]?.url : msg.content[urlKey];
if (url) {
msg.url = node.server.matrixClient.mxcUrlToHttp(url);
msg.mxc_url = url;
}
};
const setThumbnailUrls = (infoKey) => {
const thumbnailFile = msg.content.info?.[infoKey];
const thumbnailUrl = thumbnailFile?.url;
if (thumbnailUrl) {
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(thumbnailUrl);
msg.thumbnail_mxc_url = thumbnailUrl;
}
};
switch (msg.type) {
case 'm.emote': case 'm.emote':
if(!node.acceptEmotes) return; if (!node.acceptEmotes) return;
break; break;
case 'm.text': case 'm.text':
if(!node.acceptText) return; if (!node.acceptText) return;
break; break;
case 'm.sticker': case 'm.sticker':
if(!node.acceptStickers) return; if (!node.acceptStickers) return;
if(msg.content.info) { setThumbnailUrls('thumbnail_url');
if(msg.content.info.thumbnail_url) { setUrls('url', 'url');
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
}
if(msg.content.url) {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
msg.mxc_url = msg.content.url;
}
}
break; break;
case 'm.file': case 'm.file':
if(!node.acceptFiles) return; if (!node.acceptFiles) return;
msg.filename = msg.content.filename || msg.content.body; msg.filename = msg.content.filename || msg.content.body;
if(msg.encrypted) { setUrls('url', 'file');
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
msg.mxc_url = msg.content.file.url;
} else {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
msg.mxc_url = msg.content.url;
}
break; break;
case 'm.audio': case 'm.audio':
if(!node.acceptAudio) return; if (!node.acceptAudio) return;
if(msg.encrypted) { setUrls('url', 'file');
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url); if ('org.matrix.msc1767.file' in msg.content) {
msg.mxc_url = msg.content.file.url;
} else {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
msg.mxc_url = msg.content.url;
}
if('org.matrix.msc1767.file' in msg.content) {
msg.filename = msg.content['org.matrix.msc1767.file'].name; msg.filename = msg.content['org.matrix.msc1767.file'].name;
msg.mimetype = msg.content['org.matrix.msc1767.file'].mimetype; msg.mimetype = msg.content['org.matrix.msc1767.file'].mimetype;
} }
if ('org.matrix.msc1767.audio' in msg.content) {
if('org.matrix.msc1767.audio' in msg.content) {
msg.duration = msg.content['org.matrix.msc1767.audio'].duration; msg.duration = msg.content['org.matrix.msc1767.audio'].duration;
msg.waveform = msg.content['org.matrix.msc1767.audio'].waveform; msg.waveform = msg.content['org.matrix.msc1767.audio'].waveform;
} }
break; break;
case 'm.image': case 'm.image':
if(!node.acceptImages) return; if (!node.acceptImages) return;
msg.filename = msg.content.filename || msg.content.body; msg.filename = msg.content.filename || msg.content.body;
if(msg.encrypted) { setUrls('url', 'file');
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url); setThumbnailUrls('thumbnail_file');
msg.mxc_url = msg.content.file.url;
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_file.url);
msg.thumbnail_mxc_url = msg.content.info.thumbnail_file.url;
} else {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
msg.mxc_url = msg.content.url;
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
}
break; break;
case 'm.video': case 'm.video':
if(!node.acceptVideos) return; if (!node.acceptVideos) return;
msg.filename = msg.content.filename || msg.content.body; msg.filename = msg.content.filename || msg.content.body;
if(msg.encrypted) { setUrls('url', 'file');
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url); setThumbnailUrls('thumbnail_file');
msg.mxc_url = msg.content.file.url;
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_file.url);
msg.thumbnail_mxc_url = msg.content.info.thumbnail_file.url;
} else {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
msg.mxc_url = msg.content.url;
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
}
break; break;
case 'm.location': case 'm.location':
if(!node.acceptLocations) return; if (!node.acceptLocations) return;
msg.geo_uri = msg.content.geo_uri; msg.geo_uri = msg.content.geo_uri;
msg.payload = msg.content.body; msg.payload = msg.content.body;
break; break;
case 'm.reaction': case 'm.reaction':
if(!node.acceptReactions) return; if (!node.acceptReactions) return;
msg.info = msg.content["m.relates_to"].info; msg.info = msg.content["m.relates_to"].info;
msg.referenceEventId = msg.content["m.relates_to"].event_id; msg.referenceEventId = msg.content["m.relates_to"].event_id;
msg.payload = msg.content["m.relates_to"].key; msg.payload = msg.content["m.relates_to"].key;
break; break;
default: default:
// node.warn("Unknown event type: " + msg.type);
return; return;
} }
node.send(msg); node.send(msg);
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-receive", MatrixReceiveMessage); RED.nodes.registerType("matrix-receive", MatrixReceiveMessage);
} }

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
reason: { value: null } reason: { value: null }
}, },

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -26,23 +27,24 @@ module.exports = function(RED) {
node.on("input", function (msg) { node.on("input", function (msg) {
if (! node.server || ! node.server.matrixClient) { if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected"); node.error("No matrix server selected", msg);
return; return;
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
if(!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; return;
} }
if(!msg.userId) { if(!msg.userId) {
node.error("msg.userId was not set."); node.error("msg.userId was not set.", msg);
return; return;
} }
@ -53,11 +55,15 @@ module.exports = function(RED) {
node.send([msg, null]); node.send([msg, null]);
}) })
.catch(function(e){ .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; msg.error = e;
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-room-ban", MatrixBan); RED.nodes.registerType("matrix-room-ban", MatrixBan);
} }

View File

@ -8,7 +8,7 @@
outputs: 1, outputs: 1,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
}, },
label: function() { label: function() {

View File

@ -9,9 +9,10 @@ module.exports = function(RED) {
this.roomId = n.roomId; this.roomId = n.roomId;
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); 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.server.on("Room.invite", async function(msg) {
node.send(msg); node.send(msg);
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-room-invite", MatrixRoomInvite); RED.nodes.registerType("matrix-room-invite", MatrixRoomInvite);
} }

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
reason: { value: null } reason: { value: null }
}, },

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -26,23 +27,24 @@ module.exports = function(RED) {
node.on("input", function (msg) { node.on("input", function (msg) {
if (! node.server || ! node.server.matrixClient) { if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected"); node.error("No matrix server selected", msg);
return; return;
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
if(!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; return;
} }
if(!msg.userId) { if(!msg.userId) {
node.error("msg.userId was not set."); node.error("msg.userId was not set.", msg);
return; return;
} }
@ -53,11 +55,15 @@ module.exports = function(RED) {
node.send([msg, null]); node.send([msg, null]);
}) })
.catch(function(e){ .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; msg.error = e;
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-room-kick", MatrixKick); RED.nodes.registerType("matrix-room-kick", MatrixKick);
} }

View File

@ -0,0 +1,369 @@
<script type="text/html" data-template-name="matrix-room-state-events">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
<input type="text" id="node-input-room">
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
</div>
<div class="form-row node-input-rule-container-row">
<ol id="node-input-rule-container"></ol>
</div>
</script>
<script type="text/html" data-help-name="matrix-room-state-events">
<h3>Details</h3>
<p>
Set and Get a list of room state events for a given room. Allows you to set/get room name, topic, avatar, etc.
</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">msg.topic
<span class="property-type">string | null</span>
</dt>
<dd> The room to set/get settings for.</dd>
<dt class="optional">dynamic
<span class="property-type">string|object</span>
</dt>
<dd> You configure what room state events in the node configuration. <code style="white-space: normal;">m.room.name</code>, <code style="white-space: normal;">m.room.avatar</code>, and <code style="white-space: normal;">m.room.guest_access</code> 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 <a href="https://spec.matrix.org/latest/client-server-api" target="_blank">Matrix Client-Server docs</a>)</dd>
<dt class="optional">msg.state_key
<span class="property-type">string</span>
</dt>
<dd> Required for some events such as <code style="white-space: normal;">m.space.parent</code> and <code style="white-space: normal;">m.room.child</code> to set the referenced child/parent room</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg <span class="property-type">object</span></dt>
<dd>Original message object with modifications based on config.</dd>
</dl>
<dl class="message-properties">
<dt>msg.setter_errors <span class="property-type">undefined|object</span></dt>
<dd>Returned if setting a room state event failed. The key of the object is the room state event type and the value is the error that occurred.</dd>
</dl>
<dl class="message-properties">
<dt>msg.getter_errors <span class="property-type">undefined|object</span></dt>
<dd>Returned if getting a room state event failed. The key of the object is the room state event type and the value is the error that occurred. Note that you will get an error if you try getting a room state event that doesn't exist (such as fetching avatar on a room that doesn't have one).</dd>
</dl>
<dt class="optional">dynamic
<span class="property-type">string|object</span>
</dt>
<dd> You configure what room state events to output in the node configuration. <code style="white-space: normal;">m.room.name</code>, <code style="white-space: normal;">m.room.avatar</code>, and <code style="white-space: normal;">m.room.guest_access</code> will come back as strings otherwise you will get the full content object of the event (find this by referencing the <a href="https://spec.matrix.org/latest/client-server-api" target="_blank">Matrix Client-Server docs</a>). 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.</dd>
</li>
</ol>
</script>
<script type="text/javascript">
(function(){
var roomEventTypeOptions = [
{ value: "m.room.name", label: "m.room.name" },
{ value: "m.room.topic", label: "m.room.topic" },
{ value: "m.room.avatar", label: "m.room.avatar" },
{ value: "m.room.power_levels", label: "m.room.power_levels" },
{ value: "m.room.guest_access", label: "m.room.guest_access" },
{ value: "m.room.join_rules", label: "m.room.join_rules" },
{ value: "m.room.canonical_alias", label: "m.room.canonical_alias" },
{ value: "m.room.history_visibility", label: "m.room.history_visibility" },
{ value: "m.room.server_acl", label: "m.room.server_acl" },
{ value: "m.room.pinned_events", label: "m.room.pinned_events"},
{ value: "m.space.child", label: "m.space.child" },
{ value: "m.space.parent", label: "m.space.parent" },
];
var defaultRules = [{
t: "set",
p: roomEventTypeOptions[0].value,
to: "payload",
tot: "msg"
}];
function isInvalidProperty(v,vt) {
if (/msg|flow|global/.test(vt)) {
if (!RED.utils.validatePropertyExpression(v)) {
return "Invalid property: " + v;
}
} else if (vt === "jsonata") {
try{ jsonata(v); } catch(e) {
return "Invalid expression: " + e.message;
}
} else if (vt === "json") {
try{ JSON.parse(v); } catch(e) {
return "Invalid JSON data: " + e.message;
}
}
return false;
}
RED.nodes.registerType('matrix-room-state-events',{
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["success", "error"],
inputs:1,
outputs:2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
roomType: { value: "msg" },
roomValue: { value: "topic" },
rules: {
value: defaultRules,
validate: function(rules, opt) {
let msg;
const errors = []
if (!rules || rules.length === 0) { return true }
for (let i=0;i<rules.length;i++) {
const opt = { label: "Rule"+' '+(i+1) }
const r = rules[i];
if (r.t === 'set' || r.t === 'get') {
if ((msg = isInvalidProperty(r.p,r.pt)) !== false) {
return msg;
}
if ((msg = isInvalidProperty(r.to,r.tot)) !== false) {
return msg;
}
}
}
return errors.length ? errors : true;
}
},
},
oneditprepare: function() {
$("#node-input-room").typedInput({
type: this.roomType,
types:['msg','flow','global','str'],
})
.typedInput('value', this.roomValue)
.typedInput('type', this.roomType);
var set = "Set";
var to = "to the value";
var toValueLabel = "to the property";
var search = this._("change.action.search");
var replace = this._("change.action.replace");
var regex = this._("change.label.regex");
function createPropertyValue(row2_1, row2_2, type, defaultType) {
var propValInput = $('<input/>',{class:"node-input-rule-property-value",type:"text"})
.appendTo(row2_1)
.typedInput({
default: defaultType || (type === 'set' ? 'str' : 'msg'),
types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])
});
var lsLabel = $('<label style="padding-left: 130px;"></label>').appendTo(row2_2);
var localStorageEl = $('<input type="checkbox" class="node-input-rule-property-localStorage" style="width: auto; margin: 0 6px 0 0">').appendTo(lsLabel);
$('<span>').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, localStorageEl];
}
$('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var rule = opt;
if (!rule.hasOwnProperty('t')) {
rule = {t:"set",p:roomEventTypeOptions[0].value,to:"payload",tot:"msg"};
}
if (rule.t === "set" && !rule.tot) {
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
rule.to = rule.to.substring(4);
rule.tot = "msg";
} else {
rule.tot = "str";
}
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
let fragment = document.createDocumentFragment();
var row1 = $('<div/>',{style:"display:flex; align-items: center"}).appendTo(fragment);
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row4 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(fragment);
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width:110px; margin-right:10px;"}).appendTo(row1);
var selectOptions = [
{v:"set",l:"Set"},
{v:"get",l:"Get"}
];
for (var x=0; x<selectOptions.length; x++) {
selectField.append($("<option></option>").val(selectOptions[x].v).text(selectOptions[x].l));
}
var propertyName = $('<input/>',{class:"node-input-rule-property-name",type:"text"})
.appendTo(row1)
.autoComplete({
minLength:0,
search: function(val) {
if(!val) {
return roomEventTypeOptions.sort(function(A,B){return A.i-B.i});
}
var matches = [];
roomEventTypeOptions.map((x) => x.value).forEach(v => {
var i = v.toLowerCase().indexOf(val.toLowerCase());
if (i > -1) {
matches.push({
value: v,
label: v,
i: i
})
}
});
matches.sort(function(A,B){return A.i-B.i})
return matches
}
})
.on('focus', function(evt){
// following is a fix so autocomplete will show list on focus
if(!evt.isTrigger) {
evt.stopPropagation();
$(this).trigger("keyup.red-ui-autoComplete");
}
}).on("keyup", function(evt) {
// following allows autocomplete to display even when backspace/delete is used
if (evt.keyCode === 8 || evt.keyCode === 46) {
evt.stopPropagation();
$(this).trigger("keyup.red-ui-autoComplete");
}
});
var row2_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row2);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(toValueLabel)
.appendTo(row2_1);
var row2_2 = $('<div/>', {style:"margin-top: 4px;"}).appendTo(row2);
var row3_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row3);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(search)
.appendTo(row3_1);
var row3_2 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(row3);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(replace)
.appendTo(row3_2);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(to)
.appendTo(row4);
let propertyValue = null;
let localStorageEl = null;
let fromValue = null;
let toValue = null;
selectField.on("change", function() {
var type = $(this).val();
if (propertyValue) {
propertyValue.typedInput('hide');
}
if (fromValue) {
fromValue.typedInput('hide');
}
if (toValue) {
toValue.typedInput('hide');
}
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','json','jsonata'] : ['msg', 'flow', 'global']));
}
propertyValue.typedInput('show');
row2.show();
if(type === 'get') {
localStorageEl.parent().show();
} else {
localStorageEl.parent().hide();
}
row3.hide();
row4.hide();
});
selectField.val(rule.t);
propertyName.val(rule.p);
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.parent().show();
} else {
localStorageEl.parent().hide();
}
}
selectField.change();
container[0].appendChild(fragment);
},
removable: true,
sortable: true
});
for (var i=0; i<this.rules.length; i++) {
var rule = this.rules[i];
$("#node-input-rule-container").editableList('addItem',rule);
}
},
oneditsave: function() {
this.roomType = $("#node-input-room").typedInput('type');
this.roomValue = $("#node-input-room").typedInput('value');
var rules = $("#node-input-rule-container").editableList('items');
var node = this;
node.rules= [];
rules.each(function(i) {
var rule = $(this);
var type = rule.find(".node-input-rule-type").val();
var r = {
t:type,
p:rule.find(".node-input-rule-property-name").val(),
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);
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = size.height;
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
height += 16;
$("#node-input-rule-container").editableList('height',height);
},
label: function() {
return this.name || "Room State Events";
},
paletteLabel: 'Room State Events'
});
})();
</script>

View File

@ -0,0 +1,293 @@
module.exports = function(RED) {
function MatrixRoomStateEvents(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.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')) {
try {
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg);
} catch(e2) {
throw new Error("Invalid value evaluation");
}
} else if (rule.tot === 'date') {
value = Date.now();
} else if (rule.tot === 'jsonata') {
try {
value = RED.util.evaluateJSONataExpression(rule.to,msg);
} catch(e3) {
throw new Error("Invalid expression");
}
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.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;
case "m.space.parent":
if (typeof value !== 'object') {
setterErrors[rule.p] = "m.space.parent content must be object";
} else if (!msg.state_key) {
setterErrors[rule.p] = "m.space.parent required msg.state_key input to be set to the child roomId";
}else {
await node.server.matrixClient.sendStateEvent(
msg.topic,
"m.room.power_levels",
value,
msg.state_key);
}
break;
case "m.space.child":
if (typeof value !== 'object') {
setterErrors[rule.p] = "m.space.child content must be object";
} else if (!msg.state_key) {
setterErrors[rule.p] = "m.space.child required msg.state_key input to be set to the parent roomId";
}else {
await node.server.matrixClient.sendStateEvent(
msg.topic,
"m.room.power_levels",
value,
msg.state_key);
}
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;
default:
if(typeof value !== 'object') {
setterErrors[rule.p] = `${rule.p} content must be object`;
} else {
await node.server.matrixClient.sendStateEvent(
msg.topic,
rule.p,
value,
msg.state_key || "");
}
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 {
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);
cachedGetters[rule.p] = value;
} 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", MatrixRoomStateEvents);
}

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: "" } roomId: { value: "" }
}, },
label: function() { label: function() {

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -31,13 +32,14 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
let roomId = node.roomId || msg.topic; let roomId = node.roomId || msg.topic;
if(!roomId) { 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; return;
} }
@ -65,6 +67,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-room-users", MatrixRoomUsers); RED.nodes.registerType("matrix-room-users", MatrixRoomUsers);
} }

View File

@ -1,6 +1,6 @@
<script type="text/javascript"> <script type="text/javascript">
RED.nodes.registerType('matrix-send-file',{ RED.nodes.registerType('matrix-send-file',{
category: 'matrix', category: 'matrix (deprecated)',
color: '#00b7ca', color: '#00b7ca',
icon: "matrix.png", icon: "matrix.png",
outputLabels: ["success", "error"], outputLabels: ["success", "error"],
@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
contentType: { value: null } contentType: { value: null }
}, },
@ -20,6 +20,26 @@
</script> </script>
<script type="text/html" data-template-name="matrix-send-file"> <script type="text/html" data-template-name="matrix-send-file">
<style>
.matrix-chat-alert {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
margin: 30px 0;
padding: 8px 35px 8px 14px;
}
.matrix-chat-alert a, .matrix-chat-alert a:active, .matrix-chat-alert a:hover {
color: #b94a48;
text-decoration: underline;
font-weight: bold;
}
</style>
<div class="matrix-chat-alert">
<strong>Deprecation Warning!</strong> This node will be removed in a future release. We have consolidated the Send File & Send Image nodes into a single Upload File node (with more options). Read <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/issues/102" target="_blank">here</a> for more info.<br>
<br>
Replace this node with a File Upload node attached to a Send Message node to accomplish the same behavior.
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name"> <input type="text" id="node-input-name" placeholder="Name">

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -31,8 +32,9 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
@ -57,7 +59,7 @@ module.exports = function(RED) {
} }
if(!msg.payload) { if(!msg.payload) {
node.error('msg.payload is required'); node.error('msg.payload is required', msg);
return; return;
} }
@ -94,6 +96,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-send-file", MatrixSendFile); RED.nodes.registerType("matrix-send-file", MatrixSendFile);
} }

View File

@ -1,6 +1,6 @@
<script type="text/javascript"> <script type="text/javascript">
RED.nodes.registerType('matrix-send-image',{ RED.nodes.registerType('matrix-send-image',{
category: 'matrix', category: 'matrix (deprecated)',
color: '#00b7ca', color: '#00b7ca',
icon: "matrix.png", icon: "matrix.png",
outputLabels: ["success", "error"], outputLabels: ["success", "error"],
@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
contentType: { value: null } contentType: { value: null }
}, },
@ -20,6 +20,26 @@
</script> </script>
<script type="text/html" data-template-name="matrix-send-image"> <script type="text/html" data-template-name="matrix-send-image">
<style>
.matrix-chat-alert {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
margin: 30px 0;
padding: 8px 35px 8px 14px;
}
.matrix-chat-alert a, .matrix-chat-alert a:active, .matrix-chat-alert a:hover {
color: #b94a48;
text-decoration: underline;
font-weight: bold;
}
</style>
<div class="matrix-chat-alert">
<strong>Deprecation Warning!</strong> This node will be removed in a future release. We have consolidated the Send File & Send Image nodes into a single Upload File node (with more options). Read <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/issues/102" target="_blank">here</a> for more info.<br>
<br>
Replace this node with a File Upload node attached to a Send Message node to accomplish the same behavior.
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name"> <input type="text" id="node-input-name" placeholder="Name">

View File

@ -13,6 +13,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -31,8 +32,9 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
@ -57,7 +59,7 @@ module.exports = function(RED) {
} }
if(!msg.payload) { if(!msg.payload) {
node.error('msg.payload is required'); node.error('msg.payload is required', msg);
return; return;
} }
@ -98,6 +100,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-send-image", MatrixSendImage); RED.nodes.registerType("matrix-send-image", MatrixSendImage);
} }

View File

@ -6,19 +6,32 @@
outputLabels: ["success", "error"], outputLabels: ["success", "error"],
inputs:1, inputs:1,
outputs:2, outputs:2,
paletteLabel: 'Send Message',
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
message: { value: null }, message: { value: null },
messageType: { value: 'm.text' }, messageType: { value: 'm.text' },
messageFormat: { value: '' }, messageFormat: { value: '' },
replaceMessage : { value: false } replaceMessage : { value: false },
threadReplyType: { value: "msg" },
threadReplyValue: { value: "isThread" },
}, },
label: function() { label: function() {
return this.name || "Send Message"; return this.name || "Send Message";
}, },
paletteLabel: 'Send Message' oneditprepare: function() {
$("#node-input-threadReply").typedInput({
types:['msg','flow','global','bool'],
})
.typedInput('value', this.threadReplyValue || "isThread")
.typedInput('type', this.threadReplyType || "msg");
},
oneditsave: function() {
this.threadReplyType = $("#node-input-threadReply").typedInput('type');
this.threadReplyValue = $("#node-input-threadReply").typedInput('value');
},
}); });
</script> </script>
@ -43,6 +56,9 @@
<label for="node-input-message"><i class="fa fa-comment"></i> Message</label> <label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
<textarea id="node-input-message" placeholder="msg.payload" style="width: 70%;"></textarea> <textarea id="node-input-message" placeholder="msg.payload" style="width: 70%;"></textarea>
</div> </div>
<div class="form-row form-tips">
If message is an object it sets the full content of the message.
</div>
<div class="form-row"> <div class="form-row">
<input <input
@ -69,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. 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.
</div> </div>
<div class="form-row">
<label for="node-input-threadReply"><i class="fa fa-commenting-o"></i> Thread Reply</label>
<input type="text" id="node-input-threadReply">
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-messageFormat"> <label for="node-input-messageFormat">
Message Format Message Format
@ -104,9 +125,9 @@
<dd> Room ID to send image to. Optional if configured on the node. If configured on the node this input will be overridden.</dd> <dd> Room ID to send image to. Optional if configured on the node. If configured on the node this input will be overridden.</dd>
<dt>msg.payload <dt>msg.payload
<span class="property-type">string</span> <span class="property-type">string|object</span>
</dt> </dt>
<dd> the message text. If configured on the node this is ignored otherwise it required. </dd> <dd> the message text or an object to customize the full content. If configured on the node this is ignored otherwise it required. </dd>
<dt>msg.replace <dt>msg.replace
<span class="property-type">bool</span> <span class="property-type">bool</span>

View File

@ -13,6 +13,8 @@ module.exports = function(RED) {
this.messageFormat = n.messageFormat; this.messageFormat = n.messageFormat;
this.replaceMessage = n.replaceMessage; this.replaceMessage = n.replaceMessage;
this.message = n.message; this.message = n.message;
this.threadReplyType = n.threadReplyType || null;
this.threadReplyValue = n.threadReplyValue || null;
// taken from https://github.com/matrix-org/synapse/blob/master/synapse/push/mailer.py // taken from https://github.com/matrix-org/synapse/blob/master/synapse/push/mailer.py
this.allowedTags = [ this.allowedTags = [
@ -54,6 +56,7 @@ module.exports = function(RED) {
node.warn("No configuration node"); node.warn("No configuration node");
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
@ -66,24 +69,27 @@ module.exports = function(RED) {
}); });
node.on("input", function (msg) { node.on("input", function (msg) {
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;
}
let msgType = node.messageType, let msgType = node.messageType,
msgFormat = node.messageFormat; msgFormat = node.messageFormat,
threadReply = getToValue(msg, node.threadReplyType, node.threadReplyValue);
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");
return;
}
msgType = msg.type;
}
if(msgFormat === 'msg.format') {
if(!msg.format) {
node.error("Message format is set to be passed in via msg.format but was not defined");
return;
}
msgFormat = msg.format;
}
if (!node.server || !node.server.matrixClient) { if (!node.server || !node.server.matrixClient) {
node.warn("No matrix server selected"); node.warn("No matrix server selected");
@ -91,7 +97,7 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return; return;
} }
@ -104,11 +110,31 @@ module.exports = function(RED) {
let payload = n.message || msg.payload; let payload = n.message || msg.payload;
if(!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; return;
} }
let content = { let content = null;
if(typeof payload === 'object') {
content = payload;
} else {
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", msg);
return;
}
msgType = msg.type;
}
if(msgFormat === 'msg.format') {
if(!msg.format) {
node.error("Message format is set to be passed in via msg.format but was not defined", msg);
return;
}
msgFormat = msg.format;
}
content = {
msgtype: msgType, msgtype: msgType,
body: payload.toString() body: payload.toString()
}; };
@ -138,20 +164,42 @@ module.exports = function(RED) {
event_id: msg.eventId event_id: msg.eventId
}; };
content['body'] = ' * ' + content['body']; content['body'] = ' * ' + content['body'];
} else if(threadReply) {
// 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
let threadParent = (msg?.content?.['m.relates_to']?.rel_type === RelationType.Thread ? msg?.content?.['m.relates_to']?.event_id : null) || msg.eventId;
if(threadParent) {
content["m.relates_to"] = {
"rel_type": RelationType.Thread,
"event_id": threadParent,
"is_falling_back": true,
};
if(msg.eventId !== threadParent) {
content["m.relates_to"]["m.in_reply_to"] = {
"event_id": msg.eventId
};
}
}
}
} }
node.server.matrixClient.sendMessage(msg.topic, content) node.server.matrixClient.sendMessage(msg.topic, content)
.then(function(e) { .then(function(e) {
node.log("Message sent: " + payload); node.log("Message sent: " + payload);
msg.eventId = e.event_id; msg.eventId = e.event_id;
node.log(JSON.stringify(e));
node.send([msg, null]); node.send([msg, null]);
}) })
.catch(function(e){ .catch(function(e){
node.warn("Error sending message " + e); node.error("Error sending message: " + e, {});
msg.error = e; msg.error = e;
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-send-message", MatrixSendImage); RED.nodes.registerType("matrix-send-message", MatrixSendImage);
} }

View File

@ -1,3 +1,5 @@
const {RelationType, TimelineWindow} = require("matrix-js-sdk");
global.Olm = require('olm'); global.Olm = require('olm');
const fs = require("fs-extra"); const fs = require("fs-extra");
const sdk = require("matrix-js-sdk"); const sdk = require("matrix-js-sdk");
@ -44,6 +46,7 @@ module.exports = function(RED) {
this.credentials = {}; this.credentials = {};
} }
this.users = {};
this.connected = null; this.connected = null;
this.name = n.name; this.name = n.name;
this.userId = this.credentials.userId; this.userId = this.credentials.userId;
@ -52,9 +55,17 @@ module.exports = function(RED) {
this.url = this.credentials.url; this.url = this.credentials.url;
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.initializedAt = new Date(); this.initializedAt = new Date();
node.initialSyncLimit = 25;
// 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) { if(!this.userId) {
node.log("Matrix connection failed: missing user ID in configuration."); node.log("Matrix connection failed: missing user ID in configuration.");
@ -68,9 +79,9 @@ module.exports = function(RED) {
let retryStartTimeout = null; let retryStartTimeout = null;
if(!this.credentials.accessToken) { 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) { } 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 = async function(connected, cb) {
if (node.connected !== connected) { if (node.connected !== connected) {
@ -88,7 +99,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})`);
@ -107,13 +118,13 @@ module.exports = function(RED) {
}).then( }).then(
function(response) {}, function(response) {},
function(error) { function(error) {
node.error("Failed to set device label: " + error); node.error("Failed to set device label: " + error, {});
} }
); );
} }
}, },
function(error) { function(error) {
node.error("Failed to fetch device: " + error); node.error("Failed to fetch device: " + error, {});
} }
); );
} }
@ -147,6 +158,8 @@ module.exports = function(RED) {
// verificationMethods: ["m.sas.v1"] // verificationMethods: ["m.sas.v1"]
}); });
node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`);
// set globally if configured to do so // set globally if configured to do so
if(this.globalAccess) { if(this.globalAccess) {
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient); this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
@ -165,6 +178,13 @@ module.exports = function(RED) {
node.on('close', function(done) { node.on('close', function(done) {
stopClient(); stopClient();
if(node.globalAccess) {
try {
node.context().global.set('matrixClient["'+node.userId+'"]', undefined);
} catch(e){
node.error(e.message, {});
}
}
done(); done();
}); });
@ -174,22 +194,22 @@ module.exports = function(RED) {
node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) { node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) { 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 return; // ignore paginated results
} }
if (!event.getSender() || event.getSender() === node.userId) {
return; // ignore our own messages
}
if (!data || !data.liveEvent) { 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) return; // ignore old message (we only want live events)
} }
if(node.initializedAt > event.getDate()) { 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 return; // skip events that occurred before our client initialized
} }
try { try {
await node.matrixClient.decryptEventIfNeeded(event); await node.matrixClient.decryptEventIfNeeded(event);
} catch (error) { } catch (error) {
node.error(error); node.error(error, {});
return; return;
} }
@ -215,13 +235,23 @@ module.exports = function(RED) {
type : (event.getContent()['msgtype'] || event.getType()) || null, type : (event.getContent()['msgtype'] || event.getType()) || null,
payload : (event.getContent()['body'] || event.getContent()) || null, payload : (event.getContent()['body'] || event.getContent()) || null,
isDM : isDmRoom(room), isDM : isDmRoom(room),
isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread,
mentions : event.getContent()["m.mentions"] || null,
userId : event.getSender(), userId : event.getSender(),
user : node.matrixClient.getUser(event.getSender()),
topic : event.getRoomId(), topic : event.getRoomId(),
eventId : event.getId(), eventId : event.getId(),
event : event event : event,
}; };
node.log("Received" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getSender() + " :: " + msg.content.body + (toStartOfTimeline ? ' [PAGINATED]' : '')); // 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.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg); node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
}); });
@ -260,7 +290,6 @@ module.exports = function(RED) {
if (member.membership === "invite" && member.userId === node.userId) { if (member.membership === "invite" && member.userId === node.userId) {
node.log("Got invite to join room " + member.roomId); node.log("Got invite to join room " + member.roomId);
console.log(event);
if(node.autoAcceptRoomInvites) { if(node.autoAcceptRoomInvites) {
node.matrixClient.joinRoom(member.roomId).then(function() { node.matrixClient.joinRoom(member.roomId).then(function() {
node.log("Automatically accepted invitation to join room " + member.roomId); node.log("Automatically accepted invitation to join room " + member.roomId);
@ -292,7 +321,7 @@ module.exports = function(RED) {
} else if(prevState === null && state === "ERROR") { } else if(prevState === null && state === "ERROR") {
// Occurs when the initial sync failed first time. // Occurs when the initial sync failed first time.
node.setConnected(false, function(){ 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") { } else if(prevState === "ERROR" && state === "PREPARED") {
// Occurs when the initial sync succeeds // Occurs when the initial sync succeeds
@ -309,18 +338,18 @@ module.exports = function(RED) {
} else if(prevState === "SYNCING" && state === "RECONNECTING") { } else if(prevState === "SYNCING" && state === "RECONNECTING") {
// Occurs when the live update fails. // Occurs when the live update fails.
node.setConnected(false, function(){ node.setConnected(false, function(){
node.error("Connection to Matrix server lost"); node.error("Connection to Matrix server lost", {});
}); });
} else if(prevState === "RECONNECTING" && state === "RECONNECTING") { } else if(prevState === "RECONNECTING" && state === "RECONNECTING") {
// Can occur if the update calls continue to fail, // Can occur if the update calls continue to fail,
// but the keepalive calls (to /versions) succeed. // but the keepalive calls (to /versions) succeed.
node.setConnected(false, function(){ node.setConnected(false, function(){
node.error("Connection to Matrix server lost"); node.error("Connection to Matrix server lost", {});
}); });
} else if(prevState === "RECONNECTING" && state === "ERROR") { } else if(prevState === "RECONNECTING" && state === "ERROR") {
// Occurs when the keepalive call also fails // Occurs when the keepalive call also fails
node.setConnected(false, function(){ node.setConnected(false, function(){
node.error("Connection to Matrix server lost"); node.error("Connection to Matrix server lost", {});
}); });
} else if(prevState === "ERROR" && state === "SYNCING") { } else if(prevState === "ERROR" && state === "SYNCING") {
// Occurs when the client has performed a // Occurs when the client has performed a
@ -332,7 +361,7 @@ module.exports = function(RED) {
// Occurs when the client has failed to // Occurs when the client has failed to
// keepalive for a second time or more. // keepalive for a second time or more.
node.setConnected(false, function(){ node.setConnected(false, function(){
node.error("Connection to Matrix server lost"); node.error("Connection to Matrix server lost", {});
}); });
} else if(prevState === "SYNCING" && state === "SYNCING") { } else if(prevState === "SYNCING" && state === "SYNCING") {
// Occurs when the client has performed a live update. // Occurs when the client has performed a live update.
@ -344,7 +373,7 @@ module.exports = function(RED) {
// Occurs once the client has stopped syncing or // Occurs once the client has stopped syncing or
// trying to sync after stopClient has been called. // trying to sync after stopClient has been called.
node.setConnected(false, function(){ node.setConnected(false, function(){
node.error("Connection to Matrix server lost"); node.error("Connection to Matrix server lost", {});
}); });
} }
}); });
@ -362,7 +391,7 @@ module.exports = function(RED) {
// httpStatus: 401 // httpStatus: 401
// } // }
node.error("Authentication failure: " + errorObj); node.error("Authentication failure: " + errorObj, {});
stopClient(); stopClient();
}); });
@ -371,14 +400,14 @@ module.exports = function(RED) {
if(node.e2ee){ if(node.e2ee){
node.log("Initializing crypto..."); node.log("Initializing crypto...");
await node.matrixClient.initCrypto(); await node.matrixClient.initCrypto();
node.matrixClient.setGlobalErrorOnUnknownDevices(false); node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices
} }
node.log("Connecting to Matrix server..."); node.log("Connecting to Matrix server...");
await node.matrixClient.startClient({ await node.matrixClient.startClient({
initialSyncLimit: 8 initialSyncLimit: node.initialSyncLimit
}); });
} catch(error) { } catch(error) {
node.error(error); node.error(error, {});
} }
} }
@ -399,7 +428,7 @@ module.exports = function(RED) {
.then( .then(
function(data) { function(data) {
if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) { 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('device_id' in data && data['device_id'] && !node.deviceId) {
// if we have no device_id configured lets use the one // if we have no device_id configured lets use the one
@ -409,7 +438,7 @@ module.exports = function(RED) {
// make sure our userId matches the access token's // make sure our userId matches the access token's
if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) { 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; return;
} }
run().catch((error) => node.error(error)); run().catch((error) => node.error(error));
@ -418,7 +447,7 @@ module.exports = function(RED) {
// if the error isn't authentication related retry in a little bit // if the error isn't authentication related retry in a little bit
if(err.code !== "M_UNKNOWN_TOKEN") { if(err.code !== "M_UNKNOWN_TOKEN") {
retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000); retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000);
node.error("Auth check failed: " + err); node.error("Auth check failed: " + err, {});
} }
} }
) )
@ -449,13 +478,21 @@ module.exports = function(RED) {
const matrixClient = sdk.createClient({ const matrixClient = sdk.createClient({
baseUrl: baseUrl, baseUrl: baseUrl,
deviceId: deviceId, deviceId: deviceId,
timelineSupport: true,
localTimeoutMs: '30000', localTimeoutMs: '30000',
request request
}); });
new TimelineWindow()
matrixClient.timelineSupport = true;
matrixClient.login( matrixClient.login(
'm.login.password', { 'm.login.password', {
identifier: {
type: 'm.id.user',
user: userId, user: userId,
},
password: password, password: password,
initial_device_display_name: displayName initial_device_display_name: displayName
}) })
@ -494,7 +531,7 @@ module.exports = function(RED) {
fs.copySync(oldStorageDir, dir); fs.copySync(oldStorageDir, dir);
} }
} catch (err) { } catch (err) {
node.error(err); node.error(err, {});
} }
}); });

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
}, },
label: function() { label: function() {
return this.name || "Synapse Add/Edit User"; return this.name || "Synapse Add/Edit User";

View File

@ -8,9 +8,10 @@ module.exports = function(RED) {
this.server = RED.nodes.getNode(n.server); this.server = RED.nodes.getNode(n.server);
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
node.server.register(node);
this.encodeUri = function(pathTemplate, variables) { this.encodeUri = function(pathTemplate, variables) {
for (const key in variables) { for (const key in variables) {
@ -41,12 +42,13 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
if(!msg.userId) { 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; return;
} }
@ -69,6 +71,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser); RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser);
} }

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
}, },
label: function() { label: function() {
return this.name || "Synapse Deactivate User"; return this.name || "Synapse Deactivate User";

View File

@ -8,9 +8,10 @@ module.exports = function(RED) {
this.server = RED.nodes.getNode(n.server); this.server = RED.nodes.getNode(n.server);
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
node.server.register(node);
this.encodeUri = function(pathTemplate, variables) { this.encodeUri = function(pathTemplate, variables) {
for (const key in variables) { for (const key in variables) {
@ -41,12 +42,13 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
if(!msg.userId) { 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; return;
} }
@ -70,6 +72,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-synapse-deactivate-user", MatrixSynapseDeactivateUser); RED.nodes.registerType("matrix-synapse-deactivate-user", MatrixSynapseDeactivateUser);
} }

View File

@ -8,7 +8,7 @@
outputs: 2, outputs: 2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
roomId: { value: null }, roomId: { value: null },
}, },
label: function() { label: function() {

View File

@ -9,10 +9,9 @@ module.exports = function(RED) {
this.roomId = n.roomId; this.roomId = n.roomId;
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
this.encodeUri = function(pathTemplate, variables) { this.encodeUri = function(pathTemplate, variables) {
for (const key in variables) { for (const key in variables) {
if (!variables.hasOwnProperty(key)) { if (!variables.hasOwnProperty(key)) {
@ -42,18 +41,19 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
msg.topic = node.roomId || msg.topic; msg.topic = node.roomId || msg.topic;
if(!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; return;
} }
if(!msg.userId) { 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; return;
} }
@ -77,6 +77,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-synapse-join-room", MatrixJoinRoom); RED.nodes.registerType("matrix-synapse-join-room", MatrixJoinRoom);
} }

View File

@ -12,12 +12,12 @@ module.exports = function(RED) {
this.sharedSecret = this.credentials.sharedSecret; this.sharedSecret = this.credentials.sharedSecret;
if(!this.server) { if(!this.server) {
node.error('Server URL must be configured on the node.'); node.error('Server URL must be configured on the node.', {});
return; return;
} }
if(!this.sharedSecret) { 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; return;
} }
@ -25,12 +25,12 @@ module.exports = function(RED) {
const { got } = await import('got'); const { got } = await import('got');
if(!msg.payload.username) { if(!msg.payload.username) {
node.error("msg.payload.username is required"); node.error("msg.payload.username is required", msg);
return; return;
} }
if(!msg.payload.password) { if(!msg.payload.password) {
node.error("msg.payload.password is required"); node.error("msg.payload.password is required", msg);
return; return;
} }
@ -50,7 +50,7 @@ module.exports = function(RED) {
var nonce = response.body.nonce; var nonce = response.body.nonce;
if(!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; return;
} }
@ -96,6 +96,10 @@ module.exports = function(RED) {
node.send(msg); node.send(msg);
})(); })();
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-synapse-register", MatrixSynapseRegister, { RED.nodes.registerType("matrix-synapse-register", MatrixSynapseRegister, {
credentials: { credentials: {

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" } server: { type: "matrix-server-config" }
}, },
label: function() { label: function() {
return this.name || "Synapse User List"; return this.name || "Synapse User List";

View File

@ -12,6 +12,8 @@ module.exports = function(RED) {
return; return;
} }
node.server.register(node);
node.status({ fill: "red", shape: "ring", text: "disconnected" }); node.status({ fill: "red", shape: "ring", text: "disconnected" });
node.server.on("disconnected", function(){ node.server.on("disconnected", function(){
@ -29,8 +31,9 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
let queryParams = { let queryParams = {
@ -62,6 +65,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-synapse-users", MatrixSynapseUsers); RED.nodes.registerType("matrix-synapse-users", MatrixSynapseUsers);
} }

114
src/matrix-typing.html Normal file
View File

@ -0,0 +1,114 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-typing', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["success", "error"],
inputs: 1,
outputs: 2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
roomType: { value: "msg" },
roomValue: { value: "topic" },
typingType: { value: "bool" },
typingValue: { value: "true" },
timeoutMsType: { value: "num" },
timeoutMsValue: { value: 20000 },
},
label: function() {
return this.name || "Typing";
},
oneditprepare: function() {
$("#node-input-room").typedInput({
type: this.roomType,
types:['msg','flow','global','str'],
})
.typedInput('value', this.roomValue)
.typedInput('type', this.roomType);
$("#node-input-typing").typedInput({
types:['msg','flow','global','bool'],
})
.typedInput('value', this.typingValue)
.typedInput('type', this.typingType);
$("#node-input-timeoutMs").typedInput({
types:['msg','flow','global','num'],
})
.typedInput('value', this.timeoutMsValue)
.typedInput('type', this.timeoutMsType);
},
oneditsave: function() {
this.roomType = $("#node-input-room").typedInput('type');
this.roomValue = $("#node-input-room").typedInput('value');
this.typingType = $("#node-input-typing").typedInput('type');
this.typingValue = $("#node-input-typing").typedInput('value');
this.timeoutMsType = $("#node-input-timeoutMs").typedInput('type');
this.timeoutMsValue = $("#node-input-timeoutMs").typedInput('value');
},
paletteLabel: 'Typing'
});
</script>
<script type="text/html" data-template-name="matrix-typing">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
<input type="text" id="node-input-room">
</div>
<div class="form-row">
<label for="node-input-typing"><i class="fa fa-commenting-o"></i> Is Typing</label>
<input type="text" id="node-input-typing">
</div>
<div class="form-row">
<label for="node-input-timeoutMs"><i class="fa fa-clock-o"></i> Timeout Milliseconds</label>
<input type="text" id="node-input-timeoutMs">
</div>
<div class="form-row form-tips">
Timeout Milliseconds is how many milliseconds the server should show the user typing for. Ignored if setting typing to false.
</div>
</script>
<script type="text/html" data-help-name="matrix-typing">
<h3>Details</h3>
<p>
Sends typing event to a room
</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>dynamic
<span class="property-type">any</span>
</dt>
<dd> The inputs are configurable on the node.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dd>Returns from first output on success</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
</script>

88
src/matrix-typing.js Normal file
View File

@ -0,0 +1,88 @@
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);
node.send([null, 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');
} else if(type === "num") {
value = Number(property);
}
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;
}
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);
}

163
src/matrix-upload-file.html Normal file
View File

@ -0,0 +1,163 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-upload-file',{
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["success", "error"],
inputs:1,
outputs:2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
inputType: { value: "msg" },
inputValue: { value: "payload" },
fileNameType: { value: "msg" },
fileNameValue: { value: "filename" },
contentType: { value: null },
generateThumbnails: { type: "checkbox", value: true },
},
label: function() {
return this.name || "Upload File";
},
oneditprepare: function() {
$("#node-input-input").typedInput({
type: this.inputType,
types:['msg','flow','global','str'],
}).typedInput('value', this.inputValue);
$("#node-input-file-name").typedInput({
type: this.fileNameType,
types:['msg','flow','global','str'],
}).typedInput('value', this.fileNameValue);
},
oneditsave: function() {
this.inputType = $("#node-input-input").typedInput('type');
this.inputValue = $("#node-input-input").typedInput('value');
this.fileNameType = $("#node-input-file-name").typedInput('type');
this.fileNameValue = $("#node-input-file-name").typedInput('value');
},
paletteLabel: 'Upload File'
});
</script>
<script type="text/html" data-template-name="matrix-upload-file">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-input"><i class="fa fa-file"></i> File Input</label>
<input type="text" id="node-input-input">
</div>
<div class="form-tips" style="margin-bottom: 12px;">
Must be a buffer or string. If it is a string it assumes it is a path to a file on the filesystem.
</div>
<div class="form-row">
<label for="node-input-file-name"><i class="fa fa-file"></i> File Name</label>
<input type="text" id="node-input-file-name">
</div>
<div class="form-tips" style="margin-bottom: 12px;">
Name to give file on remote server. Required if file input is a buffer otherwise it uses the original file name.
</div>
<div class="form-row">
<label for="node-input-contentType"><i class="fa fa-user"></i> Content-Type</label>
<input type="text" id="node-input-contentType" placeholder="msg.contentType">
</div>
<div class="form-tips" style="margin-bottom: 12px;">
Must be a valid <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" target="_blank">MIME Type</a> (ex: application/pdf) or left undefined to auto detect
</div>
<div class="form-row">
<input
type="checkbox"
id="node-input-generateThumbnails"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-input-generateThumbnails" style="width: auto;max-width:50%;">
Generate m.video thumbnails using ffmpeg
</label>
</div>
<div class="form-tips">
ffmpeg & ffprobe must be installed for thumbnail generation, calculating duration of video or audio file, and getting width & height for videos. This functionality is disabled otherwise.
</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 type="text/html" data-help-name="matrix-upload-file">
<h3>Details</h3>
<p>Upload a file to the matrix homeserver. Returns a payload that can then be chained to a Send Message node to post the file to a room, or you can use the mxc_url to update a user or room avatar.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>msg.payload
<span class="property-type">string | Buffer</span>
</dt>
<dd> If this is not a buffer it assumes it is a path to the file. </dd>
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> Room ID to send file to. Ignored if configured on the node, otherwise required.</dd>
<dt class="optional">msg.filename
<span class="property-type">string | null</span>
</dt>
<dd> name of the file to upload. You REALLY should pass this if you want the file type to be detected correctly. If no filename is provided it will be auto-detected but note that this is not perfect (for example: .doc word files are detected incorrectly as application/x-cfb) and only works with binary files.</dd>
<dt class="optional">msg.body
<span class="property-type">string | null</span>
</dt>
<dd> this will be the display name the client will see when rendered in their chat client. If this is left empty it will just display as "Attachment".</dd>
<dt class="optional">msg.contentType
<span class="property-type">string | null</span>
</dt>
<dd> Content <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" target="_blank">MIME Type</a>. If set this forces the mime type. If you do not provide this it will try to be auto-detected.</dd>
<dt class="optional">msg.content
<span class="property-type">object | null</span>
</dt>
<dd> craft your own msg.content to send to the server. If defined then <code>msg.filename</code>, <code>msg.contentType</code>, <code>msg.body</code>, and <code>msg.payload</code> aren't required since the file already exists.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.eventId <span class="property-type">string</span></dt>
<dd>the eventId from the posted message.</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
<h3>References</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">MIME Types</a> - description of <code>msg.contentType</code> format</li>
</ul>
</script>

534
src/matrix-upload-file.js Normal file
View File

@ -0,0 +1,534 @@
const crypto = require("isomorphic-webcrypto");
const ffmpeg = require('fluent-ffmpeg');
const sharp = require('sharp');
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, msg) {
const Mime = require('mime');
let file = Buffer.isBuffer(bufferOrPath) ? filename : bufferOrPath;
try {
if(file) {
let type = Mime.getType(file);
let ext = Mime.getExtension(file);
if(type) {
return {ext: ext, mime: type};
}
}
} catch (error) {
node.error(`Error detecting file type for ${filename}: ${error.message}`, msg);
}
return null;
}
function getFileBuffer(data, msg) {
try {
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);
} catch (error) {
node.error(`Error reading file buffer: ${error.message}`, msg);
throw error;
}
}
function getToValue(msg, type, property) {
let value = property;
try {
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);
}
} catch (error) {
node.error(`Error evaluating value for type ${type}: ${error.message}`);
throw new Error("Invalid value evaluation");
}
return value;
}
node.on("input", onInput);
async function onInput(msg) {
try {
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, msg);
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, msg));
}
node.log("Uploading file ", msg);
let file;
try {
file = await node.server.matrixClient.uploadContent(
encryptedFile?.data || getFileBuffer(bufferOrPath, msg),
{
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);
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) {
try {
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;
} catch (error) {
node.error(`Error creating temp file: ${error.message}`, msg);
throw error;
}
}
function deleteTempFile() {
if (!tempFile) return null;
try {
fs.rmSync(tempFile);
} catch (error) {
node.error(`Error deleting temp file: ${error.message}`, msg);
}
}
// get size of a buffer or file in bytes
function getFileSize(bufferOrPath) {
try {
if (Buffer.isBuffer(bufferOrPath)) {
return Buffer.byteLength(bufferOrPath);
}
return fs.statSync(bufferOrPath).size;
} catch (error) {
node.error(`Error getting file size: ${error.message}`, msg);
throw error;
}
}
async function addThumbnail(buffer) {
try {
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;
}
} catch (error) {
node.error(`Error adding thumbnail: ${error.message}`, msg);
}
}
async function createImageThumbnail(bufferOrPath) {
try {
if (Buffer.isBuffer(bufferOrPath)) {
return sharp(bufferOrPath).resize({width: 320}).toBuffer();
}
return sharp(fs.readFileSync(bufferOrPath)).resize({width: 320}).toBuffer();
} catch (error) {
node.error(`Error creating image thumbnail: ${error.message}`);
throw error;
}
}
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, msg);
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(bufferOrPath);
msg.payload.info.h = imageSize.height;
msg.payload.info.w = imageSize.width;
// Generate thumbnail for image
if (node.generateThumbnails) {
let thumbnailBuffer = await createImageThumbnail(bufferOrPath);
let encryptedThumbnail = null;
if (msg.encrypted) {
encryptedThumbnail = await encryptAttachment(thumbnailBuffer);
msg.payload.info.thumbnail_file = encryptedThumbnail.info;
}
await addThumbnail(encryptedThumbnail || thumbnailBuffer);
}
} catch (e) {
node.error("thumbnail error: " + 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, msg);
}
if (node.generateThumbnails) {
try {
await _ffmpegVideoThumbnail(filepath);
} catch (e) {
node.error("Screenshot generation error: " + e, msg);
}
}
deleteTempFile();
}
node.send(msg, null);
} catch (error) {
node.error(`Unhandled error: ${error.message}`, msg);
node.log(error, msg);
}
}
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);
});
});
}
}

View File

@ -0,0 +1,329 @@
<script type="text/html" data-template-name="matrix-user-settings">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
</div>
<div class="form-row node-input-rule-container-row">
<ol id="node-input-rule-container"></ol>
</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 type="text/html" data-help-name="matrix-user-settings">
<h3>Details</h3>
<p>
Set and get the current user's display name or avatar.
</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">dynamic
<span class="property-type">string|object</span>
</dt>
<dd> The properties used to set the avatar_url or display name are configured on the node.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg <span class="property-type">object</span></dt>
<dd>Original message object with modifications based on config.</dd>
</dl>
<dl class="message-properties">
<dt>msg.setter_errors <span class="property-type">undefined|object</span></dt>
<dd>Returned if saving a setting failed.</dd>
</dl>
<dl class="message-properties">
<dt>msg.getter_errors <span class="property-type">undefined|object</span></dt>
<dd>Returned if there is an error thrown getting a user setting</dd>
</dl>
<dt class="optional">dynamic
<span class="property-type">string|object</span>
</dt>
<dd> You configure what user settings to output in the node configuration.</dd>
</li>
</ol>
</script>
<script type="text/javascript">
(function(){
var userSettingOptions = [
{ value: "display_name", label: "Display name" },
{ value: "avatar_url", label: "Avatar URL" },
];
var defaultRules = [{
t: "set",
p: userSettingOptions[0].value,
to: "payload",
tot: "msg"
}];
function isInvalidProperty(v,vt) {
if (/msg|flow|global/.test(vt)) {
if (!RED.utils.validatePropertyExpression(v)) {
return "Invalid property: " + v;
}
} else if (vt === "jsonata") {
try{ jsonata(v); } catch(e) {
return "Invalid expression: " + e.message;
}
} else if (vt === "json") {
try{ JSON.parse(v); } catch(e) {
return "Invalid JSON data: " + e.message;
}
}
return false;
}
RED.nodes.registerType('matrix-user-settings',{
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["success", "error"],
inputs:1,
outputs:2,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
roomId: { value: null },
rules: {
value: defaultRules,
validate: function(rules, opt) {
let msg;
const errors = []
if (!rules || rules.length === 0) { return true }
for (let i=0;i<rules.length;i++) {
const opt = { label: "Rule"+' '+(i+1) }
const r = rules[i];
if (r.t === 'set' || r.t === 'get') {
if ((msg = isInvalidProperty(r.p,r.pt)) !== false) {
return msg;
}
if ((msg = isInvalidProperty(r.to,r.tot)) !== false) {
return msg;
}
}
}
return errors.length ? errors : true;
}
},
},
oneditprepare: function() {
var set = "Set";
var to = "to the value";
var toValueLabel = "to the property";
var search = this._("change.action.search");
var replace = this._("change.action.replace");
var regex = this._("change.label.regex");
function createPropertyValue(row2_1, row2_2, type, defaultType) {
var propValInput = $('<input/>',{class:"node-input-rule-property-value",type:"text"})
.appendTo(row2_1)
.typedInput({
default: defaultType || (type === 'set' ? 'str' : 'msg'),
types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])
});
var lsLabel = $('<label style="padding-left: 130px;"></label>').appendTo(row2_2);
var localStorageEl = $('<input type="checkbox" class="node-input-rule-property-localStorage" style="width: auto; margin: 0 6px 0 0">').appendTo(lsLabel);
$('<span>').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, localStorageEl];
}
$('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var rule = opt;
if (!rule.hasOwnProperty('t')) {
rule = {t:"set",p:userSettingOptions[0].value,to:"payload",tot:"msg"};
}
if (rule.t === "set" && !rule.tot) {
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
rule.to = rule.to.substring(4);
rule.tot = "msg";
} else {
rule.tot = "str";
}
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
let fragment = document.createDocumentFragment();
var row1 = $('<div/>',{style:"display:flex; align-items: center"}).appendTo(fragment);
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row4 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(fragment);
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width:110px; margin-right:10px;"}).appendTo(row1);
var selectOptions = [
{v:"set",l:"Set"},
{v:"get",l:"Get"}
];
for (var x=0; x<selectOptions.length; x++) {
selectField.append($("<option></option>").val(selectOptions[x].v).text(selectOptions[x].l));
}
var propertyName = $('<input/>',{class:"node-input-rule-property-name",type:"text"})
.appendTo(row1)
.typedInput({type:"property", types:[{
value: userSettingOptions[0]['value'],
options: userSettingOptions
}]})
.on('focus', function(evt){
// following is a fix so autocomplete will show list on focus
if(!evt.isTrigger) {
evt.stopPropagation();
$(this).trigger("keyup.red-ui-autoComplete");
}
}).on("keyup", function(evt) {
// following allows autocomplete to display even when backspace/delete is used
if (evt.keyCode === 8 || evt.keyCode === 46) {
evt.stopPropagation();
$(this).trigger("keyup.red-ui-autoComplete");
}
});
var row2_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row2);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(toValueLabel)
.appendTo(row2_1);
var row2_2 = $('<div/>', {style:"margin-top: 4px;"}).appendTo(row2);
var row3_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row3);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(search)
.appendTo(row3_1);
var row3_2 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(row3);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(replace)
.appendTo(row3_2);
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
.text(to)
.appendTo(row4);
let propertyValue = null;
let localStorageEl = null;
let fromValue = null;
let toValue = null;
selectField.on("change", function() {
var type = $(this).val();
if (propertyValue) {
propertyValue.typedInput('hide');
}
if (fromValue) {
fromValue.typedInput('hide');
}
if (toValue) {
toValue.typedInput('hide');
}
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','json','jsonata'] : ['msg', 'flow', 'global']));
}
propertyValue.typedInput('show');
row2.show();
if(type === 'get') {
localStorageEl.parent().show();
} else {
localStorageEl.parent().hide();
}
row3.hide();
row4.hide();
});
selectField.val(rule.t);
propertyName.val(rule.p);
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.parent().show();
} else {
localStorageEl.parent().hide();
}
}
selectField.change();
container[0].appendChild(fragment);
},
removable: true,
sortable: true
});
for (var i=0; i<this.rules.length; i++) {
var rule = this.rules[i];
$("#node-input-rule-container").editableList('addItem',rule);
}
},
oneditsave: function() {
var rules = $("#node-input-rule-container").editableList('items');
var node = this;
node.rules= [];
rules.each(function(i) {
var rule = $(this);
var type = rule.find(".node-input-rule-type").val();
var r = {
t:type,
p:rule.find(".node-input-rule-property-name").val(),
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);
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = size.height;
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
height += 16;
$("#node-input-rule-container").editableList('height',height);
},
label: function() {
return this.name || "User Settings";
},
paletteLabel: 'User Settings'
});
})();
</script>

184
src/matrix-user-settings.js Normal file
View File

@ -0,0 +1,184 @@
module.exports = function(RED) {
function MatrixUserSettings(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.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')) {
try {
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg);
} catch(e2) {
throw new Error("Invalid value evaluation");
}
} else if (rule.tot === 'date') {
value = Date.now();
} else if (rule.tot === 'jsonata') {
try {
value = RED.util.evaluateJSONataExpression(rule.to,msg);
} catch(e3) {
throw new Error("Invalid expression");
}
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 "display_name":
await node.server.matrixClient.setDisplayName(value);
break;
case "avatar_url":
await node.server.matrixClient.setAvatarUrl(typeof value === "string" ? 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 {
// normalize some simpler events for easier access
switch(rule.p) {
case "display_name":
value = (await node.server.matrixClient.getProfileInfo(node.server.matrixClient.getUserId(), 'displayname')).displayname || false;
break;
case "avatar_url":
value = (await node.server.matrixClient.getProfileInfo(node.server.matrixClient.getUserId(), 'avatar_url')).avatar_url || false;
break;
}
setToValue(value, rule);
cachedGetters[rule.p] = value;
} 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-user-settings", MatrixUserSettings);
}

View File

@ -8,7 +8,7 @@
outputs:2, outputs:2,
defaults: { defaults: {
name: { value: null }, name: { value: null },
server: { value: "", type: "matrix-server-config" }, server: { type: "matrix-server-config" },
}, },
label: function() { label: function() {
return this.name || "WhoIs User"; return this.name || "WhoIs User";

View File

@ -8,10 +8,12 @@ module.exports = function(RED) {
this.server = RED.nodes.getNode(n.server); this.server = RED.nodes.getNode(n.server);
if(!this.server) { if(!this.server) {
node.error('Server must be configured on the node.'); node.error('Server must be configured on the node.', {});
return; return;
} }
node.server.register(node);
this.encodeUri = function(pathTemplate, variables) { this.encodeUri = function(pathTemplate, variables) {
for (const key in variables) { for (const key in variables) {
if (!variables.hasOwnProperty(key)) { if (!variables.hasOwnProperty(key)) {
@ -41,12 +43,13 @@ module.exports = function(RED) {
} }
if(!node.server.isConnected()) { 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]); node.send([null, msg]);
return;
} }
if(!msg.userId) { 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; return;
} }
@ -70,6 +73,10 @@ module.exports = function(RED) {
node.send([null, msg]); node.send([null, msg]);
}); });
}); });
node.on("close", function() {
node.server.deregister(node);
});
} }
RED.nodes.registerType("matrix-whois-user", MatrixWhoIsUser); RED.nodes.registerType("matrix-whois-user", MatrixWhoIsUser);
} }