mirror of
https://github.com/Skylar-Tech/node-red-contrib-matrix-chat.git
synced 2025-07-08 11:41:07 -06:00
Compare commits
12 Commits
1d15b0c5bf
...
1337d25631
Author | SHA1 | Date | |
---|---|---|---|
1337d25631 | |||
86640a1d79 | |||
e0074ea715 | |||
db2c54d9ee | |||
51e649b4cf | |||
a08709265e | |||
57ba70db6c | |||
a3e1381d53 | |||
6dca3aa70e | |||
b36286d994 | |||
f14190d9ea | |||
000c28e3b8 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@ -24,11 +24,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-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",
|
||||||
@ -45,7 +49,8 @@
|
|||||||
"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-typing": "src/matrix-typing.js"
|
"matrix-device-verification": "src/matrix-device-verification.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 |
240
src/matrix-device-verification.html
Normal file
240
src/matrix-device-verification.html
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
let computeInputAndOutputCounts = function(node){
|
||||||
|
switch($("#node-input-mode").val()) {
|
||||||
|
default:
|
||||||
|
node.outputs = node.inputs = 0;
|
||||||
|
break;
|
||||||
|
case 'receive':
|
||||||
|
node.outputs = 1;
|
||||||
|
node.inputs = 0;
|
||||||
|
break;
|
||||||
|
case 'request':
|
||||||
|
case 'start':
|
||||||
|
case 'accept':
|
||||||
|
case 'cancel':
|
||||||
|
node.outputs = 2;
|
||||||
|
node.inputs = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RED.nodes.registerType('matrix-device-verification', {
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
inputs: 0,
|
||||||
|
outputs: 0,
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { value: "", type: "matrix-server-config" },
|
||||||
|
mode: { value: null, type: "text", required: true },
|
||||||
|
inputs: { value: 0 },
|
||||||
|
outputs: { value: 0 }
|
||||||
|
},
|
||||||
|
oneditprepare: function () {
|
||||||
|
computeInputAndOutputCounts(this);
|
||||||
|
},
|
||||||
|
oneditsave: function () {
|
||||||
|
computeInputAndOutputCounts(this);
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
if(this.name) {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(this.mode) {
|
||||||
|
default:
|
||||||
|
return 'Device Verification';
|
||||||
|
case 'receive':
|
||||||
|
return 'Receive Device Verification';
|
||||||
|
case 'request':
|
||||||
|
return 'Request Device Verification';
|
||||||
|
case 'start':
|
||||||
|
return 'Start Device Verification';
|
||||||
|
case 'accept':
|
||||||
|
return 'Accept Device Verification';
|
||||||
|
case 'cancel':
|
||||||
|
return 'Cancel Device Verification';
|
||||||
|
}
|
||||||
|
return this.name || "Device Verify Request";
|
||||||
|
},
|
||||||
|
paletteLabel: function(){
|
||||||
|
return "Device Verification";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-device-verification">
|
||||||
|
<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-mode"><i class="fa fa-user"></i> Mode</label>
|
||||||
|
<select id="node-input-mode" style="width:70%;">
|
||||||
|
<option value="">Unconfigured</option>
|
||||||
|
<option value="receive">Receive Verification Request</option>
|
||||||
|
<option value="request">Request Verification</option>
|
||||||
|
<option value="start">Verification Start</option>
|
||||||
|
<option value="accept">Verification Accept</option>
|
||||||
|
<option value="cancel">Verification Cancel</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-device-verification">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>
|
||||||
|
Handle device verification. Check out the <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme" target="_blank">examples</a> page for a good understanding of how this works.
|
||||||
|
<br />
|
||||||
|
General flow:
|
||||||
|
<ol>
|
||||||
|
<li>Request/Receive device verification</li>
|
||||||
|
<li>Start Verification</li>
|
||||||
|
<li>Compare Emojis</li>
|
||||||
|
<li>Accept/Cancel Verification</li>
|
||||||
|
</ol>
|
||||||
|
<br />
|
||||||
|
THIS NODE IS IN BETA. There is a good chance that we will change how this node works later down the road. Make sure to read the release notes before upgrading.
|
||||||
|
</p>
|
||||||
|
<a href="https://matrix-org.github.io/synapse/develop/admin_api/room_membership.html#edit-room-membership-api" target="_blank">Synapse API Endpoint Information</a>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<ul class="node-inputs">
|
||||||
|
<li><code>mode</code> set to '<strong>Receive Verification Request</strong>'
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Doesn't take an input
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><code>mode</code> set to '<strong>Request Verification</strong>'
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.userId <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
ID of the user to request device verification from
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.devices <span class="property-type">array[string]|null</span></dt>
|
||||||
|
<dd> list of <code>msg.userId</code>'s devices IDs to request verification from. If empty it will request from all known devices.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><code>mode</code> set to '<strong>Verification Start</strong>'
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
Internal ID to reference the verification request throughout the flows
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.cancel <span class="property-type">bool</span></dt>
|
||||||
|
<dd>
|
||||||
|
If set and is true the verification request will be cancelled
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><code>mode</code> set to '<strong>Verification Accept</strong>'
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
Internal ID to reference the verification request throughout the flows
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><code>mode</code> set to '<strong>Verification Cancel</strong>'
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
Internal ID to reference the verification request throughout the flows
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ul class="node-outputs">
|
||||||
|
<li><code>mode</code> set to '<strong>Receive Verification Request</strong>' or '<strong>Request Verification</strong>'
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
Internal ID to reference the verification request throughout the flows
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.verifyMethods <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
Common verification methods supported by both sides
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.userId <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
ID of the user to request device verification from
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.deviceIds <span class="property-type">array[string]</span></dt>
|
||||||
|
<dd>
|
||||||
|
List of devices we are verifying
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.selfVerification <span class="property-type">bool</span></dt>
|
||||||
|
<dd>
|
||||||
|
true if we are verifying one of our own devices
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.phase <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
what phase of verification we are in
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><code>mode</code> set to '<strong>Verification Start</strong>'
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.payload <span class="property-type">string</span></dt>
|
||||||
|
<dd>
|
||||||
|
sas verification payload
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.emojis <span class="property-type">array[string]</span></dt>
|
||||||
|
<dd>
|
||||||
|
array of emojis for verification request
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.emojis_text <span class="property-type">array[string]</span></dt>
|
||||||
|
<dd>
|
||||||
|
array of emojis in text form for verification request
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><code>mode</code> set to '<strong>Verification Accept</strong>' or '<strong>Verification Cancel</strong>'
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Passes input straight to output on success. If an error occurs it goes to the second output.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</script>
|
267
src/matrix-device-verification.js
Normal file
267
src/matrix-device-verification.js
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
const {Phase} = require("matrix-js-sdk/lib/crypto/verification/request/VerificationRequest");
|
||||||
|
const {CryptoEvent} = require("matrix-js-sdk/lib/crypto");
|
||||||
|
|
||||||
|
module.exports = function(RED) {
|
||||||
|
const verificationRequests = new Map();
|
||||||
|
|
||||||
|
function MatrixDeviceVerification(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.mode = n.mode;
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.warn("No configuration node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!node.server.e2ee) {
|
||||||
|
node.error("End-to-end encryption needs to be enabled to use this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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" });
|
||||||
|
});
|
||||||
|
|
||||||
|
function getKeyByValue(object, value) {
|
||||||
|
return Object.keys(object).find(key => object[key] === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(node.mode) {
|
||||||
|
default:
|
||||||
|
node.error("Node not configured with a mode");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'request':
|
||||||
|
node.on('input', async function(msg){
|
||||||
|
if(!msg.userId) {
|
||||||
|
node.error("msg.userId is required for start verification mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
node.server.matrixClient.requestVerification(msg.userId, msg.devices || null)
|
||||||
|
.then(function(e) {
|
||||||
|
node.log("Successfully requested verification", e);
|
||||||
|
let verifyRequestId = msg.userId + ':' + e.channel.deviceId;
|
||||||
|
verificationRequests.set(verifyRequestId, e);
|
||||||
|
node.send({
|
||||||
|
verifyRequestId: verifyRequestId, // internally used to reference between nodes
|
||||||
|
verifyMethods: e.methods,
|
||||||
|
userId: msg.userId,
|
||||||
|
deviceIds: e.channel.devices,
|
||||||
|
selfVerification: e.isSelfVerification,
|
||||||
|
phase: getKeyByValue(Phase, e.phase)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function(e){
|
||||||
|
node.warn("Error requesting device verification: " + e);
|
||||||
|
msg.error = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'receive':
|
||||||
|
/**
|
||||||
|
* Fires when a key verification is requested.
|
||||||
|
* @event module:client~MatrixClient#"crypto.verification.request"
|
||||||
|
* @param {object} data
|
||||||
|
* @param {MatrixEvent} data.event the original verification request message
|
||||||
|
* @param {Array} data.methods the verification methods that can be used
|
||||||
|
* @param {Number} data.timeout the amount of milliseconds that should be waited
|
||||||
|
* before cancelling the request automatically.
|
||||||
|
* @param {Function} data.beginKeyVerification a function to call if a key
|
||||||
|
* verification should be performed. The function takes one argument: the
|
||||||
|
* name of the key verification method (taken from data.methods) to use.
|
||||||
|
* @param {Function} data.cancel a function to call if the key verification is
|
||||||
|
* rejected.
|
||||||
|
*/
|
||||||
|
node.server.matrixClient.on(CryptoEvent.VerificationRequestReceived, async function(data){
|
||||||
|
if(data.phase === Phase.Cancelled || data.phase === Phase.Done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(data.requested || true) {
|
||||||
|
let verifyRequestId = data.targetDevice.userId + ':' + data.targetDevice.deviceId;
|
||||||
|
verificationRequests.set(verifyRequestId, data);
|
||||||
|
node.send({
|
||||||
|
verifyRequestId: verifyRequestId, // internally used to reference between nodes
|
||||||
|
verifyMethods: data.methods,
|
||||||
|
userId: data.targetDevice.userId,
|
||||||
|
deviceId: data.targetDevice.deviceId,
|
||||||
|
selfVerification: data.isSelfVerification,
|
||||||
|
phase: getKeyByValue(Phase, data.phase)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on('close', function(done) {
|
||||||
|
// clear verification requests
|
||||||
|
verificationRequests.clear();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'start':
|
||||||
|
node.on('input', async function(msg){
|
||||||
|
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
|
||||||
|
node.error("invalid verification request (invalid msg.verifyRequestId): " + (msg.verifyRequestId || null));
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = verificationRequests.get(msg.verifyRequestId);
|
||||||
|
if(msg.cancel) {
|
||||||
|
await data.verifier.cancel();
|
||||||
|
verificationRequests.delete(msg.verifyRequestId);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
data.on('change', async function() {
|
||||||
|
// VerificationPhase {
|
||||||
|
// /** Initial state: no event yet exchanged */
|
||||||
|
// Unsent = 1,
|
||||||
|
//
|
||||||
|
// /** An `m.key.verification.request` event has been sent or received */
|
||||||
|
// Requested = 2,
|
||||||
|
//
|
||||||
|
// /** An `m.key.verification.ready` event has been sent or received, indicating the verification request is accepted. */
|
||||||
|
// Ready = 3,
|
||||||
|
//
|
||||||
|
// /** An `m.key.verification.start` event has been sent or received, choosing a verification method */
|
||||||
|
// Started = 4,
|
||||||
|
//
|
||||||
|
// /** An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling the verification request */
|
||||||
|
// Cancelled = 5,
|
||||||
|
//
|
||||||
|
// /** An `m.key.verification.done` event has been **sent**, completing the verification request. */
|
||||||
|
// Done = 6,
|
||||||
|
// }
|
||||||
|
console.log("[Verification Start] VERIFIER EVENT CHANGE", this.phase);
|
||||||
|
var that = this;
|
||||||
|
if(this.phase === Phase.Started) {
|
||||||
|
console.log("[Verification Start] VERIFIER EVENT PHASE STARTED");
|
||||||
|
let verifierCancel = function(){
|
||||||
|
let verifyRequestId = that.targetDevice.userId + ':' + that.targetDevice.deviceId;
|
||||||
|
if(verificationRequests.has(verifyRequestId)) {
|
||||||
|
verificationRequests.delete(verifyRequestId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
data.verifier.on('cancel', function(e){
|
||||||
|
node.warn("Device verification cancelled " + e);
|
||||||
|
console.log(JSON.stringify(e.value));
|
||||||
|
verifierCancel();
|
||||||
|
});
|
||||||
|
const sasEventPromise = new Promise(resolve =>
|
||||||
|
data.verifier.once("show_sas", resolve)
|
||||||
|
);
|
||||||
|
console.log("[Verification Start] Starting verification");
|
||||||
|
data.verifier.verify()
|
||||||
|
.then(function() {
|
||||||
|
console.log("[Verification Start] verify() success");
|
||||||
|
}).catch(function(e) {
|
||||||
|
console.log("[Verification Start] verify() error", e);
|
||||||
|
msg.error = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
});
|
||||||
|
console.log("[Verification Start] WAITING FOR SHOW SAS EVENT");
|
||||||
|
const sasEvent = await sasEventPromise;
|
||||||
|
|
||||||
|
console.log("SHOW SAS", sasEvent);
|
||||||
|
// e = {
|
||||||
|
// sas: {
|
||||||
|
// decimal: [ 8641, 3153, 2357 ],
|
||||||
|
// emoji: [
|
||||||
|
// [Array], [Array],
|
||||||
|
// [Array], [Array],
|
||||||
|
// [Array], [Array],
|
||||||
|
// [Array]
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// confirm: [AsyncFunction: confirm],
|
||||||
|
// cancel: [Function: cancel],
|
||||||
|
// mismatch: [Function: mismatch]
|
||||||
|
// }
|
||||||
|
msg.payload = sasEvent.sas;
|
||||||
|
msg.emojis = sasEvent.sas.emoji.map(function(emoji, i) {
|
||||||
|
return emoji[0];
|
||||||
|
});
|
||||||
|
msg.emojis_text = sasEvent.sas.emoji.map(function(emoji, i) {
|
||||||
|
return emoji[1];
|
||||||
|
});
|
||||||
|
node.send(msg);
|
||||||
|
|
||||||
|
// sasEvent.mismatch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Verification Start] Starting verification");
|
||||||
|
try {
|
||||||
|
console.log("[Verification Start] Accepting..");
|
||||||
|
await data.accept();
|
||||||
|
console.log(`[Verification] beginKeyVerification (methods=${data.methods[0]}, targetDevice=${data.targetDevice})`);
|
||||||
|
await data.beginKeyVerification(
|
||||||
|
data.methods[0],
|
||||||
|
data.targetDevice
|
||||||
|
);
|
||||||
|
} catch(e) {
|
||||||
|
console.log("[Verification Start] VERIFICATION ERROR", e);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log("ERROR", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancel':
|
||||||
|
node.on('input', async function(msg){
|
||||||
|
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
|
||||||
|
node.error("Invalid verification request: " + (msg.verifyRequestId || null));
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = verificationRequests.get(msg.verifyRequestId);
|
||||||
|
if(data) {
|
||||||
|
data.cancel()
|
||||||
|
.then(function(e){
|
||||||
|
node.send([msg, null]);
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
msg.error = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'accept':
|
||||||
|
node.on('input', async function(msg){
|
||||||
|
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
|
||||||
|
node.error("Invalid verification request: " + (msg.verifyRequestId || null));
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = verificationRequests.get(msg.verifyRequestId);
|
||||||
|
if(data.verifier && data.verifier.sasEvent) {
|
||||||
|
try {
|
||||||
|
await data.verifier.sasEvent.confirm();
|
||||||
|
node.send([msg, null]);
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
msg.error = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.error("Verification must be started");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-device-verification", MatrixDeviceVerification);
|
||||||
|
}
|
352
src/matrix-get-user.html
Normal file
352
src/matrix-get-user.html
Normal 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
145
src/matrix-get-user.js
Normal 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);
|
||||||
|
}
|
254
src/matrix-mark-read.html
Normal file
254
src/matrix-mark-read.html
Normal 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
73
src/matrix-mark-read.js
Normal 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);
|
||||||
|
}
|
284
src/matrix-paginate-room.html
Normal file
284
src/matrix-paginate-room.html
Normal 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
155
src/matrix-paginate-room.js
Normal 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);
|
||||||
|
}
|
@ -10,6 +10,7 @@
|
|||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { 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"
|
||||||
|
@ -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;
|
||||||
@ -40,6 +42,11 @@ module.exports = function(RED) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!node.acceptOwnEvents && ( !event.getSender() || event.getSender() === node.userId ) ) {
|
||||||
|
node.log("Ignoring" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getId() + " for reason: own event");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch(msg.type) {
|
switch(msg.type) {
|
||||||
case 'm.emote':
|
case 'm.emote':
|
||||||
if(!node.acceptEmotes) return;
|
if(!node.acceptEmotes) return;
|
||||||
|
@ -8,9 +8,8 @@
|
|||||||
<input type="text" id="node-input-server">
|
<input type="text" id="node-input-server">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
|
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||||
<input type="text" id="node-input-roomId" placeholder="msg.topic">
|
<input type="text" id="node-input-room">
|
||||||
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
|
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
|
||||||
@ -18,18 +17,6 @@
|
|||||||
<div class="form-row node-input-rule-container-row">
|
<div class="form-row node-input-rule-container-row">
|
||||||
<ol id="node-input-rule-container"></ol>
|
<ol id="node-input-rule-container"></ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(function(){
|
|
||||||
$("#node-input-roomId").on("keyup", function() {
|
|
||||||
if($(this).val() && !$(this).val().startsWith("!")) {
|
|
||||||
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
|
|
||||||
} else {
|
|
||||||
$("#node-input-roomId-error").hide();
|
|
||||||
}
|
|
||||||
}).trigger('keyup');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/html" data-help-name="matrix-room-state-events">
|
<script type="text/html" data-help-name="matrix-room-state-events">
|
||||||
@ -129,8 +116,8 @@
|
|||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomType: { value: "msg" },
|
||||||
reason: { value: null },
|
roomValue: { value: "topic" },
|
||||||
rules: {
|
rules: {
|
||||||
value: defaultRules,
|
value: defaultRules,
|
||||||
validate: function(rules, opt) {
|
validate: function(rules, opt) {
|
||||||
@ -154,6 +141,13 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
oneditprepare: function() {
|
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 set = "Set";
|
||||||
var to = "to the value";
|
var to = "to the value";
|
||||||
var toValueLabel = "to the property";
|
var toValueLabel = "to the property";
|
||||||
@ -333,6 +327,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
oneditsave: function() {
|
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 rules = $("#node-input-rule-container").editableList('items');
|
||||||
var node = this;
|
var node = this;
|
||||||
node.rules= [];
|
node.rules= [];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
module.exports = function(RED) {
|
module.exports = function(RED) {
|
||||||
function MatrixRoomSettings(n) {
|
function MatrixRoomStateEvents(n) {
|
||||||
RED.nodes.createNode(this, n);
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
var node = this;
|
var node = this;
|
||||||
@ -265,6 +265,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setToValue(value, rule);
|
setToValue(value, rule);
|
||||||
|
cachedGetters[rule.p] = value;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
getterErrors[rule.p] = e;
|
getterErrors[rule.p] = e;
|
||||||
}
|
}
|
||||||
@ -288,5 +289,5 @@ module.exports = function(RED) {
|
|||||||
node.server.deregister(node);
|
node.server.deregister(node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-room-state-events", MatrixRoomSettings);
|
RED.nodes.registerType("matrix-room-state-events", MatrixRoomStateEvents);
|
||||||
}
|
}
|
@ -6,6 +6,7 @@
|
|||||||
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: { type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
@ -13,12 +14,24 @@
|
|||||||
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>
|
||||||
|
|
||||||
@ -72,6 +85,11 @@
|
|||||||
It's recommended to use m.notice for bots because the message will render in a lighter text (at least in Element client) for users to distinguish bot and real user messages.
|
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
|
||||||
|
@ -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 = [
|
||||||
@ -67,8 +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 (!node.server || !node.server.matrixClient) {
|
if (!node.server || !node.server.matrixClient) {
|
||||||
node.warn("No matrix server selected");
|
node.warn("No matrix server selected");
|
||||||
@ -143,6 +164,22 @@ 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +187,7 @@ module.exports = function(RED) {
|
|||||||
.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){
|
||||||
|
@ -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");
|
||||||
@ -50,12 +52,13 @@ module.exports = function(RED) {
|
|||||||
this.userId = this.credentials.userId;
|
this.userId = this.credentials.userId;
|
||||||
this.deviceLabel = this.credentials.deviceLabel || null;
|
this.deviceLabel = this.credentials.deviceLabel || null;
|
||||||
this.deviceId = this.credentials.deviceId || null;
|
this.deviceId = this.credentials.deviceId || null;
|
||||||
|
this.secretStoragePassphrase = null;
|
||||||
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
|
// Keep track of all consumers of this node to be able to catch errors
|
||||||
node.register = function(consumerNode) {
|
node.register = function(consumerNode) {
|
||||||
@ -64,12 +67,39 @@ module.exports = function(RED) {
|
|||||||
node.deregister = function(consumerNode) {
|
node.deregister = function(consumerNode) {
|
||||||
delete node.users[consumerNode.id];
|
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.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cryptoCallbacks = undefined;
|
||||||
|
if(node.e2ee) {
|
||||||
|
cryptoCallbacks = {
|
||||||
|
getSecretStorageKey: async ({ keys }) => {
|
||||||
|
return null; // we don't do secret storage right now
|
||||||
|
const backupPassphrase = node.secretStoragePassphrase;
|
||||||
|
if (!backupPassphrase) {
|
||||||
|
node.WARN("Missing secret storage key");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let keyId = await node.matrixClient.getDefaultSecretStorageKeyId();
|
||||||
|
if (keyId && !keys[keyId]) {
|
||||||
|
keyId = undefined;
|
||||||
|
}
|
||||||
|
if (!keyId) {
|
||||||
|
keyId = keys[0][0];
|
||||||
|
}
|
||||||
|
const backupInfo = await node.matrixClient.getKeyBackupVersion();
|
||||||
|
const key = await node.matrixClient.keyBackupKeyFromPassword(
|
||||||
|
backupPassphrase,
|
||||||
|
backupInfo
|
||||||
|
);
|
||||||
|
return [keyId, key];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId),
|
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId),
|
||||||
localStorage = new LocalStorage(localStorageDir),
|
localStorage = new LocalStorage(localStorageDir),
|
||||||
initialSetup = false;
|
initialSetup = false;
|
||||||
@ -81,6 +111,61 @@ module.exports = function(RED) {
|
|||||||
} else if(!this.url) {
|
} else if(!this.url) {
|
||||||
node.error("Matrix connection failed: missing server URL in configuration.", {});
|
node.error("Matrix connection failed: missing server URL in configuration.", {});
|
||||||
} else {
|
} else {
|
||||||
|
/**
|
||||||
|
* Ensures secret storage and cross signing are ready for use. Does not
|
||||||
|
* support initial setup of secret storage. If the backup passphrase is not
|
||||||
|
* set, this is a no-op, else it is cleared once the operation is complete.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function bootstrapSSSS() {
|
||||||
|
if (!node.matrixClient) {
|
||||||
|
// client startup will do bootstrapping
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const password = "testphrase";
|
||||||
|
if (!password) {
|
||||||
|
// We do not support setting up secret storage, so we need a passphrase
|
||||||
|
// to bootstrap.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const backupInfo = await node.matrixClient.getKeyBackupVersion();
|
||||||
|
await node.matrixClient.getCrypto().bootstrapSecretStorage({
|
||||||
|
setupNewKeyBackup: false,
|
||||||
|
async getKeyBackupPassphrase() {
|
||||||
|
const key = await node.matrixClient.keyBackupKeyFromPassword(
|
||||||
|
password,
|
||||||
|
backupInfo
|
||||||
|
);
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await node.matrixClient.getCrypto().bootstrapCrossSigning({
|
||||||
|
authUploadDeviceSigningKeys(makeRequest) {
|
||||||
|
console.log("authUploadDeviceSigningKeys");
|
||||||
|
makeRequest({
|
||||||
|
"type": "m.login.password",
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": node.matrixClient.getUserId()
|
||||||
|
},
|
||||||
|
"password": "roflmaox2",
|
||||||
|
"session": node.matrixClient.getSessionId()
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await node.matrixClient.checkOwnCrossSigningTrust();
|
||||||
|
if (backupInfo) {
|
||||||
|
await node.matrixClient.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||||
|
}
|
||||||
|
// Clear passphrase once bootstrap was successful
|
||||||
|
// this.imAccount.setString("backupPassphrase", "");
|
||||||
|
// this.imAccount.save();
|
||||||
|
// this._encryptionError = "";
|
||||||
|
// await this.updateEncryptionStatus();
|
||||||
|
}
|
||||||
|
|
||||||
node.setConnected = async function(connected, cb) {
|
node.setConnected = async function(connected, cb) {
|
||||||
if (node.connected !== connected) {
|
if (node.connected !== connected) {
|
||||||
node.connected = connected;
|
node.connected = connected;
|
||||||
@ -92,11 +177,19 @@ module.exports = function(RED) {
|
|||||||
node.log("Matrix server connection ready.");
|
node.log("Matrix server connection ready.");
|
||||||
node.emit("connected");
|
node.emit("connected");
|
||||||
if(!initialSetup) {
|
if(!initialSetup) {
|
||||||
|
console.log("INITIAL SETUP", await node.matrixClient.getCrypto().getCrossSigningStatus());
|
||||||
|
if(node.e2ee && !await node.matrixClient.getCrypto().isCrossSigningReady()) {
|
||||||
|
// bootstrap cross-signing
|
||||||
|
await bootstrapSSSS();
|
||||||
|
let crossSigningStatus = node.matrixClient.getCrypto().getCrossSigningStatus();
|
||||||
|
console.log("crossSigningStatus", crossSigningStatus);
|
||||||
|
}
|
||||||
|
|
||||||
// store Device ID internally
|
// store Device ID internally
|
||||||
let stored_device_id = getStoredDeviceId(localStorage),
|
let stored_device_id = getStoredDeviceId(localStorage),
|
||||||
device_id = this.matrixClient.getDeviceId();
|
device_id = this.matrixClient.getDeviceId();
|
||||||
|
|
||||||
if(!device_id && node.enableE2ee) {
|
if(!device_id && node.e2ee) {
|
||||||
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) {
|
||||||
@ -143,6 +236,17 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist
|
fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist
|
||||||
upgradeDirectoryIfNecessary(node, storageDir);
|
upgradeDirectoryIfNecessary(node, storageDir);
|
||||||
|
|
||||||
|
// taken from https://github.com/matrix-org/matrix-react-sdk/blob/d9d0ab3d98dea8f260bd7037482c3c8cf288ae82/cypress/support/bot.ts
|
||||||
|
// these next lines are to fix "Device verification cancelled Error: No getCrossSigningKey callback supplied" error
|
||||||
|
const privateKeys = {};
|
||||||
|
const getCrossSigningKey = (type) => {
|
||||||
|
return privateKeys[type];
|
||||||
|
};
|
||||||
|
const saveCrossSigningKeys = (k) => {
|
||||||
|
Object.assign(privateKeys, k);
|
||||||
|
};
|
||||||
|
|
||||||
node.matrixClient = sdk.createClient({
|
node.matrixClient = sdk.createClient({
|
||||||
baseUrl: this.url,
|
baseUrl: this.url,
|
||||||
accessToken: this.credentials.accessToken,
|
accessToken: this.credentials.accessToken,
|
||||||
@ -152,10 +256,18 @@ module.exports = function(RED) {
|
|||||||
}),
|
}),
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined,
|
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined,
|
||||||
request
|
request,
|
||||||
// verificationMethods: ["m.sas.v1"]
|
verificationMethods: ["m.sas.v1"],
|
||||||
|
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.matrixClient.on("crypto.keyBackupStatus", function() {
|
||||||
|
console.log("crypto.keyBackupStatus");
|
||||||
|
bootstrapSSSS();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
@ -176,7 +288,7 @@ module.exports = function(RED) {
|
|||||||
stopClient();
|
stopClient();
|
||||||
if(node.globalAccess) {
|
if(node.globalAccess) {
|
||||||
try {
|
try {
|
||||||
node.context().global.delete('matrixClient["'+node.userId+'"]');
|
node.context().global.set('matrixClient["'+node.userId+'"]', undefined);
|
||||||
} catch(e){
|
} catch(e){
|
||||||
node.error(e.message, {});
|
node.error(e.message, {});
|
||||||
}
|
}
|
||||||
@ -190,15 +302,15 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,19 +337,29 @@ module.exports = function(RED) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let msg = {
|
let msg = {
|
||||||
encrypted : event.isEncrypted(),
|
encrypted : event.isEncrypted(),
|
||||||
redacted : event.isRedacted(),
|
redacted : event.isRedacted(),
|
||||||
content : event.getContent(),
|
content : event.getContent(),
|
||||||
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),
|
||||||
userId : event.getSender(),
|
isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread,
|
||||||
topic : event.getRoomId(),
|
mentions : event.getContent()["m.mentions"] || null,
|
||||||
eventId : event.getId(),
|
userId : event.getSender(),
|
||||||
event : event
|
user : node.matrixClient.getUser(event.getSender()),
|
||||||
|
topic : event.getRoomId(),
|
||||||
|
eventId : event.getId(),
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -386,11 +508,13 @@ 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.log("Bootstrapping SSSS...");
|
||||||
|
await bootstrapSSSS();
|
||||||
node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices
|
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, {});
|
||||||
@ -464,10 +588,15 @@ 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', {
|
||||||
user: userId,
|
user: userId,
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
$("#node-input-room").typedInput({
|
$("#node-input-room").typedInput({
|
||||||
type: this.roomType,
|
type: this.roomType,
|
||||||
types:['msg','flow','global','str'],
|
types:['msg','flow','global','str'],
|
||||||
}).typedInput('value', this.roomValue);
|
})
|
||||||
|
.typedInput('value', this.roomValue)
|
||||||
|
.typedInput('type', this.roomType);
|
||||||
|
|
||||||
$("#node-input-typing").typedInput({
|
$("#node-input-typing").typedInput({
|
||||||
types:['msg','flow','global','bool'],
|
types:['msg','flow','global','bool'],
|
||||||
@ -66,12 +68,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-room"><i class="fa fa-commenting-o"></i> Is Typing</label>
|
<label for="node-input-typing"><i class="fa fa-commenting-o"></i> Is Typing</label>
|
||||||
<input type="text" id="node-input-typing">
|
<input type="text" id="node-input-typing">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-room"><i class="fa fa-clock-o"></i> Timeout Milliseconds</label>
|
<label for="node-input-timeoutMs"><i class="fa fa-clock-o"></i> Timeout Milliseconds</label>
|
||||||
<input type="text" id="node-input-timeoutMs">
|
<input type="text" id="node-input-timeoutMs">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
|
|
||||||
<script type="text/html" data-help-name="matrix-upload-file">
|
<script type="text/html" data-help-name="matrix-upload-file">
|
||||||
<h3>Details</h3>
|
<h3>Details</h3>
|
||||||
<p>This node will send a file to a Matrix chat room. Supports direct linking to a File In node.</p>
|
<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>
|
<h3>Inputs</h3>
|
||||||
<dl class="message-properties">
|
<dl class="message-properties">
|
||||||
|
329
src/matrix-user-settings.html
Normal file
329
src/matrix-user-settings.html
Normal 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
184
src/matrix-user-settings.js
Normal 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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user