Compare commits

..

3 Commits

Author SHA1 Message Date
785e0cd7be - #100 add node for sending typing state to rooms
- fix global and flow variable in getters for Matrix Room States node
2023-10-22 04:32:06 -06:00
2e9633e113 - #97 msg.state_key is now allowed as an input to Room State Events node for events that support it (required for m.space.child and m.space.parent)
- #97 added support for m.room.history_visibility, m.room.server_acl, m.room.pinned_events, m.space.child, and m.space.parent
- #97 fix issue with checkbox being hidden on config page when adding new setters/getters on config page
2023-10-22 03:01:33 -06:00
1859696122 - #97 added option to fetch state event from local storage and fallback to server if necessary (allows for faster lookups and gives the full event object with information about when/who created it, etc)
- #97 remove num, bool, bin, and data from being options you can set to a state event (currently only objects and sometimes strings are allowed)
- Updated Leave Room node so it deletes the room from local storage
- Updated server config node so it deletes the matrix client from storage during shutdown (possibly solution to #94)
2023-10-22 00:29:12 -06:00
7 changed files with 323 additions and 70 deletions

View File

@ -39,7 +39,8 @@
"matrix-synapse-create-edit-user": "src/matrix-synapse-create-edit-user.js",
"matrix-synapse-deactivate-user": "src/matrix-synapse-deactivate-user.js",
"matrix-synapse-join-room": "src/matrix-synapse-join-room.js",
"matrix-whois-user": "src/matrix-whois-user.js"
"matrix-whois-user": "src/matrix-whois-user.js",
"matrix-typing": "src/matrix-typing.js"
}
},
"engines": {

View File

@ -44,6 +44,7 @@ module.exports = function(RED) {
try {
node.log("Leaving room " + msg.topic);
node.server.matrixClient.leave(msg.topic);
node.server.matrixClient.store.removeRoom(msg.topic);
node.send([msg, null]);
} catch(e) {
node.error("Failed to leave room " + msg.topic + ": " + e, msg);

View File

@ -49,6 +49,11 @@
<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>
@ -69,7 +74,7 @@
<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>)</dd>
<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>
@ -77,13 +82,18 @@
<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.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",
@ -156,15 +166,17 @@
.appendTo(row2_1)
.typedInput({
default: defaultType || (type === 'set' ? 'str' : 'msg'),
types: (type === 'set' ? ['msg','flow','global','str','num','bool','json','bin','date','jsonata'] : ['msg', 'flow', 'global'])
types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])
});
var dcLabel = $('<label style="padding-left: 130px;"></label>').appendTo(row2_2);
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];
return [propValInput, localStorageEl];
}
$('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({
@ -259,6 +271,7 @@
.appendTo(row4);
let propertyValue = null;
let localStorageEl = null;
let fromValue = null;
let toValue = null;
@ -277,12 +290,18 @@
if (!propertyValue) {
var parts = createPropertyValue(row2_1, row2_2, type);
propertyValue = parts[0];
localStorageEl = parts[1];
} else {
propertyValue.typedInput('types', (type === 'set' ? ['msg','flow','global','str','num','bool','json','bin','date','jsonata','env'] : ['msg', 'flow', 'global']));
propertyValue.typedInput('types', (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global']));
}
propertyValue.typedInput('show');
row2.show();
if(type === 'get') {
localStorageEl.parent().show();
} else {
localStorageEl.parent().hide();
}
row3.hide();
row4.hide();
});
@ -292,7 +311,14 @@
if (rule.t === "set" || rule.t === "get") {
var parts = createPropertyValue(row2_1, row2_2, rule.t, rule.tot);
propertyValue = parts[0];
localStorageEl = parts[1];
propertyValue.typedInput('value',rule.to);
localStorageEl.prop("checked", !!rule.ls);
if(rule.t === 'get') {
localStorageEl.parent().show();
} else {
localStorageEl.parent().hide();
}
}
selectField.change();
container[0].appendChild(fragment);
@ -319,6 +345,10 @@
to:rule.find(".node-input-rule-property-value").typedInput('value'),
tot:rule.find(".node-input-rule-property-value").typedInput('type')
};
if (r.t === "get" && rule.find(".node-input-rule-property-localStorage").prop("checked")) {
r.ls = true;
}
node.rules.push(r);
});
},

View File

@ -74,24 +74,19 @@ module.exports = function(RED) {
if (rule.tot === "msg") {
value = RED.util.getMessageProperty(msg,rule.to);
} else if ((rule.tot === 'flow') || (rule.tot === 'global')) {
RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg, (err,value) => {
if (err) {
throw new Error("Invalid value evaluation");
} else {
return value;
}
});
return
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') {
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
if (err) {
throw new Error("Invalid expression");
} else {
return value;
}
});
try {
value = RED.util.evaluateJSONataExpression(rule.to,msg);
} catch(e3) {
throw new Error("Invalid expression");
}
return;
}
return value;
@ -140,11 +135,11 @@ module.exports = function(RED) {
msg.topic,
"m.room.name",
typeof value === "string"
? { name: value }
? {name: value}
: value);
break;
case "m.room.topic":
if(typeof value === "string") {
if (typeof value === "string") {
await node.server.matrixClient.setRoomTopic(msg.topic, value);
} else {
await node.server.matrixClient.sendStateEvent(
@ -159,27 +154,7 @@ module.exports = function(RED) {
msg.topic,
"m.room.avatar",
typeof value === "string"
? { "url": value }
: value,
"");
break;
case "m.room.power_levels":
if(typeof value !== 'object') {
setterErrors[rule.p] = "m.room.power_levels content must be object";
} else {
await node.server.matrixClient.sendStateEvent(
msg.topic,
"m.room.power_levels",
value,
"");
}
break;
case "m.room.guest_access":
await node.server.matrixClient.sendStateEvent(
msg.topic,
"m.room.guest_access",
typeof value === "string"
? { "guest_access": value }
? {"url": value}
: value,
"");
break;
@ -205,15 +180,50 @@ module.exports = function(RED) {
"");
}
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] = "Custom event content must be 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;
}
@ -226,23 +236,33 @@ module.exports = function(RED) {
value = cachedGetters[rule.p];
} else {
try {
// we may want to fetch from local storage in the future, this is how to do that
// const room = this.getRoom(roomId);
// const ev = room.currentState.getStateEvents(EventType.RoomEncryption, "");
value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, "");
switch(rule.p) {
case "m.room.name":
value = value?.name
break;
case "m.room.topic":
value = value?.topic
break;
case "m.room.avatar":
value = value?.url
break;
case "m.room.guest_access":
value = value?.guest_access;
break;
if(rule.ls) {
// we opted to lookup from local storage, will fallback to server if necessary
let room = node.server.matrixClient.getRoom(msg.topic);
if(room) {
value = await room.getLiveTimeline().getState("f").getStateEvents(rule.p, "");
}
}
if(!value) {
// fetch the latest state event by type from server
value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, "");
if(value) {
// normalize some simpler events for easier access
switch(rule.p) {
case "m.room.name":
value = value?.name
break;
case "m.room.topic":
value = value?.topic
break;
case "m.room.avatar":
value = value?.url
break;
case "m.room.guest_access":
value = value?.guest_access;
break;
}
}
}
setToValue(value, rule);
} catch(e) {

View File

@ -174,6 +174,13 @@ module.exports = function(RED) {
node.on('close', function(done) {
stopClient();
if(node.globalAccess) {
try {
node.context().global.delete('matrixClient["'+node.userId+'"]');
} catch(e){
node.error(e.message, {});
}
}
done();
});

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

@ -0,0 +1,108 @@
<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);
$("#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-room"><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-room"><i class="fa fa-clock-o"></i> Timeout Milliseconds</label>
<input type="text" id="node-input-timeoutMs">
</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>

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

@ -0,0 +1,86 @@
module.exports = function(RED) {
function MatrixTyping(n) {
RED.nodes.createNode(this, n);
let node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
this.roomId = n.roomId;
this.roomType = n.roomType;
this.roomValue = n.roomValue;
this.typingType = n.typingType;
this.typingValue = n.typingValue;
this.timeoutMsType = n.timeoutMsType;
this.timeoutMsValue = n.timeoutMsValue;
node.status({ fill: "red", shape: "ring", text: "disconnected" });
if (!node.server) {
node.error("No configuration node", {});
return;
}
node.server.register(node);
node.server.on("disconnected", function(){
node.status({ fill: "red", shape: "ring", text: "disconnected" });
});
node.server.on("connected", function() {
node.status({ fill: "green", shape: "ring", text: "connected" });
});
node.on('input', async function(msg) {
if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected", msg);
return;
}
if(!node.server.isConnected()) {
node.error("Matrix server connection is currently closed", msg);
node.send([null, msg]);
return;
}
function getToValue(msg, type, property) {
let value = property;
if (type === "msg") {
value = RED.util.getMessageProperty(msg, property);
} else if ((type === 'flow') || (type === 'global')) {
try {
value = RED.util.evaluateNodeProperty(property, type, node, msg);
} catch(e2) {
throw new Error("Invalid value evaluation");
}
} else if(type === "bool") {
value = (property === 'true');
}
return value;
}
try {
let roomId = getToValue(msg, node.roomType, node.roomValue),
typing = getToValue(msg, node.typingType, node.typingValue),
timeoutMs = getToValue(msg, node.timeoutMsType, node.timeoutMsValue);
if(!roomId) {
node.error('No room provided in msg.topic', msg);
return;
}
console.log("sending typing",roomId, typing, timeoutMs);
await node.server.matrixClient.sendTyping(roomId, typing, timeoutMs);
node.send([msg, null]);
} catch(e) {
node.error("Failed to send typing event " + msg.topic + ": " + e, msg);
msg.payload = e;
node.send([null, msg]);
}
});
node.on("close", function() {
node.server.deregister(node);
});
}
RED.nodes.registerType("matrix-typing", MatrixTyping);
}