mirror of
https://github.com/Skylar-Tech/node-red-contrib-matrix-chat.git
synced 2026-05-23 23:53:26 -06:00
Compare commits
16 Commits
cf1bd78ee1
...
0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1ec9e65c | |||
| 2fcd463eb6 | |||
| b44629c84e | |||
| 9f0a962991 | |||
| f3df8c6f97 | |||
| d6ab098063 | |||
| 614920c40c | |||
| 58d7d23b2f | |||
| 25c92b787a | |||
| 0b9c59a7be | |||
| 9c836c9262 | |||
| 9c62a73b15 | |||
| 447b18835c | |||
| 2341cbef10 | |||
| 73d802ff0d | |||
| a10667a0e8 |
@@ -0,0 +1 @@
|
|||||||
|
@matrix-org:registry=https://gitlab.matrix.org/api/v4/packages/npm/
|
||||||
@@ -3,6 +3,8 @@ Matrix chat server client for [Node-RED](https://nodered.org/)
|
|||||||
|
|
||||||
***Currently we are in beta. We ask that you open any issues you have on our repository to help us reach a stable well tested version. Things may change & break before our first release so check changelog before updating.***
|
***Currently we are in beta. We ask that you open any issues you have on our repository to help us reach a stable well tested version. Things may change & break before our first release so check changelog before updating.***
|
||||||
|
|
||||||
|
If you need help with this feel free to join our public matrix room at [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
The following is supported from this package:
|
The following is supported from this package:
|
||||||
@@ -66,4 +68,4 @@ You will need a user to use this module. Luckily this module comes with a node t
|
|||||||
### Contributing
|
### Contributing
|
||||||
All contributions are welcome! If you do add a feature please do a pull request so that everyone benefits :)
|
All contributions are welcome! If you do add a feature please do a pull request so that everyone benefits :)
|
||||||
|
|
||||||
Sharing is caring!
|
Sharing is caring!
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ To view what sort of functions you have access to check out the `client.ts` file
|
|||||||
|
|
||||||
### Respond to "ping" with "pong"
|
### Respond to "ping" with "pong"
|
||||||
|
|
||||||
[View JSON](respond-to-html-with-html.json)
|
[View JSON](respond-ping-pong.json)
|
||||||
|
|
||||||
Use this flow to respond to anyone that says "ping" with "pong" into the same room.
|
Use this flow to respond to anyone that says "ping" with "pong" into the same room.
|
||||||
|
|
||||||
|
|||||||
Generated
+5647
-1945
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "node-red-contrib-matrix-chat",
|
"name": "node-red-contrib-matrix-chat",
|
||||||
"version": "0.2.4",
|
"version": "0.4.0",
|
||||||
"description": "Matrix chat server client for Node-RED",
|
"description": "Matrix chat server client for Node-RED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@matrix-org/olm": "^3.2.8",
|
||||||
"fs-extra": "^9.1.0",
|
"fs-extra": "^9.1.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"isomorphic-webcrypto": "^2.3.8",
|
"isomorphic-webcrypto": "^2.3.8",
|
||||||
"matrix-js-sdk": "^12.2.0",
|
"matrix-js-sdk": "^15.3.0",
|
||||||
"node-localstorage": "^2.2.1",
|
"node-localstorage": "^2.2.1",
|
||||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"utf8": "^3.0.0"
|
"utf8": "^3.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ module.exports = function(RED) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
|
||||||
node.error("msg.userId was not set.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.server.matrixClient.joinRoom(msg.topic, msg.joinOpts || {})
|
node.server.matrixClient.joinRoom(msg.topic, msg.joinOpts || {})
|
||||||
.then(function(e) {
|
.then(function(e) {
|
||||||
msg.payload = e;
|
msg.payload = e;
|
||||||
|
|||||||
+6
-8
@@ -53,16 +53,14 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
msg.type = 'm.reaction';
|
msg.type = 'm.reaction';
|
||||||
|
|
||||||
node.server.matrixClient.sendCompleteEvent(
|
node.server.matrixClient.sendEvent(
|
||||||
msg.topic,
|
msg.topic,
|
||||||
|
'm.reaction',
|
||||||
{
|
{
|
||||||
type: 'm.reaction',
|
"m.relates_to": {
|
||||||
content: {
|
event_id: eventId,
|
||||||
"m.relates_to": {
|
key: msg.payload,
|
||||||
event_id: eventId,
|
rel_type: "m.annotation"
|
||||||
key: msg.payload,
|
|
||||||
rel_type: "m.annotation"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-33
@@ -22,7 +22,7 @@ module.exports = function(RED) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.server.on("disconnected", function() {
|
node.server.on("disconnected", function(){
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,43 +30,12 @@ module.exports = function(RED) {
|
|||||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
});
|
});
|
||||||
|
|
||||||
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, data) {
|
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
|
||||||
if (toStartOfTimeline) {
|
|
||||||
return; // ignore paginated results
|
|
||||||
}
|
|
||||||
if (!event.getSender() || event.getSender() === node.server.userId) {
|
|
||||||
return; // ignore our own messages
|
|
||||||
}
|
|
||||||
if (!event.getUnsigned() || event.getUnsigned().age > 1000) {
|
|
||||||
return; // ignore old messages
|
|
||||||
}
|
|
||||||
|
|
||||||
// if node has a room ID set we only listen on that room
|
// if node has a room ID set we only listen on that room
|
||||||
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) {
|
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await node.server.matrixClient.decryptEventIfNeeded(event);
|
|
||||||
} catch (error) {
|
|
||||||
node.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let msg = {
|
|
||||||
encrypted : event.isEncrypted(),
|
|
||||||
redacted : event.isRedacted(),
|
|
||||||
content : event.getContent(),
|
|
||||||
type : (event.getContent()['msgtype'] || event.getType()) || null,
|
|
||||||
payload : (event.getContent()['body'] || event.getContent()) || null,
|
|
||||||
userId : 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);
|
|
||||||
|
|
||||||
switch(msg.type) {
|
switch(msg.type) {
|
||||||
case 'm.emote':
|
case 'm.emote':
|
||||||
if(!node.acceptEmotes) return;
|
if(!node.acceptEmotes) return;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ module.exports = function(RED) {
|
|||||||
.sendMessage(msg.topic, content)
|
.sendMessage(msg.topic, content)
|
||||||
.then(function(e) {
|
.then(function(e) {
|
||||||
node.log("File message sent: " + e);
|
node.log("File message sent: " + e);
|
||||||
msg.eventId = e.eventId;
|
msg.eventId = e.event_id;
|
||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ module.exports = function(RED) {
|
|||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed");
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
@@ -118,7 +119,7 @@ module.exports = function(RED) {
|
|||||||
node.server.matrixClient.sendMessage(msg.topic, content)
|
node.server.matrixClient.sendMessage(msg.topic, content)
|
||||||
.then(function(e) {
|
.then(function(e) {
|
||||||
node.log("Message sent: " + msg.payload);
|
node.log("Message sent: " + msg.payload);
|
||||||
msg.eventId = e.eventId;
|
msg.eventId = e.event_id;
|
||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
|
|||||||
+126
-10
@@ -1,11 +1,35 @@
|
|||||||
|
<style>
|
||||||
|
.matrix-loader {
|
||||||
|
border: 8px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 8px solid #3498db;
|
||||||
|
border-bottom: 8px solid #3498db;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
-webkit-animation: matrix-spin 2s linear infinite;
|
||||||
|
animation: matrix-spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes matrix-spin {
|
||||||
|
0% { -webkit-transform: rotate(0deg); }
|
||||||
|
100% { -webkit-transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes matrix-spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
RED.nodes.registerType('matrix-server-config',{
|
RED.nodes.registerType('matrix-server-config',{
|
||||||
category: 'config',
|
category: 'config',
|
||||||
color: '#00b7ca',
|
color: '#00b7ca',
|
||||||
credentials: {
|
credentials: {
|
||||||
userId: { type: "text", required: true },
|
userId: { type: "text", required: true },
|
||||||
|
deviceLabel: { type: "text", required: false },
|
||||||
accessToken: { type: "password", required: true },
|
accessToken: { type: "password", required: true },
|
||||||
deviceId: { type: "text", required: true },
|
deviceId: { type: "text", required: false },
|
||||||
url: { type: "text", required: true }
|
url: { type: "text", required: true }
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -26,28 +50,51 @@
|
|||||||
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
<input type="text" id="node-config-input-name" placeholder="Name">
|
<input type="text" id="node-config-input-name" placeholder="Name">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-config-input-userId"><i class="fa fa-server"></i> User ID</label>
|
<label for="node-config-input-deviceLabel"><i class="fa fa-tag"></i> Device Label</label>
|
||||||
|
<input type="text" placeholder="Node-RED" id="node-config-input-deviceLabel">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-url"><i class="fa fa-server"></i> Server URL</label>
|
||||||
|
<input type="text" placeholder="https://matrix.org" id="node-config-input-url">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-userId"><i class="fa fa-user"></i> User ID</label>
|
||||||
<input type="text" placeholder="@example:matrix.org" id="node-config-input-userId">
|
<input type="text" placeholder="@example:matrix.org" id="node-config-input-userId">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
|
||||||
|
<input type="password" placeholder="" id="node-config-input-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Password is never saved and is only used to fetch an access token using the button below.
|
||||||
|
</div>
|
||||||
|
<pre class="form-tips" id="matrix-chat-login-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;display:none;"></pre>
|
||||||
|
<pre class="form-tips" id="matrix-chat-login-success" style="color: #155724;background-color: #d4edda;border-color: #c3e6cb;margin-bottom: 12px;display:none;"></pre>
|
||||||
|
<button class="ui-button ui-corner-all ui-widget primary" id="matrix-login-btn">Update Access Token</button>
|
||||||
|
<div class="matrix-loader" id="matrix-access-token-loader" style="display:none;"></div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label>
|
<label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label>
|
||||||
<input type="text" id="node-config-input-accessToken">
|
<input type="text" id="node-config-input-accessToken">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-tips" style="margin-bottom: 12px;">
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token. You can also generate them using the Shared Secret Registration node.
|
You can either provide/generate an access token yourself or use the login button above to do it automatically. View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-config-input-deviceId"><i class="fa fa-key"></i> Device ID</label>
|
<label for="node-config-input-deviceId"><i class="fa fa-desktop"></i> Device ID</label>
|
||||||
<input type="text" id="node-config-input-deviceId">
|
<input type="text" id="node-config-input-deviceId">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-tips" style="margin-bottom: 12px;">
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
This can either be an existing Device ID attached to the above Access Token or you can enter a unique value to set a new one.
|
If you have a Device ID for this auth token you can place it here, or you can leave it blank to have one automatically generated (this field will always be blank if auto generated as it is saved internally). DO NOT manually set one, this must be generated by the server.<br />
|
||||||
|
WARNING: If you change this after the client has already initialized you will break encryption. Your Device ID is tied to your encryption keys.
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<label for="node-config-input-url"><i class="fa fa-globe"></i> Server URL</label>
|
|
||||||
<input type="text" placeholder="https://matrix.org" id="node-config-input-url">
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -79,10 +126,79 @@
|
|||||||
Global access to Matrix Client
|
Global access to Matrix Client
|
||||||
</label>
|
</label>
|
||||||
<div class="form-tips" style="margin-bottom: 12px;">
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
If enabled this allows you to access the matrix client directly with a Function node. This way you can do <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command" target="_blank">whatever you want</a> with the client. Example:<br>
|
If global access is enabled you can access the client directly within a Function node. This way you can do <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command" target="_blank">whatever you want</a> with the client. Example:<br>
|
||||||
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
|
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$("#matrix-login-btn").on("click", function() {
|
||||||
|
function prettyPrintJson(json) {
|
||||||
|
try{
|
||||||
|
return typeof json === 'object' ? JSON.stringify(json, null, 2) : json;
|
||||||
|
}
|
||||||
|
catch (error){
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let userId = $("#node-config-input-userId").val(),
|
||||||
|
userPassword = $("#node-config-input-password").val(),
|
||||||
|
serverUrl = $("#node-config-input-url").val();
|
||||||
|
|
||||||
|
if(!userId) {
|
||||||
|
alert("User ID is required to fetch access token.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!userPassword) {
|
||||||
|
alert("Password is required to fetch access token.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!serverUrl) {
|
||||||
|
alert("Server URL is required to fetch access token.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#matrix-login-btn, #matrix-chat-login-error, #matrix-chat-login-success").hide();
|
||||||
|
$("#matrix-access-token-loader").show();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/matrix-chat/login',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
'userId': userId,
|
||||||
|
'password': userPassword,
|
||||||
|
'baseUrl': serverUrl,
|
||||||
|
'displayName': $("#node-config-input-deviceLabel").val(),
|
||||||
|
}
|
||||||
|
}).then(
|
||||||
|
function(data) {
|
||||||
|
if(data.result && data.result === 'ok') {
|
||||||
|
$("#matrix-chat-login-error").hide();
|
||||||
|
$("#matrix-chat-login-success")
|
||||||
|
.html("Login Successful! Auth Token and Device ID have been set below.")
|
||||||
|
.show();
|
||||||
|
|
||||||
|
$("#node-config-input-accessToken").val(data.token);
|
||||||
|
$("#node-config-input-deviceId").val(data.device_id);
|
||||||
|
} else if(data.result && data.result === 'error') {
|
||||||
|
$("#matrix-chat-login-success").hide();
|
||||||
|
$("#matrix-chat-login-error")
|
||||||
|
.html(data.message ? ('Failed to login: <br />' + prettyPrintJson(data.message)) : 'Failed to login')
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
$("#matrix-login-btn").show();
|
||||||
|
$("#matrix-access-token-loader").hide();
|
||||||
|
}, function() {
|
||||||
|
$("#matrix-chat-login-success").hide();
|
||||||
|
$("#matrix-chat-login-error")
|
||||||
|
.html("Failed to login due to server error communicating with Node-RED")
|
||||||
|
.show();
|
||||||
|
$("#matrix-login-btn").show();
|
||||||
|
$("#matrix-access-token-loader").hide();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/html" data-help-name="matrix-server-config">
|
<script type="text/html" data-help-name="matrix-server-config">
|
||||||
|
|||||||
+245
-51
@@ -1,4 +1,4 @@
|
|||||||
global.Olm = require('olm');
|
global.Olm = require('@matrix-org/olm');
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const sdk = require("matrix-js-sdk");
|
const sdk = require("matrix-js-sdk");
|
||||||
const { LocalStorage } = require('node-localstorage');
|
const { LocalStorage } = require('node-localstorage');
|
||||||
@@ -12,9 +12,6 @@ module.exports = function(RED) {
|
|||||||
function MatrixServerNode(n) {
|
function MatrixServerNode(n) {
|
||||||
let storageDir = './matrix-client-storage';
|
let storageDir = './matrix-client-storage';
|
||||||
|
|
||||||
// we should add support for getting access token automatically from username/password
|
|
||||||
// ref: https://matrix.org/docs/guides/usage-of-the-matrix-js-sdk#login-with-an-access-token
|
|
||||||
|
|
||||||
RED.nodes.createNode(this, n);
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
let node = this;
|
let node = this;
|
||||||
@@ -29,12 +26,19 @@ module.exports = function(RED) {
|
|||||||
this.connected = null;
|
this.connected = null;
|
||||||
this.name = n.name;
|
this.name = n.name;
|
||||||
this.userId = this.credentials.userId;
|
this.userId = this.credentials.userId;
|
||||||
|
this.deviceLabel = this.credentials.deviceLabel || null;
|
||||||
this.deviceId = this.credentials.deviceId || null;
|
this.deviceId = this.credentials.deviceId || null;
|
||||||
this.url = this.credentials.url;
|
this.url = this.credentials.url;
|
||||||
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
|
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
|
||||||
this.enableE2ee = n.enableE2ee || false;
|
this.enableE2ee = n.enableE2ee || false;
|
||||||
this.e2ee = (this.enableE2ee && this.deviceId);
|
this.e2ee = (this.enableE2ee && this.deviceId);
|
||||||
this.globalAccess = n.global;
|
this.globalAccess = n.global;
|
||||||
|
this.initializedAt = new Date();
|
||||||
|
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId),
|
||||||
|
localStorage = new LocalStorage(localStorageDir),
|
||||||
|
initialSetup = false;
|
||||||
|
|
||||||
|
let retryStartTimeout = null;
|
||||||
|
|
||||||
if(!this.credentials.accessToken) {
|
if(!this.credentials.accessToken) {
|
||||||
node.log("Matrix connection failed: missing access token.");
|
node.log("Matrix connection failed: missing access token.");
|
||||||
@@ -53,6 +57,40 @@ module.exports = function(RED) {
|
|||||||
if (connected) {
|
if (connected) {
|
||||||
node.log("Matrix server connection ready.");
|
node.log("Matrix server connection ready.");
|
||||||
node.emit("connected");
|
node.emit("connected");
|
||||||
|
if(!initialSetup) {
|
||||||
|
// store Device ID internally
|
||||||
|
let stored_device_id = getStoredDeviceId(localStorage),
|
||||||
|
device_id = this.matrixClient.getDeviceId();
|
||||||
|
if(!stored_device_id || stored_device_id !== device_id) {
|
||||||
|
node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`);
|
||||||
|
storeDeviceId(localStorage, device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update device label
|
||||||
|
if(node.deviceLabel) {
|
||||||
|
node.matrixClient
|
||||||
|
.getDevice(device_id)
|
||||||
|
.then(
|
||||||
|
function(response) {
|
||||||
|
if(response.display_name !== node.deviceLabel) {
|
||||||
|
node.matrixClient.setDeviceDetails(device_id, {
|
||||||
|
display_name: node.deviceLabel
|
||||||
|
}).then(
|
||||||
|
function(response) {},
|
||||||
|
function(error) {
|
||||||
|
node.error("Failed to set device label: " + error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
node.error("Failed to fetch device: " + error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSetup = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
node.emit("disconnected");
|
node.emit("disconnected");
|
||||||
}
|
}
|
||||||
@@ -64,18 +102,15 @@ module.exports = function(RED) {
|
|||||||
};
|
};
|
||||||
node.setConnected(false);
|
node.setConnected(false);
|
||||||
|
|
||||||
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId);
|
|
||||||
|
|
||||||
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);
|
||||||
const localStorage = new LocalStorage(localStorageDir);
|
|
||||||
node.matrixClient = sdk.createClient({
|
node.matrixClient = sdk.createClient({
|
||||||
baseUrl: this.url,
|
baseUrl: this.url,
|
||||||
accessToken: this.credentials.accessToken,
|
accessToken: this.credentials.accessToken,
|
||||||
sessionStore: new sdk.WebStorageSessionStore(localStorage),
|
sessionStore: new sdk.WebStorageSessionStore(localStorage),
|
||||||
cryptoStore: new LocalStorageCryptoStore(localStorage),
|
cryptoStore: new LocalStorageCryptoStore(localStorage),
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
deviceId: this.deviceId || undefined,
|
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// set globally if configured to do so
|
// set globally if configured to do so
|
||||||
@@ -83,13 +118,19 @@ module.exports = function(RED) {
|
|||||||
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
|
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
node.on('close', function(done) {
|
function stopClient() {
|
||||||
if(node.matrixClient) {
|
if(node.matrixClient && node.matrixClient.clientRunning) {
|
||||||
node.matrixClient.close();
|
|
||||||
node.matrixClient.stopClient();
|
node.matrixClient.stopClient();
|
||||||
node.setConnected(false);
|
node.setConnected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(retryStartTimeout) {
|
||||||
|
clearTimeout(retryStartTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.on('close', function(done) {
|
||||||
|
stopClient();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,8 +138,51 @@ module.exports = function(RED) {
|
|||||||
return node.connected;
|
return node.connected;
|
||||||
};
|
};
|
||||||
|
|
||||||
node.matrixClient.on("Room.timeline", async function(event, room, toStartOfTimeline, data) {
|
node.matrixClient.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data) {
|
||||||
node.emit("Room.timeline", event, room, toStartOfTimeline, data);
|
if (toStartOfTimeline) {
|
||||||
|
return; // ignore paginated results
|
||||||
|
}
|
||||||
|
if (!event.getSender() || event.getSender() === node.userId) {
|
||||||
|
return; // ignore our own messages
|
||||||
|
}
|
||||||
|
if (!data || !data.liveEvent) {
|
||||||
|
return; // ignore old message (we only want live events)
|
||||||
|
}
|
||||||
|
if(node.initializedAt > event.getDate()) {
|
||||||
|
return; // skip events that occurred before our client initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await node.matrixClient.decryptEventIfNeeded(event);
|
||||||
|
} catch (error) {
|
||||||
|
node.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = {
|
||||||
|
encrypted : event.isEncrypted(),
|
||||||
|
redacted : event.isRedacted(),
|
||||||
|
content : event.getContent(),
|
||||||
|
type : (event.getContent()['msgtype'] || event.getType()) || null,
|
||||||
|
payload : (event.getContent()['body'] || event.getContent()) || null,
|
||||||
|
userId : 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]' : ''));
|
||||||
|
node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when we want to suggest to the user that they restore their megolm keys
|
||||||
|
* from backup or by cross-signing the device.
|
||||||
|
*
|
||||||
|
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
|
||||||
|
*/
|
||||||
|
node.matrixClient.on("crypto.suggestKeyRestore", function(){
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// node.matrixClient.on("RoomMember.typing", async function(event, member) {
|
// node.matrixClient.on("RoomMember.typing", async function(event, member) {
|
||||||
@@ -133,36 +217,70 @@ module.exports = function(RED) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.matrixClient.on("sync", async function(state, prevState, data) {
|
node.matrixClient.on("sync", async function(state, prevState, data) {
|
||||||
switch (state) {
|
node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]");
|
||||||
case "ERROR":
|
if(prevState === null && state === "PREPARED" ) {
|
||||||
node.setConnected(false, function(){
|
// Occurs when the initial sync is completed first time.
|
||||||
node.error("Connection to Matrix server lost");
|
// This involves setting up filters and obtaining push rules.
|
||||||
});
|
node.setConnected(true, function(){
|
||||||
break;
|
node.log("Matrix client connected");
|
||||||
|
});
|
||||||
case "RECONNECTING":
|
} else if(prevState === null && state === "ERROR") {
|
||||||
node.setConnected(false, function(){
|
// Occurs when the initial sync failed first time.
|
||||||
node.log("Trying to reconnect to matrix server");
|
node.setConnected(false, function(){
|
||||||
});
|
node.error("Failed to connect to Matrix server");
|
||||||
break;
|
});
|
||||||
case "STOPPED":
|
} else if(prevState === "ERROR" && state === "PREPARED") {
|
||||||
node.setConnected(false, function(){
|
// Occurs when the initial sync succeeds
|
||||||
node.log("Matrix client stopped");
|
// after previously failing.
|
||||||
});
|
node.setConnected(true, function(){
|
||||||
break;
|
node.log("Matrix client connected");
|
||||||
|
});
|
||||||
case "SYNCING":
|
} else if(prevState === "PREPARED" && state === "SYNCING") {
|
||||||
case "PREPARED":
|
// Occurs immediately after transitioning to PREPARED.
|
||||||
node.setConnected(true, function(){
|
// Starts listening for live updates rather than catching up.
|
||||||
node.log("Matrix client connected");
|
node.setConnected(true, function(){
|
||||||
});
|
node.log("Matrix client connected");
|
||||||
break;
|
});
|
||||||
|
} else if(prevState === "SYNCING" && state === "RECONNECTING") {
|
||||||
// case "PREPARED":
|
// Occurs when the live update fails.
|
||||||
// // the client instance is ready to be queried.
|
node.setConnected(false, function(){
|
||||||
// node.log("Matrix server connection ready.");
|
node.error("Connection to Matrix server lost");
|
||||||
// node.setConnected(true);
|
});
|
||||||
// break;
|
} else if(prevState === "RECONNECTING" && state === "RECONNECTING") {
|
||||||
|
// Can occur if the update calls continue to fail,
|
||||||
|
// but the keepalive calls (to /versions) succeed.
|
||||||
|
node.setConnected(false, function(){
|
||||||
|
node.error("Connection to Matrix server lost");
|
||||||
|
});
|
||||||
|
} else if(prevState === "RECONNECTING" && state === "ERROR") {
|
||||||
|
// Occurs when the keepalive call also fails
|
||||||
|
node.setConnected(false, function(){
|
||||||
|
node.error("Connection to Matrix server lost");
|
||||||
|
});
|
||||||
|
} else if(prevState === "ERROR" && state === "SYNCING") {
|
||||||
|
// Occurs when the client has performed a
|
||||||
|
// live update after having previously failed.
|
||||||
|
node.setConnected(true, function(){
|
||||||
|
node.log("Matrix client connected");
|
||||||
|
});
|
||||||
|
} else if(prevState === "ERROR" && state === "ERROR") {
|
||||||
|
// Occurs when the client has failed to
|
||||||
|
// keepalive for a second time or more.
|
||||||
|
node.setConnected(false, function(){
|
||||||
|
node.error("Connection to Matrix server lost");
|
||||||
|
});
|
||||||
|
} else if(prevState === "SYNCING" && state === "SYNCING") {
|
||||||
|
// Occurs when the client has performed a live update.
|
||||||
|
// This is called <i>after</i> processing.
|
||||||
|
node.setConnected(true, function(){
|
||||||
|
node.log("Matrix client connected");
|
||||||
|
});
|
||||||
|
} else if(state === "STOPPED") {
|
||||||
|
// Occurs once the client has stopped syncing or
|
||||||
|
// trying to sync after stopClient has been called.
|
||||||
|
node.setConnected(false, function(){
|
||||||
|
node.error("Connection to Matrix server lost");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,7 +296,9 @@ module.exports = function(RED) {
|
|||||||
// httpStatus: 401
|
// httpStatus: 401
|
||||||
// }
|
// }
|
||||||
|
|
||||||
node.error("[Session.logged_out] " + errorObj);
|
console.log("Authentication failure: ", errorObj);
|
||||||
|
node.error("Authentication failure: " + errorObj);
|
||||||
|
stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
@@ -189,25 +309,88 @@ module.exports = function(RED) {
|
|||||||
node.matrixClient.setGlobalErrorOnUnknownDevices(false);
|
node.matrixClient.setGlobalErrorOnUnknownDevices(false);
|
||||||
}
|
}
|
||||||
node.log("Connecting to Matrix server...");
|
node.log("Connecting to Matrix server...");
|
||||||
await node.matrixClient.startClient({ initialSyncLimit: 8 });
|
await node.matrixClient.startClient({
|
||||||
} catch(error){
|
initialSyncLimit: 8
|
||||||
|
});
|
||||||
|
} catch(error) {
|
||||||
node.error(error);
|
node.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch((error) => node.error(error));
|
// do an authed request and only continue if we don't get an error
|
||||||
|
// this prevent the matrix client from crashing Node-RED on invalid auth token
|
||||||
|
(function checkAuthTokenThenStart() {
|
||||||
|
if(node.matrixClient.clientRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.matrixClient.getAccountDataFromServer()
|
||||||
|
.then(
|
||||||
|
function() {
|
||||||
|
run().catch((error) => node.error(error));
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
// if the error isn't authentication related retry in a little bit
|
||||||
|
if(err.code !== "M_UNKNOWN_TOKEN") {
|
||||||
|
retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000);
|
||||||
|
node.error("Auth check failed: " + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RED.nodes.registerType("matrix-server-config", MatrixServerNode, {
|
RED.nodes.registerType("matrix-server-config", MatrixServerNode, {
|
||||||
credentials: {
|
credentials: {
|
||||||
userId: { type:"text", required: true },
|
deviceLabel: { type: "text", required: false },
|
||||||
accessToken: { type:"text", required: true },
|
userId: { type: "text", required: true },
|
||||||
deviceId: { type: "text", required: true },
|
accessToken: { type: "text", required: true },
|
||||||
url: { type: "text", required: true },
|
deviceId: { type: "text", required: false },
|
||||||
|
url: { type: "text", required: true }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RED.httpAdmin.post(
|
||||||
|
"/matrix-chat/login",
|
||||||
|
RED.auth.needsPermission('flows.write'),
|
||||||
|
function(req, res) {
|
||||||
|
let userId = req.body.userId || undefined,
|
||||||
|
password = req.body.password || undefined,
|
||||||
|
baseUrl = req.body.baseUrl || undefined,
|
||||||
|
deviceId = req.body.deviceId || undefined,
|
||||||
|
displayName = req.body.displayName || undefined;
|
||||||
|
|
||||||
|
const matrixClient = sdk.createClient({
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
deviceId: deviceId,
|
||||||
|
localTimeoutMs: '30000'
|
||||||
|
});
|
||||||
|
|
||||||
|
matrixClient.login(
|
||||||
|
'm.login.password', {
|
||||||
|
user: userId,
|
||||||
|
password: password,
|
||||||
|
initial_device_display_name: displayName
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
function(response) {
|
||||||
|
res.json({
|
||||||
|
'result': 'ok',
|
||||||
|
'token': response.access_token,
|
||||||
|
'device_id': response.device_id,
|
||||||
|
'user_id': response.user_id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
res.json({
|
||||||
|
'result': 'error',
|
||||||
|
'message': err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function upgradeDirectoryIfNecessary(node, storageDir) {
|
function upgradeDirectoryIfNecessary(node, storageDir) {
|
||||||
let oldStorageDir = './matrix-local-storage';
|
let oldStorageDir = './matrix-local-storage';
|
||||||
|
|
||||||
@@ -233,4 +416,15 @@ module.exports = function(RED) {
|
|||||||
fs.renameSync(oldStorageDir, oldStorageDir + "-backup");
|
fs.renameSync(oldStorageDir, oldStorageDir + "-backup");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a device ID is stored we will use that for the client
|
||||||
|
*/
|
||||||
|
function getStoredDeviceId(localStorage) {
|
||||||
|
return localStorage.getItem('my_device_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeDeviceId(localStorage, deviceId) {
|
||||||
|
localStorage.setItem('my_device_id', deviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,10 @@
|
|||||||
<dt class="optional">msg.payload.user_type
|
<dt class="optional">msg.payload.user_type
|
||||||
<span class="property-type">string | null</span>
|
<span class="property-type">string | null</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd> Set the user type. Leave this to null if you don't know what it is for. Check <a href="https://github.com/matrix-org/synapse/blob/master/synapse/api/constants.py">here</a> and look for <code>class UserTypes</code> to figure out what is valid.</dd>
|
<dd>
|
||||||
|
Set the user type. Leave this to null if you don't know what it is for. Check <a href="https://github.com/matrix-org/synapse/blob/master/synapse/api/constants.py">here</a> and look for <code>class UserTypes</code> to figure out what is valid.<br>
|
||||||
|
NOTE: Testing shows that "bot" and "support" do not work for this. Generate a normal user to use as a bot.
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<h3>Outputs</h3>
|
<h3>Outputs</h3>
|
||||||
|
|||||||
@@ -62,7 +62,13 @@ module.exports = function(RED) {
|
|||||||
.update(utf8.encode(msg.payload.password))
|
.update(utf8.encode(msg.payload.password))
|
||||||
.update("\x00")
|
.update("\x00")
|
||||||
.update(msg.payload.admin ? "admin" : "notadmin")
|
.update(msg.payload.admin ? "admin" : "notadmin")
|
||||||
.digest('hex');
|
|
||||||
|
if(msg.payload.user_type || null) {
|
||||||
|
hmac.update("\x00")
|
||||||
|
.update(msg.payload.user_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
hmac = hmac.digest('hex');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await got.post(this.server + '/_synapse/admin/v1/register', {
|
response = await got.post(this.server + '/_synapse/admin/v1/register', {
|
||||||
|
|||||||
Reference in New Issue
Block a user