17 Commits

Author SHA1 Message Date
Skylar Sadlier
5119bc42d4 - WIP 2022-03-04 00:12:59 -07:00
Skylar Sadlier
2993dea974 - WIP 2022-03-04 00:09:15 -07:00
Skylar Sadlier
678702e9c6 WIP on secret storage node 2022-03-04 00:09:15 -07:00
Skylar Sadlier
36c8208b01 - WIP on getting device validation to work 2022-03-04 00:09:15 -07:00
Skylar Sadlier
d0ba671452 - Fix force-join-room example links 2022-02-09 12:58:32 -07:00
Skylar Sadlier
282c8ffc80 - Bump version to 0.4.6
- Added example for force joining a room
- Deactivate a user example was missing json file contents
2022-02-09 12:48:52 -07:00
Skylar Sadlier
6dd2ec75f0 - Move client storage to the Node-RED user directory.
- Remove mention of Device ID being required for encryption (since it can now auto generate if not provided this could make people think they have to set it when that isn't the case)
- matrix-js-sdk updated from ^v15.3.0 to ^v15.5.0
- got updated from ^11.8.2 to ^12.0.1 (this also required us to change how we import this library in code)
- Node-RED version requirement added for >=v1.3.0
- NodeJS version requirement added for >=v14.0.0
- removed `process` dependency
- send-image node fixed so error doesn't get thrown (`matrix-js-sdk` updated causing some errors)
- updated send-image node docs to explain that msg.contentType is necessary for some clients to render the image (otherwise it could display as a blank message in the room).
- If a matrix server configuration node was missing it's User ID it would throw a TypeError instead of telling the user the actual issue.
- Updated user list example: it now paginates all users on the server (if you had a lot of users the message would fail to send because it was too large)
- Added example for creating a room and inviting a user
- Added example for joining a mentioned room
- Added example for listing out a user's or server's rooms
- Added example for getting session data from a user via whois info
- Added example for getting a room's user list
- Added example for downloading & storing received files/images
- Added example for kicking/banning user from a room.
- Added example for deactivating a user
- Removed message in the room-users config stating it only works if you are an admin
- Receive node now outputs `msg.filename` for files and images
2022-02-09 12:13:27 -07:00
Skylar Sadlier
ae386b90a6 Fix olm library reference. 2022-02-03 11:33:12 -07:00
Skylar Sadlier
4d1ec9e65c - Make it clear that manually generating a Device ID is bad behavior
- Make DeviceID no longer required for matrix-server-config
2022-02-03 10:36:09 -07:00
Skylar Sadlier
2fcd463eb6 - Update release to 0.4.0 2022-02-03 10:16:19 -07:00
Skylar Sadlier
b44629c84e - When checking authentication we should retry startup if the error we receive isn't related to invalid auth token (M_UNKNOWN_TOKEN) 2022-02-03 10:15:52 -07:00
Skylar Sadlier
9f0a962991 - Fixed: Reactions weren't working because sendCompleteEvent is a private method. 2022-02-03 09:40:32 -07:00
Skylar Sadlier
f3df8c6f97 This comment is no longer necessary since we implemented it. 2022-01-28 11:17:23 -07:00
Skylar Sadlier
d6ab098063 Fixed example readme linking to wrong json file Closes #38 2022-01-28 10:57:35 -07:00
Skylar Sadlier
614920c40c Fixed: matrix-join-room was requiring userId when it didn't need it Closes #39 2022-01-28 10:55:38 -07:00
Skylar Sadlier
58d7d23b2f - Device ID is now stored inside the matrix-client-storage directory within Node-RED's config folder. This way if you leave the Device ID blank it will automatically generate one and save it. Previously if you didn't set a Device ID it would automatically generate one every time the client started which would break encryption (since device_id is tied to your encryption keys).
- You can now generate an Access Token and Device ID from just a username and password (use the Update Access Token button when configuring a client).
- Updated the look of the matrix-server-config node (icons and reorganization)
- Invalid auth token will no longer crash Node-RED on deployment/start. We do an authorized request before starting the client to see if the auth token is valid. If it is not the client will not start and all nodes for this client will show disconnected and an error will display in the debug toolbar.
- Updated matrix olm library from 3.2.1 to 3.2.8
- Setting a Device Label from the server-config node will now update your device's label. If you view the sessions for the client you will see it gets updated after deployment.
2022-01-28 10:53:10 -07:00
Skylar Sadlier
25c92b787a - Messages used to get ignored if they were over 1 second old. This caused issues with messages being ignored on servers that took longer than a second to respond (*cough* matrix.org *cough*). Now it accepts all new messages. Moving forward any messages that are sent before Node-RED starts or gets deployed will be ignored (so that old messages don't pop up since the cache clears every deploy/node-red restart so old messages look like new ones).
- When the matrix-server node would shut down (from a deployment) it wasn't properly shutting the old instance of the client down. This would cause messages to start duplicating X times (X being however many times you deployed since you last started Node-RED). This is now fixed.
- Upgraded to matrix-js-sdk 15.3.0
- Message processing is now done on the matrix-server node. Before if you had multiple matrix-receive nodes it would output one line per matrix-receive node into the log. Now it only outputs from the matrix-server node. Note that all messages get logged that are sent to rooms the bot is in whereas before it would only log message in rooms the matrix-receive node was in.
- Fixed Shared secret registration failing if user_type was defined.
2022-01-14 12:56:14 -07:00
39 changed files with 8950 additions and 2273 deletions

View File

@@ -16,6 +16,14 @@ Build something cool with these nodes? Feel free to submit a pull request to sha
- [Respond to "file" with an uploaded file](#respond-to-file-with-an-uploaded-file)
- [Respond to "react" with a reaction](#respond-to-react-with-a-reaction)
- [Respond to "users" with full list of server users](#respond-to-users-with-full-list-of-server-users)
- [Respond to "newroom" by creating new room and inviting user](#respond-to-newroom-by-creating-new-room-and-inviting-user)
- [Respond to "joinroom <room_id_or_alias>" by joining mentioned room](#respond-to-joinroom-room_id_or_alias-by-joining-mentioned-room)
- [Respond to "rooms <user_id>" with user's rooms (list server's rooms if <user_id> is left blank)](#respond-to-rooms-user_id-with-users-rooms-list-servers-rooms-if-user_id-is-left-blank)
- [Respond to "whois <user_id>" with information about the user's session](#respond-to-whois-user_id-with-information-about-the-users-session)
- [Respond to "room_users" with current room's users](#respond-to-room_users-with-current-rooms-users)
- [Download & store all received files/images](#download--store-all-received-filesimages)
- [Kick/Ban user from room](#kickban-user-from-room)
- [Deactivate user](#deactivate-user)
### Create user with Shared Secret Registration
@@ -28,7 +36,7 @@ Edit the object on the inject node to the user/pass combo you want to create and
**Note:** This only works on Synapse servers.
![img.png](shared-secret-registration.png)
![shared-secret-registration.png](shared-secret-registration.png)
### Create/Edit Synapse User
@@ -37,7 +45,7 @@ Edit the object on the inject node to the user/pass combo you want to create and
Allows an administrator to create or modify a user account with a specified `msg.userId`.
![img.png](add-user-with-admin-user.png)
![add-user-with-admin-user.png](add-user-with-admin-user.png)
### Use function node to run any command
@@ -50,15 +58,15 @@ If we do not have a node for something you want to do (such as redacting events/
To view what sort of functions you have access to check out the `client.ts` file from `matrix-js-sdk` [here](https://github.com/matrix-org/matrix-js-sdk/blob/master/src/client.ts).
![img.png](custom-redact-function-node.png)
![custom-redact-function-node.png](custom-redact-function-node.png)
### 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.
![img.png](respond-ping-pong.png)
![respond-ping-pong.png](respond-ping-pong.png)
@@ -68,7 +76,7 @@ Use this flow to respond to anyone that says "ping" with "pong" into the same ro
Use this flow to respond to anyone that says "html" with an example HTML message. This shows how easy it is to send HTML.
![img.png](respond-to-html-with-html.png)
![respond-to-html-with-html.png](respond-to-html-with-html.png)
@@ -78,7 +86,7 @@ Use this flow to respond to anyone that says "html" with an example HTML message
You will need an image on the machine running Node-RED. In this case example.png exists inside the Node-RED directory.
![img_1.png](respond-image-with-image.png)
![respond-image-with-image.png](respond-image-with-image.png)
@@ -88,7 +96,7 @@ You will need an image on the machine running Node-RED. In this case example.png
You will need a file on the machine running Node-RED. In this case sample.pdf exists inside the Node-RED directory.
![img.png](respond-file-with-file.png)
![respond-file-with-file.png](respond-file-with-file.png)
@@ -98,7 +106,7 @@ You will need a file on the machine running Node-RED. In this case sample.pdf ex
Give a 👍 reaction when someone says "react"
![img.png](respond-react-with-reaction.png)
![respond-react-with-reaction.png](respond-react-with-reaction.png)
@@ -106,8 +114,109 @@ Give a 👍 reaction when someone says "react"
[View JSON](respond-users-list.json)
When someone sends the text "users" they get a HTML message back containing all the current users on the server.
When someone sends the text "users" they get a HTML message back containing all the current users on the server. If your server has a lot of users this paginates and sends a message with 25 users per message.
This requires admin privileges.
![img.png](respond-users-list.png)
![respond-users-list.png](respond-users-list.png)
### Respond to "newroom" by creating new room and inviting user
[View JSON](respond-users-list.json)
When someone sends "newroom" a new room will be created and the user that said the message will be invited. The client will also send a welcome message into the new room.
![respond-newroom-invite.png](respond-newroom-invite.png)
### Respond to "joinroom <room_id_or_alias>" by joining mentioned room
[View JSON](respond-joinroom.json)
When someone sends "newroom" a new room will be created and the user that said the message will be invited. The client will also send a welcome message into the new room.
![respond-joinroom.png](respond-joinroom.png)
### Respond to "rooms <user_id>" with user's rooms (list server's rooms if <user_id> is left blank)
[View JSON](respond-rooms.json)
Responds to "rooms <user_id>" with that user's rooms. If the message is just "rooms" it responds with a list of all rooms the server is participating in.
Note: If there are a lot of rooms this may fail to send the message as it is too large. This also only works for user's that are on the current server.
This requires admin privileges.
![respond-rooms.png](respond-rooms.png)
### Respond to "whois <user_id>" with information about the user's session
[View JSON](respond-whois.json)
This lists out the user's session info. Each session contains the IP address, when it was last seen, and the user agent. Useful to find out more about a specific user on your server.
Note: If there are a lot of sessions this may fail to send the message as it is too large. This also only works for user's that are on the current server.
This requires admin privileges.
![respond-whois.png](respond-whois.png)
### Respond to "room_users" with current room's users
[View JSON](respond-room-users.json)
List out the users participating in a room.
Note: If there are a lot of users in the room this will fail to send due to a large message error.
![respond-room-users.png](respond-room-users.png)
### Download & store all received files/images
[View JSON](store-received-files.json)
Download received files/images. If the file is encrypted it will decrypt it for you. The decrypt node downloads the file for you otherwise you need to use a HTTP Request node to download the file.
Note: You may need to edit the storage directory for this to work. Default action is to create a `downloads` folder in the Node-RED directory and places files in that but there is a good chance your Node-RED instance doesn't have access to write to this directory.
![store-received-files.png](store-received-files.png)
### Kick/Ban user from room
[View JSON](room-kick-ban.json)
If you say "kick @test:example.com" the bot will kick @test:example.com from the current room.
If you say "ban @test:example.com" the bot will ban @test:example.com from the current room.
Note: This requires the bot to have permissions to kick/ban in the current room.
![room-kick-ban.png](room-kick-ban.png)
### Deactivate user
[View JSON](deactivate-user.json)
If you say "deactivate_user @test:example.com" the bot will deactivate the @test:example.com account on the server.
Note: This requires the bot to be a server admin.
WARNING: Accounts that are deleted cannot be restored. If you want to temp-disable edit the user instead.
![room-kick-ban.png](deactivate-user.png)
### Force user to join room
[View JSON](force-join-room.json)
If you say "force_join @test:example.com !320j90mf0394f:example.com" the bot will force the user `@test:example.com` into room `!320j90mf0394f:example.com`
Note: This requires the bot to be a server admin. This also only works for rooms on the same server.
![room-kick-ban.png](force-join-room.png)

View File

@@ -0,0 +1,116 @@
[
{
"id": "843c48978ab1b0a0",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 590,
"y": 3260,
"wires": [
[
"b77d071077487ddc"
]
]
},
{
"id": "93ea76ce5e65a250",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": false,
"acceptStickers": false,
"acceptReactions": false,
"acceptFiles": false,
"acceptImages": false,
"x": 420,
"y": 3260,
"wires": [
[
"843c48978ab1b0a0"
]
]
},
{
"id": "d6795b522954bccd",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"deactivate_user <user_id>\" by deactivating the user on the server",
"info": "",
"x": 590,
"y": 3220,
"wires": []
},
{
"id": "b77d071077487ddc",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload filter",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "regex",
"v": "^deactivate_user",
"vt": "str",
"case": false
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 770,
"y": 3260,
"wires": [
[
"3c0e1e130e91206e"
]
]
},
{
"id": "3c0e1e130e91206e",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let matches = msg.payload.match(/^deactivate_user ?(\\@.*)?/);\nmsg.userId = matches[1].trim() ? matches[1].trim() : null;\nif(!msg.userId) {\n return null;\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 3320,
"wires": [
[
"1f4870b7171cc70f"
]
]
},
{
"id": "1f4870b7171cc70f",
"type": "matrix-synapse-deactivate-user",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"x": 610,
"y": 3320,
"wires": [
[],
[]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,117 @@
[
{
"id": "897b8de477d7d0df",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 470,
"y": 3460,
"wires": [
[
"1f73f36092a9536b"
]
]
},
{
"id": "973b2caeb68f3a60",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": false,
"acceptStickers": false,
"acceptReactions": false,
"acceptFiles": false,
"acceptImages": false,
"x": 300,
"y": 3460,
"wires": [
[
"897b8de477d7d0df"
]
]
},
{
"id": "be5c7290e6223b7b",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Say \"force_join <user_id> <room_id_or_alias>\" to force a user into a room",
"info": "",
"x": 460,
"y": 3420,
"wires": []
},
{
"id": "1f73f36092a9536b",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload filter",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "regex",
"v": "^deactivate_user",
"vt": "str",
"case": false
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 650,
"y": 3460,
"wires": [
[
"6a2a73bc9dfdaece"
]
]
},
{
"id": "6a2a73bc9dfdaece",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let matches = msg.payload.match(/^force_join (\\@.*) (.*)/);\nmsg.userId = matches[1].trim() ? matches[1].trim() : null;\nif(!msg.userId) {\n return null;\n}\nmsg.topic = matches[2].trim() ? matches[2].trim() : null;\nif(!msg.topic) {\n return null;\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 300,
"y": 3520,
"wires": [
[
"212e060e320918d3"
]
]
},
{
"id": "212e060e320918d3",
"type": "matrix-synapse-join-room",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"x": 490,
"y": 3520,
"wires": [
[],
[]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,90 @@
[
{
"id": "9a737b0c57b35063",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 430,
"y": 2020,
"wires": [
[
"2a44927d9317a4b4"
]
]
},
{
"id": "f377ad37af7dc49e",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 260,
"y": 2020,
"wires": [
[
"9a737b0c57b35063"
]
]
},
{
"id": "f7112a1d2808cfbb",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"joinroom <room_id_or_alias>\" by joining the mentioned room",
"info": "",
"x": 440,
"y": 1980,
"wires": []
},
{
"id": "2a44927d9317a4b4",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "msg.payload == \"joinroom\"",
"func": "if(!msg.payload.startsWith(\"joinroom\")) {\n return null;\n}\n\nlet split = msg.payload.split(\" \");\nif(split.length < 2) {\n return; // no room provided\n}\n\nmsg.topic = split[1];\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 640,
"y": 2020,
"wires": [
[
"a34d05c3b6f6bd27"
]
]
},
{
"id": "a34d05c3b6f6bd27",
"type": "matrix-join-room",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"x": 850,
"y": 2020,
"wires": [
[],
[]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,157 @@
[
{
"id": "ea0921f33b58e337",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload is \"newroom\"",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "newroom",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 640,
"y": 1820,
"wires": [
[
"d7ce5027bc1e2eee"
]
]
},
{
"id": "9f9a24376b94c977",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 430,
"y": 1820,
"wires": [
[
"ea0921f33b58e337"
]
]
},
{
"id": "3bce23488138c013",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 260,
"y": 1820,
"wires": [
[
"9f9a24376b94c977"
]
]
},
{
"id": "d7ce5027bc1e2eee",
"type": "matrix-create-room",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"x": 250,
"y": 1880,
"wires": [
[
"735b9d9fd1401e96"
],
[]
]
},
{
"id": "735b9d9fd1401e96",
"type": "matrix-invite-room",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"x": 410,
"y": 1880,
"wires": [
[
"089f2a7f0c079b6b"
],
[]
]
},
{
"id": "222ee3c08028c1df",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"messageType": "m.text",
"messageFormat": "",
"x": 760,
"y": 1880,
"wires": [
[],
[]
]
},
{
"id": "089f2a7f0c079b6b",
"type": "change",
"z": "f025a8b9fbd1b054",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "Welcome to my new room!",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 580,
"y": 1880,
"wires": [
[
"222ee3c08028c1df"
]
]
},
{
"id": "48a1fd26227f277e",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"newroom\" by creating new room and inviting person that said it then send welcome message",
"info": "",
"x": 520,
"y": 1780,
"wires": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,134 @@
[
{
"id": "a7581a6224cbd24d",
"type": "matrix-room-users",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"x": 440,
"y": 2800,
"wires": [
[
"3e37b5fded58782d"
],
[]
]
},
{
"id": "2764620d2d514996",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 610,
"y": 2740,
"wires": [
[
"f487951ea1b22a95"
]
]
},
{
"id": "7ed8fc36ba75298a",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 440,
"y": 2740,
"wires": [
[
"2764620d2d514996"
]
]
},
{
"id": "f487951ea1b22a95",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload filter",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "room_users",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 790,
"y": 2740,
"wires": [
[
"a7581a6224cbd24d"
]
]
},
{
"id": "3e37b5fded58782d",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "Room List (for user or all)",
"func": "let payload = null;\nif(!msg.payload || !msg.payload.joined) {\n payload = 'No users in room.';\n} else {\n payload = `Room ${msg.topic} users:`;\n payload += '<ul>';\n for(let user_id in msg.payload.joined) {\n payload += `<li>${msg.payload.joined[user_id].display_name} - ${user_id}</li>`;\n }\n payload += '</ul>'\n}\n\nif(payload){\n msg.payload = payload;\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 650,
"y": 2800,
"wires": [
[
"86a4895c4fe46657"
]
]
},
{
"id": "86a4895c4fe46657",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "Send HTML Message",
"server": null,
"roomId": "",
"messageType": "m.text",
"messageFormat": "html",
"x": 880,
"y": 2800,
"wires": [
[],
[]
]
},
{
"id": "cae1a37fe034b8f8",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"room_users\" with that room's users",
"info": "",
"x": 600,
"y": 2700,
"wires": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

151
examples/respond-rooms.json Normal file
View File

@@ -0,0 +1,151 @@
[
{
"id": "2eb2dd1b5dbdd03b",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Response to \"rooms\" with all rooms",
"info": "",
"x": 580,
"y": 2500,
"wires": []
},
{
"id": "f3415a10f124d586",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let matches = msg.payload.match(/^rooms ?(\\@.*)?/);\nmsg.userId = matches[1] ? matches[1] : null;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 2620,
"wires": [
[
"2cae98732747989d"
]
]
},
{
"id": "2cae98732747989d",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "Room List (for user or all)",
"func": "let matrixClient = global.get(\"matrixClient['@bot:example.com']\"),\n matrixOnline = global.get(\"matrixClientOnline['@bot:example.com']\");\n\nif(!matrixOnline) {\n msg.payload = 'Matrix client not connected.';\n return [null, msg];\n}\n\nif(msg.userId) {\n matrixClient.http\n .authedRequest(\n undefined,\n 'GET',\n `/_synapse/admin/v1/users/${msg.userId}/joined_rooms`,\n {}, // query params\n undefined,\n { prefix: '' }\n ).then(function(e){\n msg.payload = e;\n if(!e.joined_rooms) {\n msg.payload = 'User is not part of any rooms.';\n } else {\n msg.payload = `${msg.userId}'s rooms:`;\n msg.payload += '<ul>';\n e.joined_rooms.forEach(function(room_id){\n msg.payload += `<li>${room_id}</li>`;\n });\n msg.payload += '</ul>'\n }\n node.send([msg, null]);\n }).catch(function(e){\n node.warn(\"Error fetching server user list \" + e);\n msg.payload = e;\n node.send([null, msg]);\n });\n} else {\n matrixClient.http\n .authedRequest(\n undefined,\n 'GET',\n `/_synapse/admin/v1/rooms`,\n {}, // query params\n undefined,\n { prefix: '' }\n ).then(function(e){\n msg.payload = e;\n if(!e.rooms) {\n msg.payload = 'Server has no rooms.';\n } else {\n msg.payload = `Server rooms:`;\n msg.payload += '<ul>';\n e.rooms.forEach(function(room){\n msg.payload += `<li>${room.name} - ${room.room_id} (members: ${room.joined_members} | creator: ${room.creator})</li>`;\n });\n msg.payload += '</ul>'\n }\n node.send([msg, null]);\n }).catch(function(e){\n node.warn(\"Error fetching server user list \" + e);\n msg.payload = e;\n node.send([null, msg]);\n });\n}",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 610,
"y": 2620,
"wires": [
[
"d7508993ec1ce895"
],
[
"d7508993ec1ce895"
]
]
},
{
"id": "2f8c696ec0dc8f6d",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"rooms <user_id>\" with that user's rooms",
"info": "",
"x": 590,
"y": 2540,
"wires": []
},
{
"id": "d7508993ec1ce895",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "Send HTML Message",
"server": null,
"roomId": "",
"messageType": "m.text",
"messageFormat": "html",
"x": 840,
"y": 2620,
"wires": [
[],
[]
]
},
{
"id": "13a9a0331352338a",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 590,
"y": 2580,
"wires": [
[
"afe88b8e115f661f"
]
]
},
{
"id": "e261cc12da739901",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 420,
"y": 2580,
"wires": [
[
"13a9a0331352338a"
]
]
},
{
"id": "afe88b8e115f661f",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload filter",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "regex",
"v": "^rooms",
"vt": "str",
"case": false
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 770,
"y": 2580,
"wires": [
[
"f3415a10f124d586"
]
]
}
]

BIN
examples/respond-rooms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,27 +1,257 @@
[
{
"id": "a9b412957063b06c",
"type": "matrix-receive",
"id": "49e4b3a1ee3e9e1e",
"type": "matrix-synapse-users",
"z": "f025a8b9fbd1b054",
"name": "",
"roomId": "",
"ignoreText": false,
"ignoreReactions": false,
"ignoreFiles": false,
"ignoreImages": false,
"x": 460,
"y": 120,
"server": null,
"x": 610,
"y": 2240,
"wires": [
[
"4aa45cd8653ba898"
"d4978f7c2dad7ecf"
],
[
"da9d25c324cb727e"
]
]
},
{
"id": "4aa45cd8653ba898",
"id": "e199d3db1ea6757c",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "Send HTML Notice",
"server": null,
"roomId": "",
"messageType": "m.notice",
"messageFormat": "html",
"x": 970,
"y": 2240,
"wires": [
[
"e248881d6749ba70"
],
[
"da9d25c324cb727e"
]
]
},
{
"id": "d4978f7c2dad7ecf",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "msg.next_token = msg.payload.next_token || false;\nlet new_payload = '<ul>';\nmsg.payload.users.forEach(function(user, index){\n if(msg.guests == 'false' && user.is_guest) {\n return;\n }\n new_payload += '<li>'+ JSON.stringify(user, null, 2).replace(/(?:\\r\\n|\\r|\\n)/g, \"\\n<br>\"); + '</li>'\n});\nmsg.payload = new_payload + '</ul>';\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 800,
"y": 2240,
"wires": [
[
"e199d3db1ea6757c"
]
]
},
{
"id": "880cac7307d9a786",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Response to \"users\" with full server user list (excluding guests)",
"info": "",
"x": 620,
"y": 2160,
"wires": []
},
{
"id": "469d2ebd9995747a",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"messageType": "m.notice",
"messageFormat": "",
"x": 600,
"y": 2300,
"wires": [
[],
[]
]
},
{
"id": "da9d25c324cb727e",
"type": "change",
"z": "f025a8b9fbd1b054",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "error",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 420,
"y": 2300,
"wires": [
[
"469d2ebd9995747a"
]
]
},
{
"id": "86577e484ca26aca",
"type": "change",
"z": "f025a8b9fbd1b054",
"name": "",
"rules": [
{
"t": "set",
"p": "guests",
"pt": "msg",
"to": "false",
"tot": "str"
},
{
"t": "set",
"p": "limit",
"pt": "msg",
"to": "25",
"tot": "num"
},
{
"t": "set",
"p": "order_by",
"pt": "msg",
"to": "creation_ts",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 420,
"y": 2240,
"wires": [
[
"49e4b3a1ee3e9e1e"
]
]
},
{
"id": "e248881d6749ba70",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload == \"users\"",
"name": "More to paginate?",
"property": "next_token",
"propertyType": "msg",
"rules": [
{
"t": "istype",
"v": "string",
"vt": "string"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 1190,
"y": 2240,
"wires": [
[
"11d7fcf6335bc75b"
]
]
},
{
"id": "11d7fcf6335bc75b",
"type": "change",
"z": "f025a8b9fbd1b054",
"name": "",
"rules": [
{
"t": "set",
"p": "from",
"pt": "msg",
"to": "next_token",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1370,
"y": 2240,
"wires": [
[
"49e4b3a1ee3e9e1e"
]
]
},
{
"id": "8b48e27a118999ec",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 590,
"y": 2200,
"wires": [
[
"7a64319e26e8b689"
]
]
},
{
"id": "553da6dd77e5e03b",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 420,
"y": 2200,
"wires": [
[
"8b48e27a118999ec"
]
]
},
{
"id": "7a64319e26e8b689",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload filter",
"property": "payload",
"propertyType": "msg",
"rules": [
@@ -34,70 +264,12 @@
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 670,
"y": 120,
"x": 770,
"y": 2200,
"wires": [
[
"b95a674179d93416"
"86577e484ca26aca"
]
]
},
{
"id": "b95a674179d93416",
"type": "matrix-synapse-users",
"z": "f025a8b9fbd1b054",
"name": "",
"x": 890,
"y": 120,
"wires": [
[
"19b654e5f28d93c4"
],
[]
]
},
{
"id": "8720c66e867f89f6",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "Send HTML Notice",
"roomId": "",
"messageType": "m.notice",
"messageFormat": "html",
"x": 1230,
"y": 120,
"wires": [
[],
[]
]
},
{
"id": "19b654e5f28d93c4",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let new_payload = '<ul>';\nmsg.payload.users.forEach(function(user, index){\n new_payload += '<li>'+ JSON.stringify(user, null, 2).replace(/(?:\\r\\n|\\r|\\n)/g, \"\\n<br>\"); + '</li>'\n});\nmsg.payload = new_payload + '</ul>';\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1060,
"y": 120,
"wires": [
[
"8720c66e867f89f6"
]
]
},
{
"id": "563da45ab1747c2b",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Response to \"users\" with full server user list",
"info": "",
"x": 550,
"y": 80,
"wires": []
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 44 KiB

153
examples/respond-whois.json Normal file
View File

@@ -0,0 +1,153 @@
[
{
"id": "93df869346a728d3",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Response to \"whois <user_id>\" with user information",
"info": "",
"x": 610,
"y": 2360,
"wires": []
},
{
"id": "656ed1e444fd2e0a",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let matches = msg.payload.match(/^whois ?(\\@.*)?/);\nmsg.userId = matches[1].trim() ? matches[1].trim() : msg.userId;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 2440,
"wires": [
[
"4869676dab929d20"
]
]
},
{
"id": "4869676dab929d20",
"type": "matrix-whois-user",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"x": 590,
"y": 2440,
"wires": [
[
"a894fcabf9e41dca"
],
[]
]
},
{
"id": "a894fcabf9e41dca",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "try { \n var sessions = msg.payload.devices[\"\"].sessions || null;\n} catch (error) { \n var sessions = null;\n}\nlet html = \"whois (session) information for <strong>\" + msg.userId + \"</strong>\\n<ol>\";\nfor(let session of sessions) {\n try { \n var connections = session.connections;\n } catch (error) { \n var connections = null;\n }\n for(let connection of connections) {\n html += \"\\n<li><pre><code>\"+JSON.stringify(connection, null, 2)+\"</code></pre></li>\"\n }\n}\nhtml += \"</ol>\"\nmsg.payload = html;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 740,
"y": 2440,
"wires": [
[
"898b8974f2b24861"
]
]
},
{
"id": "898b8974f2b24861",
"type": "matrix-send-message",
"z": "f025a8b9fbd1b054",
"name": "Send HTML Notice",
"server": null,
"roomId": "",
"messageType": "m.notice",
"messageFormat": "html",
"x": 910,
"y": 2440,
"wires": [
[],
[]
]
},
{
"id": "c8855c79f194796e",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 590,
"y": 2400,
"wires": [
[
"ae1537a16b9e3b4c"
]
]
},
{
"id": "39109d4f02d241ca",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 420,
"y": 2400,
"wires": [
[
"c8855c79f194796e"
]
]
},
{
"id": "ae1537a16b9e3b4c",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload filter",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "regex",
"v": "^whois",
"vt": "str",
"case": false
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 770,
"y": 2400,
"wires": [
[
"656ed1e444fd2e0a"
]
]
}
]

BIN
examples/respond-whois.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

211
examples/room-kick-ban.json Normal file
View File

@@ -0,0 +1,211 @@
[
{
"id": "d78dd157085ed843",
"type": "matrix-room-kick",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"x": 570,
"y": 2980,
"wires": [
[
"44d49fcaad67790c"
],
[
"44d49fcaad67790c"
]
]
},
{
"id": "32a99c44b50fe3f3",
"type": "matrix-room-ban",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"x": 570,
"y": 3080,
"wires": [
[
"04628a976e7acf1b"
],
[
"04628a976e7acf1b"
]
]
},
{
"id": "5e786bbae3b1a368",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "is from me",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 230,
"y": 3020,
"wires": [
[
"5b3ddae8335383e7"
]
]
},
{
"id": "ca278a6ffc3b9d6b",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": false,
"acceptStickers": false,
"acceptReactions": false,
"acceptFiles": false,
"acceptImages": false,
"x": 220,
"y": 2960,
"wires": [
[
"5e786bbae3b1a368"
]
]
},
{
"id": "b6830f430157be4e",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"room_ban <user_id>\" by banning user from room",
"info": "",
"x": 580,
"y": 3040,
"wires": []
},
{
"id": "8bffe425d3c1bf24",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Respond to \"room_kick <user_id>\" by kicking user from room",
"info": "",
"x": 580,
"y": 2940,
"wires": []
},
{
"id": "5b3ddae8335383e7",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "Kick or Ban",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "regex",
"v": "^room_kick",
"vt": "str",
"case": false
},
{
"t": "regex",
"v": "^room_ban",
"vt": "str",
"case": false
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 230,
"y": 3060,
"wires": [
[
"a19b38afd7b52834"
],
[
"925c59f485bf7247"
]
]
},
{
"id": "925c59f485bf7247",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let matches = msg.payload.match(/^room_ban ?(\\@.*)?/);\nmsg.userId = matches[1].trim() ? matches[1].trim() : null;\nif(!msg.userId) {\n return null;\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 3080,
"wires": [
[
"32a99c44b50fe3f3"
]
]
},
{
"id": "a19b38afd7b52834",
"type": "function",
"z": "f025a8b9fbd1b054",
"name": "",
"func": "let matches = msg.payload.match(/^room_kick ?(\\@.*)?/);\nmsg.userId = matches[1].trim() ? matches[1].trim() : null;\nif(!msg.userId) {\n return null;\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 2980,
"wires": [
[
"d78dd157085ed843"
]
]
},
{
"id": "04628a976e7acf1b",
"type": "debug",
"z": "f025a8b9fbd1b054",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 710,
"y": 3080,
"wires": []
},
{
"id": "44d49fcaad67790c",
"type": "debug",
"z": "f025a8b9fbd1b054",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 710,
"y": 2980,
"wires": []
}
]

BIN
examples/room-kick-ban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,166 @@
[
{
"id": "d7573ed76980dc64",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": false,
"acceptEmotes": false,
"acceptStickers": false,
"acceptReactions": false,
"acceptFiles": true,
"acceptImages": true,
"x": 1740,
"y": 600,
"wires": [
[
"4d4e992ed895413f"
]
]
},
{
"id": "addcccdfea9935b1",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Download & store received files/images and decrypt if necessary",
"info": "",
"x": 1970,
"y": 560,
"wires": []
},
{
"id": "4d4e992ed895413f",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "",
"property": "encrypted",
"propertyType": "msg",
"rules": [
{
"t": "true"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 1890,
"y": 600,
"wires": [
[
"660782188696fc62"
],
[
"857bce169036e3cb"
]
]
},
{
"id": "660782188696fc62",
"type": "matrix-decrypt-file",
"z": "f025a8b9fbd1b054",
"name": null,
"x": 2050,
"y": 600,
"wires": [
[
"cdbaf88f6631322e"
],
[]
]
},
{
"id": "b62db6caf4697242",
"type": "file",
"z": "f025a8b9fbd1b054",
"name": "",
"filename": "",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 2240,
"y": 660,
"wires": [
[]
]
},
{
"id": "857bce169036e3cb",
"type": "http request",
"z": "f025a8b9fbd1b054",
"name": "",
"method": "GET",
"ret": "bin",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"senderr": false,
"x": 1910,
"y": 660,
"wires": [
[
"7d0be52f8c3f1b54"
]
]
},
{
"id": "7d0be52f8c3f1b54",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "",
"property": "statusCode",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "200",
"vt": "num"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 2050,
"y": 660,
"wires": [
[
"cdbaf88f6631322e"
]
]
},
{
"id": "cdbaf88f6631322e",
"type": "change",
"z": "f025a8b9fbd1b054",
"name": "set directory",
"rules": [
{
"t": "set",
"p": "filename",
"pt": "msg",
"to": "\"downloads/\" & msg.filename",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 2230,
"y": 600,
"wires": [
[
"b62db6caf4697242"
]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

8076
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
{
"name": "node-red-contrib-matrix-chat",
"version": "0.2.7",
"version": "0.4.6",
"description": "Matrix chat server client for Node-RED",
"dependencies": {
"fs-extra": "^9.1.0",
"got": "^11.8.2",
"fs-extra": "^10.0.0",
"got": "^12.0.1",
"isomorphic-webcrypto": "^2.3.8",
"matrix-js-sdk": "^12.2.0",
"matrix-js-sdk": "^15.5.0",
"node-localstorage": "^2.2.1",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"process": "^0.11.10",
"utf8": "^3.0.0"
},
"node-red": {
"version": ">=1.3.0",
"nodes": {
"matrix-server-config": "src/matrix-server-config.js",
"matrix-receive": "src/matrix-receive.js",
@@ -32,9 +32,14 @@
"matrix-synapse-deactivate-user": "src/matrix-synapse-deactivate-user.js",
"matrix-synapse-join-room": "src/matrix-synapse-join-room.js",
"matrix-whois-user": "src/matrix-whois-user.js",
"matrix-room-users": "src/matrix-room-users.js"
"matrix-room-users": "src/matrix-room-users.js",
"matrix-device-verify": "src/matrix-device-verify.js",
"matrix-secret-storage": "src/matrix-secret-storage.js"
}
},
"engines": {
"node": ">=14.0.0"
},
"keywords": [
"node-red",
"matrix",

View File

@@ -1,5 +1,4 @@
module.exports = function(RED) {
const got = require('got');
const crypto = require('isomorphic-webcrypto');
function MatrixDecryptFile(n) {
@@ -10,6 +9,8 @@ module.exports = function(RED) {
this.name = n.name;
node.on("input", async function (msg) {
const { got } = await import('got');
if(!msg.type) {
node.error('msg.type is required.');
return;

View File

@@ -0,0 +1,308 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-device-verify-request', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
inputs: 0,
outputs: 1,
defaults: {
name: { value: null },
server: { value: "", type: "matrix-server-config" }
},
label: function() {
return this.name || "Device Verify Request";
},
paletteLabel: 'Device Verify Request'
});
</script>
<script type="text/html" data-template-name="matrix-device-verify-request">
<div class="form-row">
<label for="node-input-name"><i class="icon-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>
</script>
<script type="text/html" data-help-name="matrix-device-verify-request">
<h3>Details</h3>
<p>
This API invites a user to participate in a particular room. They do not start participating in the room until they actually join the room.
</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>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> The room identifier to invite to: for example, <code>!h8zld9j31:example.com. If configured on the node it overrides this input and is no longer required.</code>.</dd>
<dt>msg.userId
<span class="property-type">string</span>
</dt>
<dd> User's ID that will be invited to the room.</dd>
<dt class="optional">msg.reason
<span class="property-type">string</span>
</dt>
<dd> Reason for the membership change.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.payload <span class="property-type">object</span></dt>
<dd>Currently this endpoint returns an empty object</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
</script>
<script type="text/javascript">
RED.nodes.registerType('matrix-device-verify-start', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
inputs: 1,
outputs: 1,
defaults: {
name: { value: null },
server: { value: "", type: "matrix-server-config" }
},
label: function() {
return this.name || "Device Verify Start";
},
paletteLabel: 'Device Verify Start'
});
</script>
<script type="text/html" data-template-name="matrix-device-verify-start">
<div class="form-row">
<label for="node-input-name"><i class="icon-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>
</script>
<script type="text/html" data-help-name="matrix-device-verify-start">
<h3>Details</h3>
<p>
This API invites a user to participate in a particular room. They do not start participating in the room until they actually join the room.
</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>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> The room identifier to invite to: for example, <code>!h8zld9j31:example.com. If configured on the node it overrides this input and is no longer required.</code>.</dd>
<dt>msg.userId
<span class="property-type">string</span>
</dt>
<dd> User's ID that will be invited to the room.</dd>
<dt class="optional">msg.reason
<span class="property-type">string</span>
</dt>
<dd> Reason for the membership change.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.payload <span class="property-type">object</span></dt>
<dd>Currently this endpoint returns an empty object</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
</script>
<script type="text/javascript">
RED.nodes.registerType('matrix-device-verify-cancel', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
inputs: 1,
outputs: 1,
defaults: {
name: { value: null },
server: { value: "", type: "matrix-server-config" }
},
label: function() {
return this.name || "Device Verify Cancel";
},
paletteLabel: 'Device Verify Cancel'
});
</script>
<script type="text/html" data-template-name="matrix-device-verify-cancel">
<div class="form-row">
<label for="node-input-name"><i class="icon-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>
</script>
<script type="text/html" data-help-name="matrix-device-verify-cancel">
<h3>Details</h3>
<p>
This API invites a user to participate in a particular room. They do not start participating in the room until they actually join the room.
</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>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> The room identifier to invite to: for example, <code>!h8zld9j31:example.com. If configured on the node it overrides this input and is no longer required.</code>.</dd>
<dt>msg.userId
<span class="property-type">string</span>
</dt>
<dd> User's ID that will be invited to the room.</dd>
<dt class="optional">msg.reason
<span class="property-type">string</span>
</dt>
<dd> Reason for the membership change.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.payload <span class="property-type">object</span></dt>
<dd>Currently this endpoint returns an empty object</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
</script>
<script type="text/javascript">
RED.nodes.registerType('matrix-device-verify-accept', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
inputs: 1,
outputs: 1,
defaults: {
name: { value: null },
server: { value: "", type: "matrix-server-config" }
},
label: function() {
return this.name || "Device Verify Accept";
},
paletteLabel: 'Device Verify Accept'
});
</script>
<script type="text/html" data-template-name="matrix-device-verify-accept">
<div class="form-row">
<label for="node-input-name"><i class="icon-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>
</script>
<script type="text/html" data-help-name="matrix-device-verify-accept">
<h3>Details</h3>
<p>
This API invites a user to participate in a particular room. They do not start participating in the room until they actually join the room.
</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>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> The room identifier to invite to: for example, <code>!h8zld9j31:example.com. If configured on the node it overrides this input and is no longer required.</code>.</dd>
<dt>msg.userId
<span class="property-type">string</span>
</dt>
<dd> User's ID that will be invited to the room.</dd>
<dt class="optional">msg.reason
<span class="property-type">string</span>
</dt>
<dd> Reason for the membership change.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.payload <span class="property-type">object</span></dt>
<dd>Currently this endpoint returns an empty object</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
</script>

271
src/matrix-device-verify.js Normal file
View File

@@ -0,0 +1,271 @@
module.exports = function(RED) {
const verificationRequests = new Map();
function MatrixDeviceVerifyRequest(n) {
RED.nodes.createNode(this, n);
var node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
if (!node.server) {
node.warn("No configuration node");
return;
}
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" });
});
/**
* 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("crypto.verification.request", async function(data){
console.log("[######### crypto.verification.request #########]", data.phase, data);
if(data.phase === 5 || data.phase === 6) {
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,
type: 'crypto.verification.request',
selfVerification: data.isSelfVerification
});
}
});
}
RED.nodes.registerType("matrix-device-verify-request", MatrixDeviceVerifyRequest);
function MatrixDeviceVerifyStart(n) {
RED.nodes.createNode(this, n);
var node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
if (!node.server) {
node.warn("No configuration node");
return;
}
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('close', function(done) {
verificationRequests.clear();
done();
});
node.on('input', async function(msg){
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
// if(msg.userId && msg.deviceId) {
// node.server.beginKeyVerification("m.sas.v1", msg.userId, msg.deviceId);
// }
node.error("Invaid verification request: " + (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() {
var that = this;
console.log("[##### VERIFICATION PHASE CHANGE #######]", this.phase);
if(this.phase === 4) {
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 verificaiton cancelled " + e);
verifierCancel();
});
let show_sas = function(e) {
// 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 = e.sas;
msg.emojis = e.sas.emoji.map(function(emoji, i) {
return emoji[0];
});
msg.emojis_text = e.sas.emoji.map(function(emoji, i) {
return emoji[1];
});
node.send(msg);
};
data._verifier.on('show_sas', show_sas);
data._verifier.verify()
.then(function(e){
console.log("!!!!!!!!!!! VERIFY THEN", e);
data._verifier.off('show_sas', show_sas);
data._verifier.done();
}, function(e) {
verifierCancel();
node.warn(e);
// @todo return over second output
});
}
});
data.emit("change");
await data.accept();
} catch(e) {
console.log("ERROR", e);
}
}
});
}
RED.nodes.registerType("matrix-device-verify-start", MatrixDeviceVerifyStart);
function MatrixDeviceVerifyCancel(n) {
RED.nodes.createNode(this, n);
var node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
if (!node.server) {
node.warn("No configuration node");
return;
}
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('close', function(done) {
verificationRequests.clear();
done();
});
node.on('input', async function(msg){
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
node.error("Invaid verification request: " + (msg.verifyRequestId || null));
}
var data = verificationRequests.get(msg.verifyRequestId);
if(data) {
data.cancel();
}
});
}
RED.nodes.registerType("matrix-device-verify-cancel", MatrixDeviceVerifyCancel);
function MatrixDeviceVerifyAccept(n) {
RED.nodes.createNode(this, n);
var node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
if (!node.server) {
node.warn("No configuration node");
return;
}
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('close', function(done) {
verificationRequests.clear();
done();
});
node.on('input', async function(msg){
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
node.error("Invaid verification request: " + (msg.verifyRequestId || null));
}
var data = verificationRequests.get(msg.verifyRequestId);
if(data._verifier && data._verifier.sasEvent) {
data._verifier.sasEvent.confirm()
.then(function(e){
console.log("!!!!!!!! CONFIRMED VERIFY", e);
})
.catch(function(e) {
console.log("!!!!!!!! CONFIRMED VERIFY FAILED", e);
});
} else {
console.log("Verification must be started", data);
node.error("Verification must be started");
}
});
}
RED.nodes.registerType("matrix-device-verify-accept", MatrixDeviceVerifyAccept);
}

View File

@@ -38,11 +38,6 @@ module.exports = function(RED) {
return;
}
if(!msg.userId) {
node.error("msg.userId was not set.");
return;
}
node.server.matrixClient.joinRoom(msg.topic, msg.joinOpts || {})
.then(function(e) {
msg.payload = e;

View File

@@ -53,16 +53,14 @@ module.exports = function(RED) {
msg.type = 'm.reaction';
node.server.matrixClient.sendCompleteEvent(
node.server.matrixClient.sendEvent(
msg.topic,
'm.reaction',
{
type: 'm.reaction',
content: {
"m.relates_to": {
event_id: eventId,
key: msg.payload,
rel_type: "m.annotation"
}
"m.relates_to": {
event_id: eventId,
key: msg.payload,
rel_type: "m.annotation"
}
}
)

View File

@@ -204,6 +204,11 @@
</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>
@@ -216,6 +221,11 @@
</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>

View File

@@ -22,7 +22,7 @@ module.exports = function(RED) {
return;
}
node.server.on("disconnected", function() {
node.server.on("disconnected", function(){
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.server.on("Room.timeline", async function(event, room, toStartOfTimeline, data) {
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
}
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
// if node has a room ID set we only listen on that room
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) {
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) {
case 'm.emote':
if(!node.acceptEmotes) return;
@@ -93,6 +62,7 @@ module.exports = function(RED) {
case 'm.file':
if(!node.acceptFiles) return;
msg.filename = msg.content.filename || msg.content.body;
if(msg.encrypted) {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
msg.mxc_url = msg.content.file.url;
@@ -104,7 +74,7 @@ module.exports = function(RED) {
case 'm.image':
if(!node.acceptImages) return;
msg.filename = msg.content.filename || msg.content.body;
if(msg.encrypted) {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
msg.mxc_url = msg.content.file.url;

View File

@@ -31,9 +31,6 @@
<label for="node-input-server"><i class="fa fa-user"></i> Room Id</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic">
</div>
<div class="form-tips">
This only works on Synapse servers. The user also must be an administrator.
</div>
</script>
<script type="text/html" data-help-name="matrix-room-users">

View File

@@ -0,0 +1,71 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-secret-storage', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
inputs: 1,
outputs: 1,
defaults: {
name: { value: null },
server: { value: "", type: "matrix-server-config" }
},
label: function() {
return this.name || "Secret Storage";
},
paletteLabel: 'Secret Storage'
});
</script>
<script type="text/html" data-template-name="matrix-secret-storage">
<div class="form-row">
<label for="node-input-name"><i class="icon-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>
</script>
<script type="text/html" data-help-name="matrix-secret-storage">
<h3>Details</h3>
<p>
Secure backup node. Use this to setup security key backup to the remote server. You can also use this node to import an existing secure backup.
</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>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> The room identifier to invite to: for example, <code>!h8zld9j31:example.com. If configured on the node it overrides this input and is no longer required.</code>.</dd>
<dt>msg.userId
<span class="property-type">string</span>
</dt>
<dd> User's ID that will be invited to the room.</dd>
<dt class="optional">msg.reason
<span class="property-type">string</span>
</dt>
<dd> Reason for the membership change.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.payload <span class="property-type">object</span></dt>
<dd>Currently this endpoint returns an empty object</dd>
</dl>
</li>
<li>Error
<dl class="message-properties">
<dt>msg.error <span class="property-type">string</span></dt>
<dd>the error that occurred.</dd>
</dl>
</li>
</ol>
</script>

View File

@@ -0,0 +1,92 @@
module.exports = function(RED) {
const verificationRequests = new Map();
function MatrixSecretStorage(n) {
RED.nodes.createNode(this, n);
var node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
if (!node.server) {
node.warn("No configuration node");
return;
}
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){
try {
msg.hasSecretStorage = await node.server.matrixClient.hasSecretStorageKey();
} catch(e) {
console.log("ERROR", e);
}
if(msg.action) {
if(msg.action === 'create') {
if(msg.hasSecretStorage && !msg.forceReset) {
node.error("Secret storage already setup. Pass msg.forceReset to bypass and regenerate.");
return;
}
// copying this from https://github.com/matrix-org/matrix-react-sdk/blob/e78a1adb6f1af2ea425b0bae9034fb7344a4b2e8/src/SecurityManager.ts#L294
const recoveryKey = await node.server.matrixClient.createRecoveryKeyFromPassphrase(msg.key || undefined);
if(msg.forceReset) {
await node.server.matrixClient.bootstrapSecretStorage({
createSecretStorageKey: async () => recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
} else {
// For password authentication users after 2020-09, this cross-signing
// step will be a no-op since it is now setup during registration or login
// when needed. We should keep this here to cover other cases such as:
// * Users with existing sessions prior to 2020-09 changes
// * SSO authentication users which require interactive auth to upload
// keys (and also happen to skip all post-authentication flows at the
// moment via token login)
await node.server.matrixClient.bootstrapCrossSigning({
// maybe we can skip this?
// authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
});
const backupInfo = await node.server.matrixClient.getKeyBackupVersion();
await node.server.matrixClient.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: backupInfo,
setupNewKeyBackup: !backupInfo,
getKeyBackupPassphrase: () => {
// We may already have the backup key if we earlier went
// through the restore backup path, so pass it along
// rather than prompting again.
if (this._backupKey) {
return this._backupKey;
}
return promptForBackupPassphrase();
},
});
}
}
if(msg.action === 'download') {
if(!msg.hasSecretStorage) {
node.error("Secret storage not setup so cannot download.");
return;
}
}
}
node.send(msg);
});
}
RED.nodes.registerType("matrix-secret-storage", MatrixSecretStorage);
}

View File

@@ -43,7 +43,7 @@
<script type="text/html" data-help-name="matrix-send-image">
<h3>Details</h3>
<p>This node will send an image to a Matrix chat room. Supports direct linking to a File In node.</p>
<p>This node will send an image to a Matrix chat room. Supports direct linking to a File In node. It's recommended you set <code>msg.contentType</code> so the client knows how to render the image (otherwise the image could show blank).</p>
<h3>Inputs</h3>
<dl class="message-properties">

View File

@@ -72,20 +72,28 @@ module.exports = function(RED) {
})
.then(function(file){
node.server.matrixClient
.sendImageMessage(msg.topic, file.content_uri, {}, (msg.body || msg.filename) || "")
.sendImageMessage(
msg.topic,
file.content_uri,
{},
(msg.body || msg.filename) || null,
null
)
.then(function(e) {
node.log("Image message sent: " + e);
msg.eventId = e.event_id;
msg.content_uri_mxc = file.content_uri;
msg.content_uri = node.server.matrixClient.mxcUrlToHttp(file.content_uri);
node.send([msg, null]);
})
.catch(function(e){
node.warn("Error sending image message " + e);
node.warn("Error sending image message: " + e);
msg.error = e;
node.send([null, msg]);
});
})
.catch(function(e){
node.warn("Error uploading image message " + e);
node.warn("Error uploading image message: " + e);
msg.error = e;
node.send([null, msg]);
});

View File

@@ -1,11 +1,36 @@
<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">
RED.nodes.registerType('matrix-server-config',{
category: 'config',
color: '#00b7ca',
credentials: {
userId: { type: "text", required: true },
deviceLabel: { type: "text", required: false },
accessToken: { type: "password", required: true },
deviceId: { type: "text", required: true },
secureStoragePassphrase: { type: "text", required: false },
url: { type: "text", required: true }
},
defaults: {
@@ -26,28 +51,59 @@
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<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">
</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">
<label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label>
<input type="text" id="node-config-input-accessToken">
</div>
<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 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">
</div>
<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 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">
<label for="node-config-input-secureStoragePassphrase"><i class="fa fa-key"></i> Secure Storage Passphrase</label>
<input type="text" id="node-config-input-secureStoragePassphrase">
</div>
<div class="form-tips" style="margin-bottom: 12px;">
If set secure storage will be setup with this passphrase. If secure storage already exists on the account it will attempt to decrypt it. Leave blank to skip.
</div>
<div class="form-row">
<input
type="checkbox"
@@ -65,7 +121,7 @@
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-config-input-enableE2ee" style="width: auto;max-width:50%;">
Enable end-to-end encryption (requires a Device ID to be set.)
Enable end-to-end encryption
</label>
</div>
@@ -79,10 +135,85 @@
Global access to Matrix Client
</label>
<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>
</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();
}
);
});
$(function(){
$("#node-config-input-enableE2ee").on('change', function(e){
$("#node-config-input-secureStoragePassphrase").attr('disabled', !$(this).is(':checked'));
}).change();
});
</script>
</script>
<script type="text/html" data-help-name="matrix-server-config">

View File

@@ -1,8 +1,10 @@
global.Olm = require('olm');
const fs = require("fs-extra");
const sdk = require("matrix-js-sdk");
const { resolve } = require('path');
const { LocalStorage } = require('node-localstorage');
const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
const {deriveKey} = require("matrix-js-sdk/lib/crypto/key_passphrase");
module.exports = function(RED) {
function MatrixFolderNameFromUserId(name) {
@@ -10,40 +12,47 @@ module.exports = function(RED) {
}
function MatrixServerNode(n) {
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
let node = this,
storageDir = RED.settings.userDir + '/matrix-client-storage';
RED.nodes.createNode(this, n);
node.setMaxListeners(1000);
let node = this;
node.log("Initializing Matrix Server Config node");
if(!this.credentials) {
this.credentials = {};
}
node.setMaxListeners(1000);
this.connected = null;
this.name = n.name;
this.userId = this.credentials.userId;
this.deviceLabel = this.credentials.deviceLabel || null;
this.deviceId = this.credentials.deviceId || null;
this.secureStoragePassphrase = this.credentials.secureStoragePassphrase || null;
this.url = this.credentials.url;
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
this.enableE2ee = n.enableE2ee || false;
this.e2ee = (this.enableE2ee && this.deviceId);
this.globalAccess = n.global;
this.initializedAt = new Date();
if(!this.userId) {
node.log("Matrix connection failed: missing user ID in configuration.");
return;
}
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId),
localStorage = new LocalStorage(localStorageDir),
initialSetup = false;
let retryStartTimeout = null;
if(!this.credentials.accessToken) {
node.log("Matrix connection failed: missing access token.");
node.error("Matrix connection failed: missing access token in configuration.");
} else if(!this.url) {
node.log("Matrix connection failed: missing server URL.");
} else if(!this.userId) {
node.log("Matrix connection failed: missing user ID.");
node.error("Matrix connection failed: missing server URL in configuration.");
} else {
node.setConnected = function(connected, cb) {
node.setConnected = async function(connected, cb) {
if (node.connected !== connected) {
node.connected = connected;
if(typeof cb === 'function') {
@@ -53,6 +62,47 @@ module.exports = function(RED) {
if (connected) {
node.log("Matrix server connection ready.");
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);
}
);
}
try {
await accessSecretStorage(function(){});
} catch(e) {
node.error("secret storage bootstrap failure: " + e);
console.log("secret storage bootstrap failure: ", e);
}
initialSetup = true;
}
} else {
node.emit("disconnected");
}
@@ -64,18 +114,72 @@ module.exports = function(RED) {
};
node.setConnected(false);
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId);
fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist
upgradeDirectoryIfNecessary(node, storageDir);
const localStorage = new LocalStorage(localStorageDir);
node.matrixClient = sdk.createClient({
baseUrl: this.url,
accessToken: this.credentials.accessToken,
sessionStore: new sdk.WebStorageSessionStore(localStorage),
cryptoStore: new LocalStorageCryptoStore(localStorage),
userId: this.userId,
deviceId: this.deviceId || undefined,
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined,
verificationMethods: ["m.sas.v1"],
// cryptoCallbacks: {
// getSecretStorageKey: async function(
// { keys: keyInfos },
// ssssItemName,
// ){
// const cli = node.matrixClient;
// let keyId = await cli.getDefaultSecretStorageKeyId();
// // console.log("DEFAULT SECRET STORAGE KEY ID: " + keyId, keyInfos);
// //
// // let decodeBase64 = function(base64) {
// // return Buffer.from(base64, "base64");
// // }
// // return await this.crypto.getSecretStorageKey(keyId);
// let keyInfo;
// if (keyId) {
// // use the default SSSS key if set
// keyInfo = keyInfos[keyId];
// if (!keyInfo) {
// // if the default key is not available, pretend the default key
// // isn't set
// keyId = undefined;
// }
// }
// if (!keyId) {
// // if no default SSSS key is set, fall back to a heuristic of using the
// // only available key, if only one key is set
// const keyInfoEntries = Object.entries(keyInfos);
// if (keyInfoEntries.length > 1) {
// throw new Error("Multiple storage key requests not implemented");
// }
// [keyId, keyInfo] = keyInfoEntries[0];
// }
//
// // Check the in-memory cache
// // if (isCachingAllowed() && secretStorageKeys[keyId]) {
// // return [keyId, secretStorageKeys[keyId]];
// // }
//
// // if (dehydrationCache.key) {
// // if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
// // cacheSecretStorageKey(keyId, keyInfo, dehydrationCache.key);
// // return [keyId, dehydrationCache.key];
// // }
// // }
//
// // const backupInfo = await node.matrixClient.getKeyBackupVersion();
// const backupInfo = await node.matrixClient.getAccountDataFromServer(
// "m.secret_storage.key." + keyId
// );
//
// // if(await cli.checkSecretStorageKey(key, keyInfo)) {
// // }
// return [keyId, await node.matrixClient.keyBackupKeyFromPassword(node.secureStoragePassphrase, backupInfo)] ;
// }
// }
});
// set globally if configured to do so
@@ -83,13 +187,19 @@ module.exports = function(RED) {
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
}
node.on('close', function(done) {
if(node.matrixClient) {
node.matrixClient.close();
function stopClient() {
if(node.matrixClient && node.matrixClient.clientRunning) {
node.matrixClient.stopClient();
node.setConnected(false);
}
if(retryStartTimeout) {
clearTimeout(retryStartTimeout);
}
}
node.on('close', function(done) {
stopClient();
done();
});
@@ -97,8 +207,51 @@ module.exports = function(RED) {
return node.connected;
};
node.matrixClient.on("Room.timeline", async function(event, room, toStartOfTimeline, data) {
node.emit("Room.timeline", event, room, toStartOfTimeline, data);
node.matrixClient.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, 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) {
@@ -212,38 +365,179 @@ module.exports = function(RED) {
// httpStatus: 401
// }
node.error("[Session.logged_out] " + errorObj);
node.error("Authentication failure: " + errorObj);
stopClient();
});
/**
* This helper should be used whenever you need to access secret storage. It
* ensures that secret storage (and also cross-signing since they each depend on
* each other in a cycle of sorts) have been bootstrapped before running the
* provided function.
*
* Bootstrapping secret storage may take one of these paths:
* 1. Create secret storage from a passphrase and store cross-signing keys
* in secret storage.
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
*
* @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional.
* @param {boolean} [forceReset] Reset secret storage even if it's already set up
*/
let accessSecretStorage = async function(func = async () => { }, forceReset = false) {
// only do this if we have a secure storage password
if(!node.secureStoragePassphrase) {
return;
}
const recoveryKey = await node.matrixClient.createRecoveryKeyFromPassphrase(node.secureStoragePassphrase);
const cli = node.matrixClient;
try {
if (!(await cli.hasSecretStorageKey()) || forceReset) {
// For password authentication users after 2020-09, this cross-signing
// step will be a no-op since it is now setup during registration or login
// when needed. We should keep this here to cover other cases such as:
// * Users with existing sessions prior to 2020-09 changes
// * SSO authentication users which require interactive auth to upload
// keys (and also happen to skip all post-authentication flows at the
// moment via token login)
if(!await node.matrixClient.isCrossSigningReady()) {
await node.matrixClient.bootstrapCrossSigning({
// maybe we can skip this?
authUploadDeviceSigningKeys: () => {
return true;
}
});
}
const backupInfo = await node.matrixClient.getKeyBackupVersion();
await node.matrixClient.bootstrapSecretStorage({
createSecretStorageKey: async () => recoveryKey,
keyBackupInfo: backupInfo,
setupNewKeyBackup: !backupInfo,
getKeyBackupPassphrase: () => {
return recoveryKey;
},
});
} else {
await node.matrixClient.bootstrapSecretStorage({
getKeyBackupPassphrase: async () => recoveryKey,
});
}
// `return await` needed here to ensure `finally` block runs after the
// inner operation completes.
return await func();
} catch (e) {
node.error("Secret storage init failure: " + e);
}
};
async function run() {
try {
if(node.e2ee){
if(node.e2ee && node.matrixClient.initCrypto){
node.log("Initializing crypto...");
await node.matrixClient.initCrypto();
node.matrixClient.setGlobalErrorOnUnknownDevices(false);
try {
await node.matrixClient.initCrypto();
node.matrixClient.setGlobalErrorOnUnknownDevices(false);
node.matrixClient.setCryptoTrustCrossSignedDevices(true); // false = manually verify sessions
// await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient);
} catch (e) {
node.error("Failed to initialize crypto: " + e);
console.log(e);
}
}
node.log("Connecting to Matrix server...");
await node.matrixClient.startClient({ initialSyncLimit: 8 });
} catch(error){
await node.matrixClient.startClient({
initialSyncLimit: 8
});
} catch(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, {
credentials: {
userId: { type:"text", required: true },
accessToken: { type:"text", required: true },
deviceId: { type: "text", required: true },
url: { type: "text", required: true },
deviceLabel: { type: "text", required: false },
userId: { type: "text", required: true },
accessToken: { type: "text", required: true },
deviceId: { type: "text", required: false },
secureStoragePassphrase: { 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) {
let oldStorageDir = './matrix-local-storage';
let oldStorageDir = './matrix-local-storage',
oldStorageDir2 = './matrix-client-storage';
// if the old storage location exists lets move it to it's new location
if(fs.pathExistsSync(oldStorageDir)){
@@ -266,5 +560,28 @@ module.exports = function(RED) {
node.log("archiving old config folder '" + oldStorageDir + "' to '" + oldStorageDir + "-backup");
fs.renameSync(oldStorageDir, oldStorageDir + "-backup");
}
if(RED.settings.userDir !== resolve('./')) {
// user directory does not match running directory
// check if we stored stuff in wrong directory and move it
if(fs.pathExistsSync(oldStorageDir2)){
fs.ensureDirSync(storageDir);
node.log("found old '" + oldStorageDir2 + "' path, copying to new location '" + storageDir);
fs.copySync(oldStorageDir2, storageDir);
// rename folder to keep as a backup (and so we don't run again)
fs.renameSync(oldStorageDir2, oldStorageDir2 + "-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);
}
}

View File

@@ -78,7 +78,10 @@
<dt class="optional">msg.payload.user_type
<span class="property-type">string | null</span>
</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>
<h3>Outputs</h3>

View File

@@ -1,5 +1,4 @@
module.exports = function(RED) {
const got = require("got");
const utf8 = require('utf8');
const crypto = require('crypto');
@@ -22,7 +21,8 @@ module.exports = function(RED) {
return;
}
node.on("input", function (msg) {
node.on("input", async function (msg) {
const { got } = await import('got');
if(!msg.payload.username) {
node.error("msg.payload.username is required");
@@ -34,7 +34,7 @@ module.exports = function(RED) {
return;
}
(async () => {
await (async () => {
try {
var response = await got.get(this.server + '/_synapse/admin/v1/register', {
responseType: 'json'
@@ -62,7 +62,13 @@ module.exports = function(RED) {
.update(utf8.encode(msg.payload.password))
.update("\x00")
.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 {
response = await got.post(this.server + '/_synapse/admin/v1/register', {