Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
04de0b4eb3 | |||
8cb52112c1 | |||
54a9972bbc | |||
ad34f018ab | |||
20345787d2 | |||
99c19923c6 | |||
093d59893e | |||
913f5dfcb9 | |||
e0947dd3bc | |||
8287f3c08a | |||
|
2a78524a90 | ||
|
d01838ac84 | ||
|
2059f8455d | ||
0cb8ecf8aa | |||
|
77f2c4be46 | ||
|
cf82daf5da | ||
487241e097 | |||
3b161f1ad9 | |||
45ff930518 | |||
9e3b66f4aa | |||
02826e2769 | |||
6bbd1d5119 | |||
5de1274def | |||
1b54bc03eb | |||
65edc94854 | |||
351679ad77 | |||
|
2b2da4faf7 | ||
5090e4fbb6 | |||
51e649b4cf | |||
a08709265e | |||
57ba70db6c | |||
a3e1381d53 | |||
6dca3aa70e | |||
b36286d994 | |||
f14190d9ea | |||
000c28e3b8 | |||
fd174e32ff | |||
e8506d8887 | |||
d7c4bc26bb | |||
785e0cd7be | |||
2e9633e113 | |||
1859696122 | |||
fd605005d1 | |||
85de450a1a | |||
e7e0f2967b | |||
611e23b845 | |||
9d050a0d44 | |||
c833a40a84 | |||
0e755bc350 | |||
c920dd12cb | |||
|
9661922f78 | ||
|
20c7182511 | ||
|
f48ba74a72 | ||
|
124a0cba34 | ||
|
8ca11f36d8 | ||
8a7fba39e8 | |||
c61eadd05d |
81
README.md
@ -1,71 +1,76 @@
|
|||||||
# node-red-contrib-matrix-chat
|
# node-red-contrib-matrix-chat
|
||||||
[Matrix](https://matrix.org/) chat server client for [Node-RED](https://nodered.org/)
|
[Matrix](https://matrix.org/) chat server client for [Node-RED](https://nodered.org/)
|
||||||
|
|
||||||
***Currently we are in beta. We ask that you open any issues you have on our repository to help us reach a stable well tested version. Things may change & break before our first release so check changelog before updating.***
|
***Currently in beta. Please report any issues in our repository to help us reach a stable, well-tested release. Breaking changes may occur before our first stable release, so be sure to check the changelog before updating.***
|
||||||
|
|
||||||
If you need help with this feel free to join our public matrix room at [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech)
|
Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech)
|
||||||
|
|
||||||
|
[](https://ko-fi.com/B0B51BM7C)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
The following is supported from this package:
|
Supported functionality in this package includes:
|
||||||
|
|
||||||
- End-to-end encryption
|
- **End-to-end encryption (E2EE)**
|
||||||
- [Currently a WIP](#end-to-end-encryption-notes)
|
- [Work in progress](#end-to-end-encryption-notes)
|
||||||
- Receive events from a room (messages, reactions, images, audio, locations, and files) whether encrypted or not
|
- Alternative: Use [Pantalaimon](https://github.com/matrix-org/pantalaimon) for E2EE key synchronization across sessions
|
||||||
- Send Images/Files (sending files to e2ee room doesn't currently encrypt them yet)
|
- **Receive events** from rooms: Messages, reactions, images, audio, locations, files, encrypted or unencrypted
|
||||||
- Edit messages
|
- **Fetch/modify room state**: Update room settings
|
||||||
- Delete events (messages, reactions, etc)
|
- **Paginate room history**
|
||||||
- Decrypt files in e2ee rooms
|
- **Send files** (encryption support for files coming soon)
|
||||||
- Send HTML/Plain Text Message/Notice
|
- **Send/edit messages** (supports plain text and HTML formats)
|
||||||
- React to messages
|
- **Send typing notifications**
|
||||||
- Register user's on closed registration Synapse servers using `registration_shared_secret` (Admin Only)
|
- **Delete events** (messages, reactions, etc.)
|
||||||
- List out users on a Synapse server (Admin Only)
|
- **Decrypt files** in E2EE rooms
|
||||||
- Get WhoIs info for a Synapse user (Admin Only)
|
- **React to messages**
|
||||||
- Add/Edit Synapse users using the v2 API (requires a pre-existing admin account)
|
- **Admin tools**:
|
||||||
- Deactivate users on Synapse servers (Admin Only)
|
- Register users on closed Synapse servers (`registration_shared_secret`)
|
||||||
- Get a user list from a room
|
- Manage users, including listing, adding, editing, deactivating (Synapse API)
|
||||||
- Kick user from room
|
- Force-add users to rooms
|
||||||
- Ban user from room
|
- **Room management**: Invite, kick, ban, join, create, and leave rooms
|
||||||
- Join, Create, Invite, and Leave rooms
|
|
||||||
- Synapse admin API to force add user to room (requires bot to be in same room already)
|
|
||||||
|
|
||||||
|
These features allow you to easily build bots, set up chat relays, or even administrate your Matrix server directly from [Node-RED](https://nodered.org/).
|
||||||
Therefore, you can easily build a bot, chat relay, or administrate your Matrix server from within [Node-RED](https://nodered.org/).
|
|
||||||
|
|
||||||
### Installing
|
### Installing
|
||||||
|
|
||||||
You can either install from within Node-RED by searching for `node-red-contrib-matrix-chat` or run this from within your Node-RED directory:
|
Install through Node-RED's UI by searching for `node-red-contrib-matrix-chat`, or use the following command inside your Node-RED directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install node-red-contrib-matrix-chat
|
npm install node-red-contrib-matrix-chat
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
We have examples! [Check them out](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme)
|
|
||||||
|
|
||||||
#### Extra functionality
|
Explore our [examples](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) to see the module in action.
|
||||||
You are not limited by just the nodes we have created. If you turn on global access when setting up your Matrix Client you can access the client directly from any function node to write your own logic.
|
|
||||||
|
|
||||||
View an example [here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command)
|
#### Extending functionality
|
||||||
|
|
||||||
|
You're not limited to just the nodes we've created. Enable global access in your Matrix Client to directly interact with the client from function nodes and create custom logic.
|
||||||
|
|
||||||
|
[View an example here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command).
|
||||||
|
|
||||||
### End-to-End Encryption Notes
|
### End-to-End Encryption Notes
|
||||||
Currently, this module has no way of getting encryption keys from other devices on the same account. Therefore it is recommended you use the bot exclusively with Node-RED after it's creation. Failure to do so will lead to your bot being unable to receive messages from e2ee rooms it joined from another client. Shared secret registration makes this super easy since it returns a token and device ID.
|
|
||||||
|
|
||||||
This module stores a folder in your Node-RED directory called `matrix-client-storage` and is it vital that you periodically back this up if you are using e2ee. This is where the client stores all the keys necessary to decrypt messages and if lost you will lose access to e2e rooms. If you move your client to another NR install make sure to migrate this folder as well (and do not let both the old and new client run at same time).
|
- This module doesn't handle encryption key synchronization between devices. It’s recommended to use the bot exclusively in Node-RED to prevent issues with E2EE messages.
|
||||||
|
- **Storage:** Keys for E2EE are saved in a folder called `matrix-client-storage` within your Node-RED directory. Back up this folder regularly! If lost, you won’t be able to decrypt messages from E2EE rooms.
|
||||||
|
- To move your bot to a different installation, migrate this folder and ensure the old and new clients don't run simultaneously.
|
||||||
|
|
||||||
Want to contribute? Any help on getting the last pieces of e2ee figured out would be greatly appreciated :)
|
Interested in helping? Contributions to finalize E2EE support are welcome!
|
||||||
|
|
||||||
### Generate user
|
### Registering a User
|
||||||
You will need a user to use this module. Luckily this module comes with a node that allows you to register users to a homeserver using the secret registration endpoint. This is perfect because it returns an `access_token` as well as a `device_id` which is exactly what we need.
|
|
||||||
|
|
||||||
[Click here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) to see how to generate a user using secret registration
|
This module includes a node to register users using the Synapse secret registration endpoint. It returns both an `access_token` and a `device_id`, perfect for setting up the bot.
|
||||||
|
|
||||||
|
[Guide on registering a user via the web browser](https://skylar.tech/matrix-chat-bot-module-for-node-red/)
|
||||||
|
|
||||||
|
[Guide on registering using shared secret registration](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) (for server owners)
|
||||||
|
|
||||||
### Other Packages
|
### Other Packages
|
||||||
|
|
||||||
- [node-red-contrib-gamedig](https://www.npmjs.com/package/node-red-contrib-gamedig) - Query game servers from Node-RED!
|
- [node-red-contrib-gamedig](https://www.npmjs.com/package/node-red-contrib-gamedig) - Query game servers from Node-RED.
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
All contributions are welcome! If you do add a feature please do a pull request so that everyone benefits :)
|
|
||||||
|
|
||||||
Sharing is caring!
|
We welcome all contributions! Please submit a pull request if you add a feature so the whole community can benefit.
|
||||||
|
|
||||||
|
**Sharing is caring!**
|
||||||
|
@ -1,257 +1,466 @@
|
|||||||
# Examples
|
# Examples
|
||||||
These are examples of what is possible with the [node-red-contrib-matrix-chat](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat) module for [Node-RED](https://nodered.org/)
|
|
||||||
|
|
||||||
If you want to try any of them out just copy their JSON contents from their .json file and use the hamburger menu in Node-RED to import the flow.
|
These examples showcase what is possible with the [node-red-contrib-matrix-chat](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat) module for [Node-RED](https://nodered.org/).
|
||||||
|
|
||||||
Build something cool with these nodes? Feel free to submit a pull request to share it!
|
Build something cool with these nodes? Feel free to submit a pull request to share it!
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Matrix account
|
||||||
|
- Node-RED set up and running
|
||||||
|
- Installed `node-red-contrib-matrix-chat` module
|
||||||
|
|
||||||
|
## How to Use the Examples
|
||||||
|
|
||||||
|
To try out any of the examples:
|
||||||
|
|
||||||
|
1. Copy the JSON contents from the `.json` file linked in each example.
|
||||||
|
2. In Node-RED, use the menu to import the flow:
|
||||||
|
- Click the hamburger menu (top right corner).
|
||||||
|
- Select **Import**.
|
||||||
|
- Paste the JSON and click **Import**.
|
||||||
|
|
||||||
## Index
|
## Index
|
||||||
|
|
||||||
- [Create User with Shared Secret Registration](#create-user-with-shared-secret-registration)
|
**Click the ▶ example to ▼ expand**
|
||||||
- [Create/Edit Synapse User](#createedit-synapse-user)
|
|
||||||
- [Use function node to run any command](#use-function-node-to-run-any-command)
|
|
||||||
- [Respond to "ping" with "pong"](#respond-to-ping-with-pong)
|
|
||||||
- [Respond to "html" with an HTML message](#respond-to-html-with-an-html-message)
|
|
||||||
- [Respond to "image" with an uploaded image](#respond-to-image-with-an-uploaded-image)
|
|
||||||
- [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)
|
|
||||||
- [Accept room invites from specific user](#accept-room-invites-from-specific-user)
|
|
||||||
- [Leave room when someone says "bye"](#leave-room-when-someone-says-bye)
|
|
||||||
- [Remove messages containing "delete"](#remove-messages-containing-delete)
|
|
||||||
- [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)
|
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
|
||||||
### Create user with Shared Secret Registration
|
<details>
|
||||||
|
<summary>Get or set current user display name</summary>
|
||||||
|
|
||||||
|
[View JSON](get-set-displayname.json)
|
||||||
|
|
||||||
|
This flow lets you get or set the displayname for the current user.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Set user avatar using URL</summary>
|
||||||
|
|
||||||
|
[View JSON](set-avatar-from-url.json)
|
||||||
|
|
||||||
|
Inject a URL to an image and Node-RED will fetch the contents, upload to matrix, then set the user avatar to the new mxc_url.
|
||||||
|
|
||||||
|
This is a good example of how to use the upload file node.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Fetch user info by userId</summary>
|
||||||
|
|
||||||
|
[View JSON](get-user.json)
|
||||||
|
|
||||||
|
Note this only works for users that the bot shares a room with. It will attempt to fetch the user from local storage first and if not found will query the server for the data.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Create User with Shared Secret Registration</summary>
|
||||||
|
|
||||||
[View JSON](shared-secret-registration.json)
|
[View JSON](shared-secret-registration.json)
|
||||||
|
|
||||||
Use this flow to create users on servers with closed registration. You also use this endpoint to create your first admin user as it is the same as running the local python script on the server. This requires your registration secret from your homeserver.yaml Synapse server configuration file.
|
Use this flow to create users on servers with closed registration. You can also use this endpoint to create your first admin user, as it is the same as running the local Python script on the server. This requires your registration secret from your `homeserver.yaml` Synapse server configuration file.
|
||||||
|
|
||||||
Edit the object on the inject node to the user/pass combo you want to create and hit the inject button (to the left of the inject node).
|
**Instructions:**
|
||||||
|
|
||||||
|
1. Edit the object on the inject node to specify the desired username and password.
|
||||||
|
2. Click the inject button (to the left of the inject node) to create the user.
|
||||||
|
|
||||||
**Note:** This only works on Synapse servers.
|
**Note:** This only works on Synapse servers.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Create/Edit Synapse User
|
<details>
|
||||||
|
<summary>Create/Edit Synapse User</summary>
|
||||||
|
|
||||||
[View JSON](add-user-with-admin-user.json)
|
[View JSON](add-user-with-admin-user.json)
|
||||||
|
|
||||||
Allows an administrator to create or modify a user account with a specified `msg.userId`.
|
Allows an administrator to create or modify a user account with a specified `msg.userId`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Use function node to run any command
|
<details>
|
||||||
|
<summary>Deactivate User</summary>
|
||||||
[View JSON](custom-redact-function-node.json)
|
|
||||||
|
|
||||||
If we do not have a node for something you want to do you can do this manually with a function node. We now have a node for removing events but this is still a good example.
|
|
||||||
|
|
||||||
**Note:** You should make sure to catch any errors in your function node otherwise you could cause Node-RED to crash.
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Respond to "ping" with "pong"
|
|
||||||
|
|
||||||
[View JSON](respond-ping-pong.json)
|
|
||||||
|
|
||||||
Use this flow to respond to anyone that says "ping" with "pong" into the same room.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Respond to "html" with an HTML message
|
|
||||||
|
|
||||||
[View JSON](respond-to-html-with-html.json)
|
|
||||||
|
|
||||||
Use this flow to respond to anyone that says "html" with an example HTML message. This shows how easy it is to send HTML.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Respond to "image" with an uploaded image
|
|
||||||
|
|
||||||
[View JSON](respond-image-with-image.json)
|
|
||||||
|
|
||||||
You will need an image on the machine running Node-RED. In this case example.png exists inside the Node-RED directory.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Respond to "file" with an uploaded file
|
|
||||||
|
|
||||||
[View JSON](respond-file-with-file.json)
|
|
||||||
|
|
||||||
You will need a file on the machine running Node-RED. In this case sample.pdf exists inside the Node-RED directory.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Respond to "react" with a reaction
|
|
||||||
|
|
||||||
[View JSON](respond-react-with-reaction.json)
|
|
||||||
|
|
||||||
Give a 👍 reaction when someone says "react"
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Accept room invites from specific user
|
|
||||||
|
|
||||||
[View JSON](accept-room-invites.json)
|
|
||||||
|
|
||||||
Accept room invites from specific user.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Leave room when someone says bye
|
|
||||||
|
|
||||||
[View JSON](leave-room-bye.json)
|
|
||||||
|
|
||||||
Leave room when someone says "bye".
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Remove messages containing "delete"
|
|
||||||
|
|
||||||
[View JSON](delete-event.json)
|
|
||||||
|
|
||||||
Any messages containing "delete" will try to be removed by the client.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Respond to "users" with full list of server users
|
|
||||||
|
|
||||||
[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. If your server has a lot of users this paginates and sends a message with 25 users per message.
|
|
||||||
|
|
||||||
This requires admin privileges.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### 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 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 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 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 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### Deactivate user
|
|
||||||
|
|
||||||
[View JSON](deactivate-user.json)
|
[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.
|
If you send "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.
|
**Note:**
|
||||||
|
|
||||||
WARNING: Accounts that are deleted cannot be restored. If you want to temp-disable edit the user instead.
|
- This requires the bot to be a server admin.
|
||||||
|
- **WARNING:** Accounts that are deleted cannot be restored. If you want to temporarily disable a user, consider modifying the user instead.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Force user to join room
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Force User to Join Room</summary>
|
||||||
|
|
||||||
[View JSON](force-join-room.json)
|
[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`
|
If you send "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.
|
**Note:**
|
||||||
|
|
||||||

|
- This requires the bot to be a server admin.
|
||||||
|
- This only works for rooms on the same server.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Message Handling
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Upload file and send to room</summary>
|
||||||
|
|
||||||
|
[View JSON](send-image-to-room.json)
|
||||||
|
|
||||||
|
This flow will download an image from a given URL and upload it to the matrix server then send it to a room.
|
||||||
|
|
||||||
|
This isn't just for images and supports any sort of file format. Videos, images, and audio files will have metadata detected automatically and appended to the message (duration, dimensions, thumbnail, etc)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "ping" with "pong"</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-ping-pong.json)
|
||||||
|
|
||||||
|
Use this flow to respond to anyone who says "ping" with "pong" in the same room.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "html" with an HTML Message</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-to-html-with-html.json)
|
||||||
|
|
||||||
|
Use this flow to respond to anyone who says "html" with an example HTML message. This shows how easy it is to send HTML content.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "react" with a Reaction</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-react-with-reaction.json)
|
||||||
|
|
||||||
|
Gives a 👍 reaction when someone says "react".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Remove Messages Containing "delete"</summary>
|
||||||
|
|
||||||
|
[View JSON](delete-event.json)
|
||||||
|
|
||||||
|
Any messages containing "delete" will be removed by the client.
|
||||||
|
|
||||||
|
**Note:** The bot needs appropriate permissions to remove messages.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Event Handling
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Sending Typing Events to a Room</summary>
|
||||||
|
|
||||||
|
[View JSON](send-typing-events.json)
|
||||||
|
|
||||||
|
You can indicate to a room that the bot is typing and also cancel the typing event. This can be useful for making bots feel more interactive (e.g., show typing while requesting an API endpoint).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Mark all received events as read</summary>
|
||||||
|
|
||||||
|
[View JSON](mark-all-read.json)
|
||||||
|
|
||||||
|
With this flow anytime an event is received by the bot it will mark it as read.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Fetch event by eventId and roomId</summary>
|
||||||
|
|
||||||
|
[View JSON](get-event.json)
|
||||||
|
|
||||||
|
Fetch an event from Matrix by eventId and roomId
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
- Change the inject node to contain a proper eventId and roomId (topic)
|
||||||
|
- Inject the payload and you should see the result contain the event data
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Paginate the entire history of a given room</summary>
|
||||||
|
|
||||||
|
[View JSON](paginate-room-history.json)
|
||||||
|
|
||||||
|
This flow iterates the entire history of a room (outputting for every page we hit).
|
||||||
|
|
||||||
|
There is a configurable delay (currently set at 1000ms) in this flow. This is recommended, so you are not bogging down the server.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Paginate related events to a given eventId</summary>
|
||||||
|
|
||||||
|
[View JSON](fetch-event-relations.json)
|
||||||
|
|
||||||
|
Paginate through the related events to a given eventId. Related events being reactions, thread messages, message modifications, message removals, etc. This outputs once per iterated page.
|
||||||
|
|
||||||
|
If you would rather have it output one massive list at the end of pagination use this flow:
|
||||||
|
[View Aggregated Flow JSON](fetch-event-relations-aggregated.json)
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
- Change the inject node to contain a proper eventId and roomId (topic)
|
||||||
|
- Inject the payload and you should see the result contain a list of related events for the given eventId
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Room Management
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Set room name and topic</summary>
|
||||||
|
|
||||||
|
[View JSON](set-room-name-and-topic.json)
|
||||||
|
|
||||||
|
Changes the specified room's name and topic to the injected values.
|
||||||
|
|
||||||
|
There are a bunch of different settings you can change, this is just an example for these two fields to show how it's done.
|
||||||
|
|
||||||
|
This node can also be used to read these values.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Accept Room Invites from Specific User</summary>
|
||||||
|
|
||||||
|
[View JSON](accept-room-invites.json)
|
||||||
|
|
||||||
|
Automatically accept room invites from a specific user.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Leave Room When Someone Says "bye"</summary>
|
||||||
|
|
||||||
|
[View JSON](leave-room-bye.json)
|
||||||
|
|
||||||
|
Leaves the room when someone says "bye".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "newroom" by Creating a New Room and Inviting User</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-newroom-invite.json)
|
||||||
|
|
||||||
|
When someone sends "newroom", a new room will be created, and the user who sent the message will be invited. The bot will also send a welcome message into the new room.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "joinroom <room_id_or_alias>" by Joining Mentioned Room</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-joinroom.json)
|
||||||
|
|
||||||
|
When someone sends "joinroom <room_id_or_alias>", the bot will join the mentioned room.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Kick/Ban User from Room</summary>
|
||||||
|
|
||||||
|
[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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### User Information
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "users" with Full List of Server Users</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-users-list.json)
|
||||||
|
|
||||||
|
When someone sends the text "users", they receive an HTML message containing all the current users on the server. If your server has many users, this paginates and sends messages with 25 users per message.
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- This requires admin privileges.
|
||||||
|
- If there are many users, the bot may send multiple messages due to message size limits.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "whois <user_id>" with Information about the User's Session</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-whois.json)
|
||||||
|
|
||||||
|
Lists out the user's session info, including IP address, last seen time, and user agent. Useful for finding out more about a specific user on your server.
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- This requires admin privileges.
|
||||||
|
- If the user has many sessions, the message may be too large to send in one piece.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "rooms <user_id>" with User's Rooms</summary>
|
||||||
|
|
||||||
|
[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.
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- This requires admin privileges.
|
||||||
|
- If there are many rooms, the message may be too large to send in one piece.
|
||||||
|
- This only works for users on the current server.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "room_users" with Current Room's Users</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-room-users.json)
|
||||||
|
|
||||||
|
Lists the users participating in the current room.
|
||||||
|
|
||||||
|
**Note:** If there are many users in the room, the message may be too large to send.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Use Function Node to Run Any Command</summary>
|
||||||
|
|
||||||
|
[View JSON](custom-redact-function-node.json)
|
||||||
|
|
||||||
|
If there isn't a node for something you want to do, you can use a function node to manually execute commands. For example, you can redact events (remove messages).
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
- Use the function node to write custom commands using the `matrix-js-sdk` client.
|
||||||
|
- Make sure to catch any errors in your function node to prevent Node-RED from crashing.
|
||||||
|
|
||||||
|
To view the available functions, check out the [`client.ts` file from `matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk/blob/master/src/client.ts).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Download & Store All Received Files/Images</summary>
|
||||||
|
|
||||||
|
[View JSON](store-received-files.json)
|
||||||
|
|
||||||
|
Downloads received files/images. If the file is encrypted, it will decrypt it for you. The decrypt node downloads the file; otherwise, you need to use an HTTP Request node to download the file.
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
- You may need to edit the storage directory for this to work.
|
||||||
|
- By default, files are saved in a `downloads` folder in the Node-RED directory.
|
||||||
|
- Ensure that Node-RED has permission to write to the specified directory.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "image" with an uploaded image</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-image-with-image.json)
|
||||||
|
|
||||||
|
You will need an image on the machine running Node-RED. In this example, `example.png` exists inside the Node-RED directory.
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
1. Place the image file (`example.png`) in the appropriate directory.
|
||||||
|
2. Import the flow and deploy it.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Respond to "file" with an uploaded file</summary>
|
||||||
|
|
||||||
|
[View JSON](respond-file-with-file.json)
|
||||||
|
|
||||||
|
You will need a file on the machine running Node-RED. In this example, `sample.pdf` exists inside the Node-RED directory.
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
1. Place the file (`sample.pdf`) in the appropriate directory.
|
||||||
|
2. Import the flow and deploy it.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
141
examples/fetch-event-relations-aggregated.json
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "10a897739618e1f3",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Aggregates paginated matrix event relations and outputs the full set after reaching the last page",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"83d9261d8fef6c29",
|
||||||
|
"c2e00e38bbeea60a",
|
||||||
|
"8c1df4f49b913bf8",
|
||||||
|
"4be02d632d13cebf",
|
||||||
|
"09fc3b3f18df27af"
|
||||||
|
],
|
||||||
|
"x": 774,
|
||||||
|
"y": 939,
|
||||||
|
"w": 772,
|
||||||
|
"h": 142
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "83d9261d8fef6c29",
|
||||||
|
"type": "matrix-fetch-relations",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "10a897739618e1f3",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomType": "msg",
|
||||||
|
"roomValue": "topic",
|
||||||
|
"eventIdType": "msg",
|
||||||
|
"eventIdValue": "eventId",
|
||||||
|
"relationTypeType": "json",
|
||||||
|
"relationTypeValue": "null",
|
||||||
|
"eventTypeType": "json",
|
||||||
|
"eventTypeValue": "null",
|
||||||
|
"directionType": "str",
|
||||||
|
"directionValue": "b",
|
||||||
|
"limitType": "json",
|
||||||
|
"limitValue": "null",
|
||||||
|
"recurseType": "bool",
|
||||||
|
"recurseValue": "false",
|
||||||
|
"fromType": "msg",
|
||||||
|
"fromValue": "payload.next_batch",
|
||||||
|
"toType": "json",
|
||||||
|
"toValue": "null",
|
||||||
|
"x": 1180,
|
||||||
|
"y": 1040,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"c2e00e38bbeea60a"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"4be02d632d13cebf"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c2e00e38bbeea60a",
|
||||||
|
"type": "function",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "10a897739618e1f3",
|
||||||
|
"name": "Loop - output on finish",
|
||||||
|
"func": "// you will want to use a unique flow_key \n// if you duplicate this node multiple times\nlet flow_key = \"relations_\" + msg.topic;\nlet relations = flow.get(flow_key) || [];\n\n// add our chunk to the flow variable\nrelations.push(...msg.payload.chunk);\n\nif(msg.payload.next_batch) {\n // loop around for more records\n flow.set(flow_key, relations);\n return [null, msg];\n}\n\n// if msg.payload.next_batch is unset we have reached the end\nflow.set(flow_key, undefined);\nmsg.payload = relations;\nreturn [msg, null];",
|
||||||
|
"outputs": 2,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 1180,
|
||||||
|
"y": 980,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"8c1df4f49b913bf8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"83d9261d8fef6c29"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8c1df4f49b913bf8",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "10a897739618e1f3",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1420,
|
||||||
|
"y": 980,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4be02d632d13cebf",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "10a897739618e1f3",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1410,
|
||||||
|
"y": 1040,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "09fc3b3f18df27af",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "10a897739618e1f3",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "eventId",
|
||||||
|
"v": "$example",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!example:skylar.tech",
|
||||||
|
"x": 920,
|
||||||
|
"y": 1040,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"83d9261d8fef6c29"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/fetch-event-relations.png
Normal file
After Width: | Height: | Size: 26 KiB |
103
examples/get-event.json
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "13e21fb561dd6367",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Get event by eventId and roomId",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"79aa7974c32c41e7",
|
||||||
|
"f895181928491647",
|
||||||
|
"687ab5804c6ab05c",
|
||||||
|
"c731a6923324de48"
|
||||||
|
],
|
||||||
|
"x": 814,
|
||||||
|
"y": 759,
|
||||||
|
"w": 692,
|
||||||
|
"h": 122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "79aa7974c32c41e7",
|
||||||
|
"type": "matrix-get-event",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "13e21fb561dd6367",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomIdType": "msg",
|
||||||
|
"roomIdValue": "topic",
|
||||||
|
"eventIdType": "msg",
|
||||||
|
"eventIdValue": "eventId",
|
||||||
|
"x": 1200,
|
||||||
|
"y": 820,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"f895181928491647"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"687ab5804c6ab05c"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f895181928491647",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "13e21fb561dd6367",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1380,
|
||||||
|
"y": 800,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "687ab5804c6ab05c",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "13e21fb561dd6367",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1370,
|
||||||
|
"y": 840,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c731a6923324de48",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "13e21fb561dd6367",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "eventId",
|
||||||
|
"v": "$example",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!example:skylar.tech",
|
||||||
|
"x": 960,
|
||||||
|
"y": 820,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"79aa7974c32c41e7"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/get-event.png
Normal file
After Width: | Height: | Size: 17 KiB |
201
examples/get-set-displayname.json
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "844cdfc6e3fc3207",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Get current display name",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"9807698e516450ec",
|
||||||
|
"3f700b2d3458a1e8",
|
||||||
|
"78ff7e5088a08ff6",
|
||||||
|
"0ae57f85687ba6b3"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 2119,
|
||||||
|
"w": 612,
|
||||||
|
"h": 122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9807698e516450ec",
|
||||||
|
"type": "matrix-user-settings",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "844cdfc6e3fc3207",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomId": null,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"t": "get",
|
||||||
|
"p": "display_name",
|
||||||
|
"to": "payload",
|
||||||
|
"tot": "msg",
|
||||||
|
"ls": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x": 1020,
|
||||||
|
"y": 2180,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"78ff7e5088a08ff6"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0ae57f85687ba6b3"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3f700b2d3458a1e8",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "844cdfc6e3fc3207",
|
||||||
|
"name": "",
|
||||||
|
"props": [],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "",
|
||||||
|
"x": 850,
|
||||||
|
"y": 2180,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"9807698e516450ec"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "78ff7e5088a08ff6",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "844cdfc6e3fc3207",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1240,
|
||||||
|
"y": 2160,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0ae57f85687ba6b3",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "844cdfc6e3fc3207",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1230,
|
||||||
|
"y": 2200,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "289fac90afc8bfa6",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Set current display name",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"8f166980f4bfe6a4",
|
||||||
|
"c31399d30d9ea44f",
|
||||||
|
"c7984555f4ad668e",
|
||||||
|
"634935af5451baf9"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 2259,
|
||||||
|
"w": 612,
|
||||||
|
"h": 122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8f166980f4bfe6a4",
|
||||||
|
"type": "matrix-user-settings",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "289fac90afc8bfa6",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomId": null,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"t": "set",
|
||||||
|
"p": "display_name",
|
||||||
|
"to": "payload",
|
||||||
|
"tot": "msg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x": 1020,
|
||||||
|
"y": 2320,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"c7984555f4ad668e"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"634935af5451baf9"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c31399d30d9ea44f",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "289fac90afc8bfa6",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "payload"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "",
|
||||||
|
"payload": "New Name",
|
||||||
|
"payloadType": "str",
|
||||||
|
"x": 860,
|
||||||
|
"y": 2320,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"8f166980f4bfe6a4"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c7984555f4ad668e",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "289fac90afc8bfa6",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1240,
|
||||||
|
"y": 2300,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "634935af5451baf9",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "289fac90afc8bfa6",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1230,
|
||||||
|
"y": 2340,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/get-set-displayname.png
Normal file
After Width: | Height: | Size: 32 KiB |
99
examples/get-user.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "fdbc1eb4cc492b04",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Fetch user info by userId",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"8869afc68deeede0",
|
||||||
|
"e29636a733134aef",
|
||||||
|
"15c5caf17e83263c",
|
||||||
|
"52a65daa26891471"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 1939,
|
||||||
|
"w": 552,
|
||||||
|
"h": 122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8869afc68deeede0",
|
||||||
|
"type": "matrix-get-user",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "fdbc1eb4cc492b04",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"userType": "msg",
|
||||||
|
"userValue": "userId",
|
||||||
|
"propertyType": "msg",
|
||||||
|
"propertyValue": "user",
|
||||||
|
"x": 1000,
|
||||||
|
"y": 2000,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"15c5caf17e83263c"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"52a65daa26891471"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e29636a733134aef",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "fdbc1eb4cc492b04",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "userId",
|
||||||
|
"v": "@skylord123:skylar.tech",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "",
|
||||||
|
"x": 850,
|
||||||
|
"y": 2000,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"8869afc68deeede0"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "15c5caf17e83263c",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "fdbc1eb4cc492b04",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1180,
|
||||||
|
"y": 1980,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "52a65daa26891471",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "fdbc1eb4cc492b04",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1170,
|
||||||
|
"y": 2020,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/get-user.png
Normal file
After Width: | Height: | Size: 14 KiB |
99
examples/mark-all-read.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "a8bd7a89b91a93c4",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Mark all received events as read",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"f9e49ebbc4bda431",
|
||||||
|
"c58fce26efb45b27",
|
||||||
|
"c8feb91421fcbbb3",
|
||||||
|
"e6d52159a6cfe0c0"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 1579,
|
||||||
|
"w": 632,
|
||||||
|
"h": 122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f9e49ebbc4bda431",
|
||||||
|
"type": "matrix-mark-read",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "a8bd7a89b91a93c4",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomType": "msg",
|
||||||
|
"roomValue": "topic",
|
||||||
|
"eventIdType": "msg",
|
||||||
|
"eventIdValue": "eventId",
|
||||||
|
"x": 1070,
|
||||||
|
"y": 1640,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"c8feb91421fcbbb3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"e6d52159a6cfe0c0"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c58fce26efb45b27",
|
||||||
|
"type": "matrix-receive",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "a8bd7a89b91a93c4",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomId": "",
|
||||||
|
"acceptOwnEvents": false,
|
||||||
|
"acceptText": true,
|
||||||
|
"acceptEmotes": true,
|
||||||
|
"acceptStickers": true,
|
||||||
|
"acceptReactions": true,
|
||||||
|
"acceptFiles": true,
|
||||||
|
"acceptAudio": true,
|
||||||
|
"acceptImages": true,
|
||||||
|
"acceptVideos": true,
|
||||||
|
"acceptLocations": true,
|
||||||
|
"x": 860,
|
||||||
|
"y": 1640,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"f9e49ebbc4bda431"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c8feb91421fcbbb3",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "a8bd7a89b91a93c4",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1260,
|
||||||
|
"y": 1620,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e6d52159a6cfe0c0",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "a8bd7a89b91a93c4",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1250,
|
||||||
|
"y": 1660,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/mark-all-read.png
Normal file
After Width: | Height: | Size: 17 KiB |
173
examples/paginate-room-history.json
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1317b79375a65579",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Paginate the entire history of a given room",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"e8fc071b3f81fdee",
|
||||||
|
"33937ba1e74ed92d",
|
||||||
|
"4aadbf1c37d2ac8b",
|
||||||
|
"e4025b7df45f3d91",
|
||||||
|
"973dd418b00172c8",
|
||||||
|
"3edbea9403d7c347"
|
||||||
|
],
|
||||||
|
"x": 854,
|
||||||
|
"y": 1339,
|
||||||
|
"w": 832,
|
||||||
|
"h": 182
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e8fc071b3f81fdee",
|
||||||
|
"type": "matrix-paginate-room",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "1317b79375a65579",
|
||||||
|
"name": "Paginate Room",
|
||||||
|
"server": null,
|
||||||
|
"roomType": "msg",
|
||||||
|
"roomValue": "topic",
|
||||||
|
"paginateKeyType": "msg",
|
||||||
|
"paginateKeyValue": "paginateKey",
|
||||||
|
"paginateBackwardsType": "msg",
|
||||||
|
"paginateBackwardsValue": "paginateBackwards",
|
||||||
|
"pageSizeType": "msg",
|
||||||
|
"pageSizeValue": "pageSize",
|
||||||
|
"x": 1280,
|
||||||
|
"y": 1480,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"973dd418b00172c8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"4aadbf1c37d2ac8b"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "33937ba1e74ed92d",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "1317b79375a65579",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1560,
|
||||||
|
"y": 1380,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4aadbf1c37d2ac8b",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "1317b79375a65579",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1530,
|
||||||
|
"y": 1480,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e4025b7df45f3d91",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "1317b79375a65579",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "paginateKey",
|
||||||
|
"v": "",
|
||||||
|
"vt": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "paginateBackwards",
|
||||||
|
"v": "true",
|
||||||
|
"vt": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "pageSize",
|
||||||
|
"v": "25",
|
||||||
|
"vt": "num"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "timeout",
|
||||||
|
"v": "1000",
|
||||||
|
"vt": "num"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!example:skylar.tech",
|
||||||
|
"x": 1000,
|
||||||
|
"y": 1480,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"e8fc071b3f81fdee"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "973dd418b00172c8",
|
||||||
|
"type": "function",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "1317b79375a65579",
|
||||||
|
"name": "Loop - return each chunk",
|
||||||
|
"func": "node.status({\n fill: \"green\",\n text: `${msg.start} - ${msg.end} ${msg.payload ? '' : 'DONE'}`\n});\n\nif(msg.payload.length) {\n // we have more records so output to both\n return [msg, msg];\n}\n\n// no more records so only send to output 1\nreturn [msg, null];",
|
||||||
|
"outputs": 2,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 1290,
|
||||||
|
"y": 1400,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"33937ba1e74ed92d"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"3edbea9403d7c347"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3edbea9403d7c347",
|
||||||
|
"type": "delay",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "1317b79375a65579",
|
||||||
|
"name": "msg.delay",
|
||||||
|
"pauseType": "delayv",
|
||||||
|
"timeout": "0",
|
||||||
|
"timeoutUnits": "seconds",
|
||||||
|
"rate": "1",
|
||||||
|
"nbRateUnits": "1",
|
||||||
|
"rateUnits": "second",
|
||||||
|
"randomFirst": "1",
|
||||||
|
"randomLast": "5",
|
||||||
|
"randomUnits": "seconds",
|
||||||
|
"drop": false,
|
||||||
|
"allowrate": false,
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 1550,
|
||||||
|
"y": 1420,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"e8fc071b3f81fdee"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/paginate-room-history.png
Normal file
After Width: | Height: | Size: 31 KiB |
142
examples/send-image-to-room.json
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "f4a0c2a9d7eed027",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Send an uploaded file to a room",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"8d475ab136d1ee7e",
|
||||||
|
"2524f5a9a7ea2444",
|
||||||
|
"9a149a36d6ab6470",
|
||||||
|
"9da1ed1dc33930bb",
|
||||||
|
"f93782c346d0e6ef"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 2719,
|
||||||
|
"w": 992,
|
||||||
|
"h": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8d475ab136d1ee7e",
|
||||||
|
"type": "matrix-upload-file",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "f4a0c2a9d7eed027",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"inputType": "msg",
|
||||||
|
"inputValue": "payload",
|
||||||
|
"fileNameType": "msg",
|
||||||
|
"fileNameValue": "filename",
|
||||||
|
"contentType": "",
|
||||||
|
"generateThumbnails": true,
|
||||||
|
"x": 1270,
|
||||||
|
"y": 2760,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"2524f5a9a7ea2444"
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2524f5a9a7ea2444",
|
||||||
|
"type": "matrix-send-message",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "f4a0c2a9d7eed027",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomId": "",
|
||||||
|
"message": "",
|
||||||
|
"messageType": "m.text",
|
||||||
|
"messageFormat": "",
|
||||||
|
"replaceMessage": false,
|
||||||
|
"threadReplyType": "msg",
|
||||||
|
"threadReplyValue": "isThread",
|
||||||
|
"x": 1440,
|
||||||
|
"y": 2760,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"f93782c346d0e6ef"
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9a149a36d6ab6470",
|
||||||
|
"type": "http request",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "f4a0c2a9d7eed027",
|
||||||
|
"name": "",
|
||||||
|
"method": "GET",
|
||||||
|
"ret": "bin",
|
||||||
|
"paytoqs": "ignore",
|
||||||
|
"url": "",
|
||||||
|
"tls": "",
|
||||||
|
"persist": false,
|
||||||
|
"proxy": "",
|
||||||
|
"insecureHTTPParser": false,
|
||||||
|
"authType": "",
|
||||||
|
"senderr": false,
|
||||||
|
"headers": [],
|
||||||
|
"x": 1110,
|
||||||
|
"y": 2760,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"8d475ab136d1ee7e"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9da1ed1dc33930bb",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "f4a0c2a9d7eed027",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "url",
|
||||||
|
"v": "https://nodered.org/about/resources/media/node-red-icon.png",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "filename",
|
||||||
|
"v": "avatar.jpg",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!example:skylar.tech",
|
||||||
|
"x": 900,
|
||||||
|
"y": 2760,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"9a149a36d6ab6470"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f93782c346d0e6ef",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "f4a0c2a9d7eed027",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1620,
|
||||||
|
"y": 2760,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/send-image-to-room.png
Normal file
After Width: | Height: | Size: 22 KiB |
127
examples/send-typing-events.json
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "541dbfc3f04220cf",
|
||||||
|
"type": "matrix-typing",
|
||||||
|
"z": "f025a8b9fbd1b054",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomType": "msg",
|
||||||
|
"roomValue": "topic",
|
||||||
|
"typingType": "msg",
|
||||||
|
"typingValue": "typing",
|
||||||
|
"timeoutMsType": "num",
|
||||||
|
"timeoutMsValue": "20000",
|
||||||
|
"x": 1090,
|
||||||
|
"y": 220,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"febf8236f3683963"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"9db9819ebb6343c8"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d7c3714c657bfe4f",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "f025a8b9fbd1b054",
|
||||||
|
"name": "Start Typing",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "typing",
|
||||||
|
"v": "true",
|
||||||
|
"vt": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!mohVKgDFYUubJQHcuX:skylar.tech",
|
||||||
|
"x": 910,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"541dbfc3f04220cf"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "783121ff1a6bd833",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "f025a8b9fbd1b054",
|
||||||
|
"name": "Stop Typing",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "typing",
|
||||||
|
"v": "false",
|
||||||
|
"vt": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!mohVKgDFYUubJQHcuX:skylar.tech",
|
||||||
|
"x": 910,
|
||||||
|
"y": 240,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"541dbfc3f04220cf"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9db9819ebb6343c8",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "f025a8b9fbd1b054",
|
||||||
|
"name": "Failure msg",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"statusVal": "",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 1270,
|
||||||
|
"y": 240,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "febf8236f3683963",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "f025a8b9fbd1b054",
|
||||||
|
"name": "Success msg",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"statusVal": "",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 1270,
|
||||||
|
"y": 200,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01e8c4c6303af390",
|
||||||
|
"type": "comment",
|
||||||
|
"z": "f025a8b9fbd1b054",
|
||||||
|
"name": "Send typing events to a room",
|
||||||
|
"info": "",
|
||||||
|
"x": 940,
|
||||||
|
"y": 160,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/send-typing-events.png
Normal file
After Width: | Height: | Size: 18 KiB |
122
examples/set-avatar-from-url.json
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "4cac7bbe81dd3a54",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Set new avatar image from remote image URL",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"91f08bf02decd653",
|
||||||
|
"44a586ebac1fb619",
|
||||||
|
"d3bd4a0d4e1520a3",
|
||||||
|
"e9e591def93615c5"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 2419,
|
||||||
|
"w": 712,
|
||||||
|
"h": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "91f08bf02decd653",
|
||||||
|
"type": "http request",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "4cac7bbe81dd3a54",
|
||||||
|
"name": "",
|
||||||
|
"method": "GET",
|
||||||
|
"ret": "bin",
|
||||||
|
"paytoqs": "ignore",
|
||||||
|
"url": "",
|
||||||
|
"tls": "",
|
||||||
|
"persist": false,
|
||||||
|
"proxy": "",
|
||||||
|
"insecureHTTPParser": false,
|
||||||
|
"authType": "",
|
||||||
|
"senderr": false,
|
||||||
|
"headers": [],
|
||||||
|
"x": 1010,
|
||||||
|
"y": 2460,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"e9e591def93615c5"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "44a586ebac1fb619",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "4cac7bbe81dd3a54",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "url",
|
||||||
|
"v": "https://nodered.org/about/resources/media/node-red-icon.png",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "filename",
|
||||||
|
"v": "avatar.jpg",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "",
|
||||||
|
"x": 850,
|
||||||
|
"y": 2460,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"91f08bf02decd653"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d3bd4a0d4e1520a3",
|
||||||
|
"type": "matrix-user-settings",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "4cac7bbe81dd3a54",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomId": null,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"t": "set",
|
||||||
|
"p": "avatar_url",
|
||||||
|
"to": "payload.url",
|
||||||
|
"tot": "msg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x": 1360,
|
||||||
|
"y": 2460,
|
||||||
|
"wires": [
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e9e591def93615c5",
|
||||||
|
"type": "matrix-upload-file",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "4cac7bbe81dd3a54",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"inputType": "msg",
|
||||||
|
"inputValue": "payload",
|
||||||
|
"fileNameType": "msg",
|
||||||
|
"fileNameValue": "filename",
|
||||||
|
"contentType": "",
|
||||||
|
"generateThumbnails": true,
|
||||||
|
"x": 1190,
|
||||||
|
"y": 2460,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"d3bd4a0d4e1520a3"
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/set-avatar-from-url.png
Normal file
After Width: | Height: | Size: 14 KiB |
120
examples/set-room-name-and-topic.json
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "2e2bb3f8521150c0",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Set room name and topic",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"610648ad6bd73072",
|
||||||
|
"aca9be4e86e111f3",
|
||||||
|
"7f5e16c4f6c7885f",
|
||||||
|
"915ce202570af51a"
|
||||||
|
],
|
||||||
|
"x": 674,
|
||||||
|
"y": 2539,
|
||||||
|
"w": 732,
|
||||||
|
"h": 122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "610648ad6bd73072",
|
||||||
|
"type": "matrix-room-state-events",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "2e2bb3f8521150c0",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomType": "msg",
|
||||||
|
"roomValue": "topic",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"t": "set",
|
||||||
|
"p": "m.room.name",
|
||||||
|
"to": "roomName",
|
||||||
|
"tot": "msg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": "set",
|
||||||
|
"p": "m.room.topic",
|
||||||
|
"to": "description",
|
||||||
|
"tot": "msg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x": 1050,
|
||||||
|
"y": 2600,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"7f5e16c4f6c7885f"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"915ce202570af51a"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aca9be4e86e111f3",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "2e2bb3f8521150c0",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "roomName",
|
||||||
|
"v": "Test Room",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "description",
|
||||||
|
"v": "This is a test room for my Node-RED bot",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!example:skylar.tech",
|
||||||
|
"x": 820,
|
||||||
|
"y": 2600,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"610648ad6bd73072"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7f5e16c4f6c7885f",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "2e2bb3f8521150c0",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1280,
|
||||||
|
"y": 2580,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "915ce202570af51a",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "2e2bb3f8521150c0",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1270,
|
||||||
|
"y": 2620,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
BIN
examples/set-room-name-and-topic.png
Normal file
After Width: | Height: | Size: 18 KiB |
141
examples/view-event-relations.json
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "ab09dd64e9c48bba",
|
||||||
|
"type": "group",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"name": "Pages through matrix event relations, delivering results one page at a time",
|
||||||
|
"style": {
|
||||||
|
"label": true
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
"ed98bce4958c4203",
|
||||||
|
"501758e233cc42d9",
|
||||||
|
"edb9af1a22e2e17f",
|
||||||
|
"4734e9ee26fe0812",
|
||||||
|
"fc27f2058f9c934b"
|
||||||
|
],
|
||||||
|
"x": 754,
|
||||||
|
"y": 1139,
|
||||||
|
"w": 892,
|
||||||
|
"h": 142
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ed98bce4958c4203",
|
||||||
|
"type": "matrix-fetch-relations",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "ab09dd64e9c48bba",
|
||||||
|
"name": "",
|
||||||
|
"server": null,
|
||||||
|
"roomType": "msg",
|
||||||
|
"roomValue": "topic",
|
||||||
|
"eventIdType": "msg",
|
||||||
|
"eventIdValue": "eventId",
|
||||||
|
"relationTypeType": "json",
|
||||||
|
"relationTypeValue": "null",
|
||||||
|
"eventTypeType": "json",
|
||||||
|
"eventTypeValue": "null",
|
||||||
|
"directionType": "str",
|
||||||
|
"directionValue": "b",
|
||||||
|
"limitType": "json",
|
||||||
|
"limitValue": "null",
|
||||||
|
"recurseType": "bool",
|
||||||
|
"recurseValue": "false",
|
||||||
|
"fromType": "msg",
|
||||||
|
"fromValue": "payload.next_batch",
|
||||||
|
"toType": "json",
|
||||||
|
"toValue": "null",
|
||||||
|
"x": 1280,
|
||||||
|
"y": 1240,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"edb9af1a22e2e17f"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fc27f2058f9c934b"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "501758e233cc42d9",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "ab09dd64e9c48bba",
|
||||||
|
"name": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"p": "topic",
|
||||||
|
"vt": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "eventId",
|
||||||
|
"v": "$example",
|
||||||
|
"vt": "str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": 0.1,
|
||||||
|
"topic": "!example:skylar.tech",
|
||||||
|
"x": 900,
|
||||||
|
"y": 1240,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"ed98bce4958c4203"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "edb9af1a22e2e17f",
|
||||||
|
"type": "function",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "ab09dd64e9c48bba",
|
||||||
|
"name": "Loop - return each chunk",
|
||||||
|
"func": "if(msg.payload.next_batch) {\n // we have more records so output to both\n return [msg, msg];\n}\n\n// no more records so only send to output 1\nreturn [msg, null];",
|
||||||
|
"outputs": 2,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 1290,
|
||||||
|
"y": 1180,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"4734e9ee26fe0812"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ed98bce4958c4203"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4734e9ee26fe0812",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "ab09dd64e9c48bba",
|
||||||
|
"name": "Debug Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1520,
|
||||||
|
"y": 1180,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc27f2058f9c934b",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "8fd89a0b44c61e76",
|
||||||
|
"g": "ab09dd64e9c48bba",
|
||||||
|
"name": "Error Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 1240,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
17370
package-lock.json
generated
23
package.json
@ -1,17 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "node-red-contrib-matrix-chat",
|
"name": "node-red-contrib-matrix-chat",
|
||||||
"version": "0.7.0",
|
"version": "0.9.1",
|
||||||
"description": "Matrix chat server client for Node-RED",
|
"description": "Matrix chat server client for Node-RED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"got": "^12.0.2",
|
"got": "^12.0.2",
|
||||||
|
"image-size": "^1.0.2",
|
||||||
"isomorphic-webcrypto": "^2.3.8",
|
"isomorphic-webcrypto": "^2.3.8",
|
||||||
"matrix-js-sdk": "^22.0.0",
|
"matrix-js-sdk": "34.11.1",
|
||||||
|
"mime": "^3.0.0",
|
||||||
"node-fetch": "^3.3.0",
|
"node-fetch": "^3.3.0",
|
||||||
"node-localstorage": "^2.2.1",
|
"node-localstorage": "^2.2.1",
|
||||||
"olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/472/download",
|
"olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download",
|
||||||
"request": "^2.88.2",
|
"sharp": "^0.33.4",
|
||||||
|
"tmp": "^0.2.1",
|
||||||
"utf8": "^3.0.0"
|
"utf8": "^3.0.0"
|
||||||
},
|
},
|
||||||
"node-red": {
|
"node-red": {
|
||||||
@ -20,10 +24,15 @@
|
|||||||
"matrix-server-config": "src/matrix-server-config.js",
|
"matrix-server-config": "src/matrix-server-config.js",
|
||||||
"matrix-receive": "src/matrix-receive.js",
|
"matrix-receive": "src/matrix-receive.js",
|
||||||
"matrix-send-message": "src/matrix-send-message.js",
|
"matrix-send-message": "src/matrix-send-message.js",
|
||||||
|
"matrix-typing": "src/matrix-typing.js",
|
||||||
|
"matrix-mark-read": "src/matrix-mark-read.js",
|
||||||
"matrix-delete-event": "src/matrix-delete-event.js",
|
"matrix-delete-event": "src/matrix-delete-event.js",
|
||||||
"matrix-send-file": "src/matrix-send-file.js",
|
"matrix-send-file": "src/matrix-send-file.js",
|
||||||
"matrix-send-image": "src/matrix-send-image.js",
|
"matrix-send-image": "src/matrix-send-image.js",
|
||||||
|
"matrix-upload-file": "src/matrix-upload-file.js",
|
||||||
"matrix-react": "src/matrix-react.js",
|
"matrix-react": "src/matrix-react.js",
|
||||||
|
"matrix-user-settings": "src/matrix-user-settings.js",
|
||||||
|
"matrix-get-user": "src/matrix-get-user.js",
|
||||||
"matrix-create-room": "src/matrix-create-room.js",
|
"matrix-create-room": "src/matrix-create-room.js",
|
||||||
"matrix-invite-room": "src/matrix-invite-room.js",
|
"matrix-invite-room": "src/matrix-invite-room.js",
|
||||||
"matrix-room-invite": "src/matrix-room-invite.js",
|
"matrix-room-invite": "src/matrix-room-invite.js",
|
||||||
@ -32,13 +41,17 @@
|
|||||||
"matrix-crypt-file": "src/matrix-crypt-file.js",
|
"matrix-crypt-file": "src/matrix-crypt-file.js",
|
||||||
"matrix-room-kick": "src/matrix-room-kick.js",
|
"matrix-room-kick": "src/matrix-room-kick.js",
|
||||||
"matrix-room-ban": "src/matrix-room-ban.js",
|
"matrix-room-ban": "src/matrix-room-ban.js",
|
||||||
|
"matrix-room-users": "src/matrix-room-users.js",
|
||||||
|
"matrix-room-state-events": "src/matrix-room-state-events.js",
|
||||||
"matrix-synapse-users": "src/matrix-synapse-users.js",
|
"matrix-synapse-users": "src/matrix-synapse-users.js",
|
||||||
"matrix-synapse-register": "src/matrix-synapse-register.js",
|
"matrix-synapse-register": "src/matrix-synapse-register.js",
|
||||||
"matrix-synapse-create-edit-user": "src/matrix-synapse-create-edit-user.js",
|
"matrix-synapse-create-edit-user": "src/matrix-synapse-create-edit-user.js",
|
||||||
"matrix-synapse-deactivate-user": "src/matrix-synapse-deactivate-user.js",
|
"matrix-synapse-deactivate-user": "src/matrix-synapse-deactivate-user.js",
|
||||||
"matrix-synapse-join-room": "src/matrix-synapse-join-room.js",
|
"matrix-synapse-join-room": "src/matrix-synapse-join-room.js",
|
||||||
"matrix-whois-user": "src/matrix-whois-user.js",
|
"matrix-whois-user": "src/matrix-whois-user.js",
|
||||||
"matrix-room-users": "src/matrix-room-users.js"
|
"matrix-paginate-room": "src/matrix-paginate-room.js",
|
||||||
|
"matrix-get-event": "src/matrix-get-event.js",
|
||||||
|
"matrix-event-relations": "src/matrix-event-relations.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.0 KiB |
@ -8,7 +8,7 @@
|
|||||||
outputs: 2,
|
outputs: 2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" }
|
server: { type: "matrix-server-config" }
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "Create Room";
|
return this.name || "Create Room";
|
||||||
|
@ -8,9 +8,10 @@ module.exports = function(RED) {
|
|||||||
this.server = RED.nodes.getNode(n.server);
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
this.encodeUri = function(pathTemplate, variables) {
|
this.encodeUri = function(pathTemplate, variables) {
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
@ -41,8 +42,9 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.payload) {
|
if(!msg.payload) {
|
||||||
@ -64,6 +66,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-create-room", MatrixCreateRoom);
|
RED.nodes.registerType("matrix-create-room", MatrixCreateRoom);
|
||||||
}
|
}
|
@ -12,22 +12,22 @@ module.exports = function(RED) {
|
|||||||
const { got } = await import('got');
|
const { got } = await import('got');
|
||||||
|
|
||||||
if(!msg.type) {
|
if(!msg.type) {
|
||||||
node.error('msg.type is required.');
|
node.error('msg.type is required.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.content) {
|
if(!msg.content) {
|
||||||
node.error('msg.content is required.');
|
node.error('msg.content is required.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.content.file) {
|
if(!msg.content.file) {
|
||||||
node.error('msg.content.file is required.');
|
node.error('msg.content.file is required.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.url) {
|
if(!msg.url) {
|
||||||
node.error('msg.url is required.');
|
node.error('msg.url is required.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
reason: { value: "" },
|
reason: { value: "" },
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ module.exports = function(RED) {
|
|||||||
node.on('input', function(msg) {
|
node.on('input', function(msg) {
|
||||||
|
|
||||||
if(!msg.eventId) {
|
if(!msg.eventId) {
|
||||||
node.error("eventId is missing");
|
node.error("eventId is missing", msg);
|
||||||
node.send([null, msg])
|
node.send([null, msg])
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -38,7 +39,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -70,6 +71,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent);
|
RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent);
|
||||||
}
|
}
|
||||||
|
230
src/matrix-event-relations.html
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('matrix-fetch-relations', {
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["Related Events", "Error"],
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomType: { value: "msg" },
|
||||||
|
roomValue: { value: "topic" },
|
||||||
|
eventIdType: { value: "msg" },
|
||||||
|
eventIdValue: { value: "eventId" },
|
||||||
|
relationTypeType: { value: "json" },
|
||||||
|
relationTypeValue: { value: "null" },
|
||||||
|
eventTypeType: { value: "json" },
|
||||||
|
eventTypeValue: { value: "null" },
|
||||||
|
directionType: { value: "str" },
|
||||||
|
directionValue: { value: "b" },
|
||||||
|
limitType: { value: "json" },
|
||||||
|
limitValue: { value: "null" },
|
||||||
|
recurseType: { value: "bool" },
|
||||||
|
recurseValue: { value: "false" },
|
||||||
|
fromType: { value: "msg" },
|
||||||
|
fromValue: { value: "payload.next_batch" },
|
||||||
|
toType: { value: "json" },
|
||||||
|
toValue: { value: "null" },
|
||||||
|
},
|
||||||
|
label: function () {
|
||||||
|
return this.name || "Fetch Event Relations";
|
||||||
|
},
|
||||||
|
paletteLabel: 'Fetch Event Relations',
|
||||||
|
oneditprepare: function () {
|
||||||
|
$("#node-input-room").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types: ['msg', 'flow', 'global', 'str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.roomValue)
|
||||||
|
.typedInput('type', this.roomType);
|
||||||
|
|
||||||
|
$("#node-input-eventId").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.eventIdValue)
|
||||||
|
.typedInput('type', this.eventIdType);
|
||||||
|
|
||||||
|
$("#node-input-relationType").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'str', 'json'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.relationTypeValue)
|
||||||
|
.typedInput('type', this.relationTypeType);
|
||||||
|
|
||||||
|
$("#node-input-eventType").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'str', 'json'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.eventTypeValue)
|
||||||
|
.typedInput('type', this.eventTypeType);
|
||||||
|
|
||||||
|
$("#node-input-direction").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.directionValue)
|
||||||
|
.typedInput('type', this.directionType);
|
||||||
|
|
||||||
|
$("#node-input-limit").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'num', 'json'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.limitValue)
|
||||||
|
.typedInput('type', this.limitType);
|
||||||
|
|
||||||
|
$("#node-input-recurse").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'bool', 'json'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.recurseValue)
|
||||||
|
.typedInput('type', this.recurseType);
|
||||||
|
|
||||||
|
$("#node-input-from").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'str', 'json'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.fromValue)
|
||||||
|
.typedInput('type', this.fromType);
|
||||||
|
|
||||||
|
$("#node-input-to").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'str', 'json'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.toValue)
|
||||||
|
.typedInput('type', this.toType);
|
||||||
|
},
|
||||||
|
oneditsave: function () {
|
||||||
|
this.roomType = $("#node-input-room").typedInput('type');
|
||||||
|
this.roomValue = $("#node-input-room").typedInput('value');
|
||||||
|
this.eventIdType = $("#node-input-eventId").typedInput('type');
|
||||||
|
this.eventIdValue = $("#node-input-eventId").typedInput('value');
|
||||||
|
this.relationTypeType = $("#node-input-relationType").typedInput('type');
|
||||||
|
this.relationTypeValue = $("#node-input-relationType").typedInput('value');
|
||||||
|
this.eventTypeType = $("#node-input-eventType").typedInput('type');
|
||||||
|
this.eventTypeValue = $("#node-input-eventType").typedInput('value');
|
||||||
|
this.directionType = $("#node-input-direction").typedInput('type');
|
||||||
|
this.directionValue = $("#node-input-direction").typedInput('value');
|
||||||
|
this.limitType = $("#node-input-limit").typedInput('type');
|
||||||
|
this.limitValue = $("#node-input-limit").typedInput('value');
|
||||||
|
this.recurseType = $("#node-input-recurse").typedInput('type');
|
||||||
|
this.recurseValue = $("#node-input-recurse").typedInput('value');
|
||||||
|
this.fromType = $("#node-input-from").typedInput('type');
|
||||||
|
this.fromValue = $("#node-input-from").typedInput('value');
|
||||||
|
this.toType = $("#node-input-to").typedInput('type');
|
||||||
|
this.toValue = $("#node-input-to").typedInput('value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-fetch-relations">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-server"></i> Matrix Server</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||||
|
<input type="text" id="node-input-room">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-eventId"><i class="fa fa-key"></i> Event ID</label>
|
||||||
|
<input type="text" id="node-input-eventId">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">The event ID for which relations are being fetched.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-relationType"><i class="fa fa-link"></i> Relation Type</label>
|
||||||
|
<input type="text" id="node-input-relationType">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">The type of relations (e.g., m.in_reply_to, m.replace, m.annotation, m.thread, m.reference) to be fetched for the event. You can read more in the <a href="https://spec.matrix.org/v1.11/client-server-api/#relationship-types" target="_blank">Matrix specification</a>.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-eventType"><i class="fa fa-file"></i> Event Type</label>
|
||||||
|
<input type="text" id="node-input-eventType">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">The type of event (e.g., m.reaction, m.annotation) to be retrieved.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-direction"><i class="fa fa-arrow-left"></i> Direction</label>
|
||||||
|
<input type="text" id="node-input-direction">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">Set the direction of pagination. Can be "b" for backwards or "f" for forwards. Defaults to "b".</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-limit"><i class="fa fa-arrow-left"></i> Limit</label>
|
||||||
|
<input type="text" id="node-input-limit">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">The maximum number of results to return in a single chunk. Recommended to leave as null to use the server default.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-recurse"><i class="fa fa-arrow-left"></i> Recurse</label>
|
||||||
|
<input type="text" id="node-input-recurse">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">
|
||||||
|
Whether to additionally include events which only relate indirectly to the given event, i.e. events related to the given event via two or more direct relationships.
|
||||||
|
<br><br>
|
||||||
|
If set to false, only events which have a direct relation with the given event will be included.
|
||||||
|
<br><br>
|
||||||
|
If set to true, events which have an indirect relation with the given event will be included additionally up to a certain depth level. Homeservers SHOULD traverse at least 3 levels of relationships. Implementations MAY perform more but MUST be careful to not infinitely recurse.
|
||||||
|
<br><br>
|
||||||
|
The default value is false.
|
||||||
|
<br><br>
|
||||||
|
Added in v1.10
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-from"><i class="fa fa-arrow-left"></i> From</label>
|
||||||
|
<input type="text" id="node-input-from">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">
|
||||||
|
The pagination token to start returning results from. If not supplied, results start at the most recent topological event known to the server.
|
||||||
|
<br><br>
|
||||||
|
Can be a next_batch or prev_batch token from a previous call, or a returned start token from /messages, or a next_batch token from /sync.
|
||||||
|
<br><br>
|
||||||
|
Defaults to msg.payload.next_batch to easily fetch the next batch in a recursive loop.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-to"><i class="fa fa-arrow-left"></i> To</label>
|
||||||
|
<input type="text" id="node-input-to">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">
|
||||||
|
The pagination token to stop returning results at. If not supplied, results continue up to limit or until there are no more events.
|
||||||
|
<br><br>
|
||||||
|
Like from, this can be a previous token from a prior call to this endpoint or from /messages or /sync.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-fetch-relations">
|
||||||
|
<p>
|
||||||
|
The Matrix Fetch Relations node allows you to retrieve event relations for a specific event within a Matrix room.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>msg</strong> (<em>default</em>): Triggers the fetching of relations based on the provided parameters.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Output 1 (Related Events)</strong>: Returns an array of events with their relations.
|
||||||
|
Each event contains details such as:
|
||||||
|
<ul>
|
||||||
|
<li><code>msg.payload.chunk</code> - Array of related events.</li>
|
||||||
|
<li><code>msg.payload.prev_batch</code> - The previous batch token for pagination.</li>
|
||||||
|
<li><code>msg.payload.next_batch</code> - The next batch token for pagination.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Output 2 (Error)</strong>: If an error occurs during the fetch process, the error message is sent to this output.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Dynamic Properties</h3>
|
||||||
|
<p>Properties such as <strong>Room ID</strong>, <strong>Event ID</strong>, <strong>Relation Type</strong>, and <strong>Event Type</strong> can be dynamically set using message, flow, or global context variables.</p>
|
||||||
|
|
||||||
|
<h3>Usage</h3>
|
||||||
|
<p>To fetch event relations, trigger this node with a <code>msg</code> input. The node will fetch the relations for the given event ID and return the related events.</p>
|
||||||
|
</script>
|
111
src/matrix-event-relations.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
const sdkPromise = import("matrix-js-sdk");
|
||||||
|
|
||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixFetchRelations(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
let node = this;
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomType = n.roomType;
|
||||||
|
this.roomValue = n.roomValue;
|
||||||
|
this.eventIdType = n.eventIdType;
|
||||||
|
this.eventIdValue = n.eventIdValue;
|
||||||
|
this.relationTypeType = n.relationTypeType;
|
||||||
|
this.relationTypeValue = n.relationTypeValue;
|
||||||
|
this.eventTypeType = n.eventTypeType;
|
||||||
|
this.eventTypeValue = n.eventTypeValue;
|
||||||
|
this.directionType = n.directionType;
|
||||||
|
this.directionValue = n.directionValue;
|
||||||
|
this.limitType = n.limitType;
|
||||||
|
this.limitValue = n.limitValue;
|
||||||
|
this.recurseType = n.recurseType;
|
||||||
|
this.recurseValue = n.recurseValue;
|
||||||
|
this.fromType = n.fromType;
|
||||||
|
this.fromValue = n.fromValue;
|
||||||
|
this.toType = n.toType;
|
||||||
|
this.toValue = n.toValue;
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.error("No configuration node", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.server.on("disconnected", function() {
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("input", async function(msg) {
|
||||||
|
if (!node.server || !node.server.matrixClient) {
|
||||||
|
node.error("No matrix server selected", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sdk = await sdkPromise;
|
||||||
|
const Direction = sdk.Direction;
|
||||||
|
|
||||||
|
function evaluateNodePropertySafe(value, type, node, msg) {
|
||||||
|
try {
|
||||||
|
return RED.util.evaluateNodeProperty(value, type, node, msg);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof TypeError) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let roomId = RED.util.evaluateNodeProperty(node.roomValue, node.roomType, node, msg),
|
||||||
|
eventId = RED.util.evaluateNodeProperty(node.eventIdValue, node.eventIdType, node, msg),
|
||||||
|
relationType = RED.util.evaluateNodeProperty(node.relationTypeValue, node.relationTypeType, node, msg),
|
||||||
|
eventType = RED.util.evaluateNodeProperty(node.eventTypeValue, node.eventTypeType, node, msg),
|
||||||
|
direction = RED.util.evaluateNodeProperty(node.directionValue, node.directionType, node, msg) || Direction.Backward,
|
||||||
|
limit = RED.util.evaluateNodeProperty(node.limitValue, node.limitType, node, msg),
|
||||||
|
recurse = RED.util.evaluateNodeProperty(node.recurseValue, node.recurseType, node, msg),
|
||||||
|
from = evaluateNodePropertySafe(node.fromValue, node.fromType, node, msg),
|
||||||
|
to = evaluateNodePropertySafe(node.toValue, node.toType, node, msg);
|
||||||
|
|
||||||
|
let opts = { dir: direction };
|
||||||
|
if (limit) {
|
||||||
|
opts.limit = limit;
|
||||||
|
}
|
||||||
|
if (recurse === true || recurse === false) {
|
||||||
|
opts.recurse = recurse;
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
opts.from = from;
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
opts.to = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.payload = await node.server.matrixClient.fetchRelations(
|
||||||
|
roomId,
|
||||||
|
eventId,
|
||||||
|
relationType || null,
|
||||||
|
eventType || null,
|
||||||
|
opts
|
||||||
|
);
|
||||||
|
node.send([msg, null]);
|
||||||
|
} catch (e) {
|
||||||
|
msg.error = `Event relations pagination error: ${e.stack}`;
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType("matrix-fetch-relations", MatrixFetchRelations);
|
||||||
|
};
|
0
src/matrix-file-crypt.js
Normal file
95
src/matrix-get-event.html
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('matrix-get-event',{
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomIdType: { value: "msg" },
|
||||||
|
roomIdValue: { value: "topic" },
|
||||||
|
eventIdType: { value: "msg" },
|
||||||
|
eventIdValue: { value: "eventId" }
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Get Event";
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
$("#node-input-roomId").typedInput({
|
||||||
|
types: ['msg','flow','global','str'],
|
||||||
|
typeField: "#node-input-roomId"
|
||||||
|
});
|
||||||
|
$("#node-input-roomId").typedInput("type", this.roomIdType || "msg");
|
||||||
|
$("#node-input-roomId").typedInput("value", this.roomIdValue || "topic");
|
||||||
|
|
||||||
|
$("#node-input-eventId").typedInput({
|
||||||
|
types: ['msg','flow','global','str'],
|
||||||
|
typeField: "#node-input-eventId"
|
||||||
|
});
|
||||||
|
$("#node-input-eventId").typedInput("type", this.eventIdType || "msg");
|
||||||
|
$("#node-input-eventId").typedInput("value", this.eventIdValue || "eventId");
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.roomIdType = $("#node-input-roomId").typedInput('type');
|
||||||
|
this.roomIdValue = $("#node-input-roomId").typedInput('value');
|
||||||
|
this.eventIdType = $("#node-input-eventId").typedInput('type');
|
||||||
|
this.eventIdValue = $("#node-input-eventId").typedInput('value');
|
||||||
|
},
|
||||||
|
paletteLabel: 'Get Event'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-get-event">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-roomId"><i class="fa fa-file"></i> Room ID</label>
|
||||||
|
<input type="text" id="node-input-roomId">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-eventId"><i class="fa fa-file"></i> Event ID</label>
|
||||||
|
<input type="text" id="node-input-eventId">
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-get-event">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>Get an event from a Matrix room using the Room ID and Event ID. Returns the event object as the payload.</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.payload.roomId <span class="property-type">string</span></dt>
|
||||||
|
<dd>The Room ID from which the event will be fetched.</dd>
|
||||||
|
|
||||||
|
<dt>msg.payload.eventId <span class="property-type">string</span></dt>
|
||||||
|
<dd>The Event ID of the message to fetch.</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>The fetched event object.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
<li>Error
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.payload <span class="property-type">string</span></dt>
|
||||||
|
<dd>Error details.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</script>
|
87
src/matrix-get-event.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixGetEvent(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
let node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomIdType = n.roomIdType;
|
||||||
|
this.roomIdValue = n.roomIdValue;
|
||||||
|
this.eventIdType = n.eventIdType;
|
||||||
|
this.eventIdValue = n.eventIdValue;
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.error("No configuration node", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on('input', async function(msg) {
|
||||||
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
|
node.error("No matrix server selected", msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.server.isConnected()) {
|
||||||
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if ((type === 'flow') || (type === 'global')) {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if(type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if(type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let roomId = getToValue(msg, node.roomIdType, node.roomIdValue),
|
||||||
|
eventId = getToValue(msg, node.eventIdType, node.eventIdValue);
|
||||||
|
|
||||||
|
if(!roomId) {
|
||||||
|
node.error('Missing roomId', msg);
|
||||||
|
return;
|
||||||
|
} else if(!eventId) {
|
||||||
|
node.error('Missing eventId', msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.payload = await node.server.matrixClient.fetchRoomEvent(roomId, eventId);
|
||||||
|
node.send([msg, null]);
|
||||||
|
} catch(e) {
|
||||||
|
node.error("Failed to get event " + msg.topic + ": " + e, msg);
|
||||||
|
msg.payload = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-get-event", MatrixGetEvent);
|
||||||
|
}
|
129
src/matrix-get-user.html
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<script type="text/html" data-template-name="matrix-get-user">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-user"><i class="fa fa-user"></i> User ID</label>
|
||||||
|
<input type="text" id="node-input-user">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-property"><i class="fa fa-user"></i> Output Property</label>
|
||||||
|
<input type="text" id="node-input-property">
|
||||||
|
</div>
|
||||||
|
<div class="form-row form-tips">
|
||||||
|
This is the property the user data object will be set to
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-get-user">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>
|
||||||
|
Get data for a user. Data includes display name, avatar URL, presence, last active, currently active, and latest user events. You must share a room with the user. This tried to fetch the user from local storage and if it does not exist will then ask the server.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt class="optional">msg.userId | dynamic
|
||||||
|
<span class="property-type">string</span>
|
||||||
|
</dt>
|
||||||
|
<dd> The user to get details for.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ol class="node-ports">
|
||||||
|
<li>Success
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg <span class="property-type">object</span></dt>
|
||||||
|
<dd>Original message object with modifications based on config.</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.error <span class="property-type">undefined|object</span></dt>
|
||||||
|
<dd>Returned if there was an error getting the user</dd>
|
||||||
|
</dl>
|
||||||
|
<dt class="optional">dynamic
|
||||||
|
<span class="property-type">string|object</span>
|
||||||
|
</dt>
|
||||||
|
<dd> You configure what to return on the node.</dd>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(){
|
||||||
|
function isInvalidProperty(v,vt) {
|
||||||
|
if (/msg|flow|global/.test(vt)) {
|
||||||
|
if (!RED.utils.validatePropertyExpression(v)) {
|
||||||
|
return "Invalid property: " + v;
|
||||||
|
}
|
||||||
|
} else if (vt === "jsonata") {
|
||||||
|
try{ jsonata(v); } catch(e) {
|
||||||
|
return "Invalid expression: " + e.message;
|
||||||
|
}
|
||||||
|
} else if (vt === "json") {
|
||||||
|
try{ JSON.parse(v); } catch(e) {
|
||||||
|
return "Invalid JSON data: " + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType('matrix-get-user',{
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
inputs:1,
|
||||||
|
outputs:2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
userType: { value: "msg" },
|
||||||
|
userValue: { value: "userId" },
|
||||||
|
propertyType: { value: "msg" },
|
||||||
|
propertyValue: { value: "user" },
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
$("#node-input-user").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.userValue)
|
||||||
|
.typedInput('type', this.userType);
|
||||||
|
|
||||||
|
$("#node-input-property").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.propertyValue)
|
||||||
|
.typedInput('type', this.propertyType);
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.userType = $("#node-input-user").typedInput('type');
|
||||||
|
this.userValue = $("#node-input-user").typedInput('value');
|
||||||
|
this.propertyType = $("#node-input-property").typedInput('type');
|
||||||
|
this.propertyValue = $("#node-input-property").typedInput('value');
|
||||||
|
},
|
||||||
|
oneditresize: function(size) {
|
||||||
|
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
|
||||||
|
var height = size.height;
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
height -= $(rows[i]).outerHeight(true);
|
||||||
|
}
|
||||||
|
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
|
||||||
|
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
||||||
|
height += 16;
|
||||||
|
$("#node-input-rule-container").editableList('height',height);
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Get User";
|
||||||
|
},
|
||||||
|
paletteLabel: 'Get User'
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
145
src/matrix-get-user.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixGetUser(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.userType = n.userType || "msg";
|
||||||
|
this.userValue = n.userValue || "userId";
|
||||||
|
this.propertyType = n.propertyType || "msg";
|
||||||
|
this.propertyValue = n.propertyValue || "user";
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.warn("No configuration node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("input", async function (msg) {
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if ((type === 'flow') || (type === 'global')) {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if(type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if(type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToValue(value, type, property) {
|
||||||
|
if(type === 'global' || type === 'flow') {
|
||||||
|
var contextKey = RED.util.parseContextStore(property);
|
||||||
|
if (/\[msg/.test(contextKey.key)) {
|
||||||
|
// The key has a nest msg. reference to evaluate first
|
||||||
|
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true)
|
||||||
|
}
|
||||||
|
var target = node.context()[type];
|
||||||
|
var callback = err => {
|
||||||
|
if (err) {
|
||||||
|
node.error(err, msg);
|
||||||
|
getterErrors[rule.p] = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.set(contextKey.key, value, contextKey.store, callback);
|
||||||
|
} else if(type === 'msg') {
|
||||||
|
if (!RED.util.setMessageProperty(msg, property, value)) {
|
||||||
|
node.warn(RED._("change.errors.no-override",{property:property}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
|
msg.error = "No matrix server selected";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!node.server.isConnected()) {
|
||||||
|
msg.error = "Matrix server connection is currently closed";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userId = getToValue(msg, node.userType, node.userValue);
|
||||||
|
if(!userId) {
|
||||||
|
msg.error = "Missing userId";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = null;
|
||||||
|
try {
|
||||||
|
user = node.server.matrixClient.getUser(userId);
|
||||||
|
} catch(e) {
|
||||||
|
msg.error = "Failed getting user: " + e.message;
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!user) {
|
||||||
|
// failed to fetch from local storage, try to fetch data from server
|
||||||
|
let user2 = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let profileInfo = node.server.matrixClient.getProfileInfo(userId);
|
||||||
|
if(Object.keys(profileInfo).length > 0) {
|
||||||
|
user2.displayName = profileInfo.displayname;
|
||||||
|
user2.avatarUrl = profileInfo.avatar_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
let presence = node.server.matrixClient.getPresence(userId);
|
||||||
|
if(Object.keys(presence).length > 0) {
|
||||||
|
user2.currentlyActive = presence.currently_active;
|
||||||
|
user2.lastActiveAgo = presence.last_active_ago;
|
||||||
|
user2.presenceStatusMsg = presence.presence_status_msg;
|
||||||
|
user2.presence = presence.presence;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Object.keys(user2).length > 0) {
|
||||||
|
setToValue(user2, node.propertyType, node.propertyValue);
|
||||||
|
node.send([msg, null]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
msg.error = "Failed getting user: " + e.message;
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setToValue(user, node.propertyType, node.propertyValue);
|
||||||
|
node.send([msg, null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-get-user", MatrixGetUser);
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs: 2,
|
outputs: 2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
|
@ -9,9 +9,10 @@ module.exports = function(RED) {
|
|||||||
this.roomId = n.roomId;
|
this.roomId = n.roomId;
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
this.encodeUri = function(pathTemplate, variables) {
|
this.encodeUri = function(pathTemplate, variables) {
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
@ -37,18 +38,19 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on("input", function (msg) {
|
node.on("input", function (msg) {
|
||||||
if (! node.server || ! node.server.matrixClient) {
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
node.error("No matrix server selected");
|
node.error("No matrix server selected", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error("room must be defined in either msg.topic or in node config");
|
node.error("room must be defined in either msg.topic or in node config", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +66,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-invite-room", MatrixInviteRoom);
|
RED.nodes.registerType("matrix-invite-room", MatrixInviteRoom);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" }
|
server: { type: "matrix-server-config" }
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "Join Room";
|
return this.name || "Join Room";
|
||||||
|
@ -11,6 +11,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -24,17 +25,18 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on("input", function (msg) {
|
node.on("input", function (msg) {
|
||||||
if (! node.server || ! node.server.matrixClient) {
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
node.error("No matrix server selected");
|
node.error("No matrix server selected", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error("Room must be specified in msg.topic");
|
node.error("Room must be specified in msg.topic", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,11 +48,15 @@ module.exports = function(RED) {
|
|||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
node.error("Error trying to join room " + msg.topic + ":" + e);
|
node.error("Error trying to join room " + msg.topic + ":" + e, msg);
|
||||||
msg.error = e;
|
msg.error = e;
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-join-room", MatrixJoinRoom);
|
RED.nodes.registerType("matrix-join-room", MatrixJoinRoom);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs: 2,
|
outputs: 2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
|
@ -11,9 +11,10 @@ module.exports = function(RED) {
|
|||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
if (!node.server) {
|
if (!node.server) {
|
||||||
node.error("No configuration node");
|
node.error("No configuration node", {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.server.on("disconnected", function(){
|
node.server.on("disconnected", function(){
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
@ -25,30 +26,36 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on('input', function(msg) {
|
node.on('input', function(msg) {
|
||||||
if (! node.server || ! node.server.matrixClient) {
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
node.error("No matrix server selected");
|
node.error("No matrix server selected", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error('No room provided in msg.topic');
|
node.error('No room provided in msg.topic', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
node.log("Leaving room " + msg.topic);
|
node.log("Leaving room " + msg.topic);
|
||||||
node.server.matrixClient.leave(msg.topic);
|
node.server.matrixClient.leave(msg.topic);
|
||||||
|
node.server.matrixClient.store.removeRoom(msg.topic);
|
||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
node.error("Failed to leave room " + msg.topic + ": " + e);
|
node.error("Failed to leave room " + msg.topic + ": " + e, msg);
|
||||||
msg.payload = e;
|
msg.payload = e;
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-leave-room", MatrixLeaveRoom);
|
RED.nodes.registerType("matrix-leave-room", MatrixLeaveRoom);
|
||||||
}
|
}
|
98
src/matrix-mark-read.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('matrix-mark-read',{
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["message"],
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomType: { value: "msg" },
|
||||||
|
roomValue: { value: "topic" },
|
||||||
|
eventIdType: { value: "msg" },
|
||||||
|
eventIdValue: { value: "eventId" }
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Mark Read";
|
||||||
|
},
|
||||||
|
paletteLabel: 'Mark Read',
|
||||||
|
oneditprepare: function() {
|
||||||
|
$("#node-input-room").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.roomValue)
|
||||||
|
.typedInput('type', this.roomType);
|
||||||
|
|
||||||
|
$("#node-input-eventId").typedInput({
|
||||||
|
types:['msg','flow','global','bool'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.eventIdValue)
|
||||||
|
.typedInput('type', this.eventIdType);
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.roomType = $("#node-input-room").typedInput('type');
|
||||||
|
this.roomValue = $("#node-input-room").typedInput('value');
|
||||||
|
this.eventIdType = $("#node-input-eventId").typedInput('type');
|
||||||
|
this.eventIdValue = $("#node-input-eventId").typedInput('value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-mark-read">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-server"></i> Matrix Server</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||||
|
<input type="text" id="node-input-room">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-eventId"><i class="fa fa-comments"></i> Event Id</label>
|
||||||
|
<input type="text" id="node-input-eventId">
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-mark-read">
|
||||||
|
<p>This node marks an event in a Matrix room as read.</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<ul class="message-properties">
|
||||||
|
<li>The input message (<code>msg</code>) should contain the following values, which can be configured to read from different properties:
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>Room ID</dt>
|
||||||
|
<dd>The ID of the room where the event resides. By default, this is read from <code>msg.topic</code>, but it can be configured to read from any property via a typed input.</dd>
|
||||||
|
|
||||||
|
<dt>Event ID</dt>
|
||||||
|
<dd>The event ID you want to mark as read. By default, this is read from <code>msg.eventId</code>, but it can be configured to read from any property via a typed input.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ul class="node-ports">
|
||||||
|
<li>Output 1 (Success):
|
||||||
|
<ul>
|
||||||
|
<li>Triggered when the event is successfully marked as read.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Output 2 (Failure):
|
||||||
|
<ul>
|
||||||
|
<li>Triggered when there is an error marking the event as read. An error message will be included in <code>msg.error</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Usage</h3>
|
||||||
|
<p>This node dynamically reads the room ID and event ID from the message or other properties using typed inputs, allowing you to configure where the values are sourced from. It retrieves the corresponding event and sends a "read" receipt to the Matrix server to mark the event as read. If successful, it will trigger the first output. If an error occurs (e.g., the event or room is not found), the second output is triggered with the error message.</p>
|
||||||
|
</script>
|
82
src/matrix-mark-read.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixMarkRead(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
let node = this;
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomType = n.roomType;
|
||||||
|
this.roomValue = n.roomValue;
|
||||||
|
this.eventIdType = n.eventIdType;
|
||||||
|
this.eventIdValue = n.eventIdValue;
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.error("No configuration node", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("input", async function (msg) {
|
||||||
|
if (!node.server || !node.server.matrixClient) {
|
||||||
|
node.error("No matrix server selected", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if (type === 'flow' || type === 'global') {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch (e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if (type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if (type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let roomId = getToValue(msg, node.roomType, node.roomValue),
|
||||||
|
eventId = getToValue(msg, node.eventIdType, node.eventIdValue);
|
||||||
|
|
||||||
|
const room = node.server.matrixClient.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
throw new Error(`Room ${roomId} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = room.findEventById(eventId);
|
||||||
|
if (!event) {
|
||||||
|
throw new Error(`Event ${eventId} not found in room ${roomId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await node.server.matrixClient.sendReceipt(event, "m.read");
|
||||||
|
node.send([msg, null]);
|
||||||
|
} catch (e) {
|
||||||
|
msg.error = `Room pagination error: ${e}`;
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-mark-read", MatrixMarkRead);
|
||||||
|
}
|
133
src/matrix-paginate-room.html
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('matrix-paginate-room', {
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["Paginated Data", "Error"],
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomType: { value: "msg" },
|
||||||
|
roomValue: { value: "topic" },
|
||||||
|
paginateKeyType: { value: "msg" },
|
||||||
|
paginateKeyValue: { value: "paginationKey" },
|
||||||
|
paginateBackwardsType: { value: "bool" },
|
||||||
|
paginateBackwardsValue: { value: "true" },
|
||||||
|
pageSizeType: { value: "num" },
|
||||||
|
pageSizeValue: { value: "25" }
|
||||||
|
},
|
||||||
|
label: function () {
|
||||||
|
return this.name || "Paginate Room";
|
||||||
|
},
|
||||||
|
paletteLabel: 'Paginate Room',
|
||||||
|
oneditprepare: function () {
|
||||||
|
$("#node-input-room").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types: ['msg', 'flow', 'global', 'str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.roomValue)
|
||||||
|
.typedInput('type', this.roomType);
|
||||||
|
|
||||||
|
$("#node-input-paginateBackwards").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'bool'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.paginateBackwardsValue)
|
||||||
|
.typedInput('type', this.paginateBackwardsType);
|
||||||
|
|
||||||
|
$("#node-input-paginateKey").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.paginateKeyValue)
|
||||||
|
.typedInput('type', this.paginateKeyType);
|
||||||
|
|
||||||
|
$("#node-input-pageSize").typedInput({
|
||||||
|
types: ['msg', 'flow', 'global', 'num'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.pageSizeValue)
|
||||||
|
.typedInput('type', this.pageSizeType);
|
||||||
|
},
|
||||||
|
oneditsave: function () {
|
||||||
|
this.roomType = $("#node-input-room").typedInput('type');
|
||||||
|
this.roomValue = $("#node-input-room").typedInput('value');
|
||||||
|
this.paginateBackwardsType = $("#node-input-paginateBackwards").typedInput('type');
|
||||||
|
this.paginateBackwardsValue = $("#node-input-paginateBackwards").typedInput('value');
|
||||||
|
this.paginateKeyType = $("#node-input-paginateKey").typedInput('type');
|
||||||
|
this.paginateKeyValue = $("#node-input-paginateKey").typedInput('value');
|
||||||
|
this.pageSizeType = $("#node-input-pageSize").typedInput('type');
|
||||||
|
this.pageSizeValue = $("#node-input-pageSize").typedInput('value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-paginate-room">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-server"></i> Matrix Server</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||||
|
<input type="text" id="node-input-room">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-paginateKey"><i class="fa fa-key"></i> Pagination Key</label>
|
||||||
|
<input type="text" id="node-input-paginateKey">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">A unique key to identify the current pagination session. If not provided, a new key will be generated for each session.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-paginateBackwards"><i class="fa fa-arrow-left"></i> Paginate Backwards</label>
|
||||||
|
<input type="text" id="node-input-paginateBackwards">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">Set to true to paginate backwards (older events). Set to false to paginate forwards (newer events).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-pageSize"><i class="fa fa-list"></i> Page Size</label>
|
||||||
|
<input type="text" id="node-input-pageSize">
|
||||||
|
<div class="form-tips" style="margin-top: 10px;">Set the number of events to retrieve per pagination call. It's recommended to keep this value at or below 25 to match the current initial synchronization limit. Adjust this based on your server’s load and capacity to avoid throttling or performance issues.</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-paginate-room">
|
||||||
|
<p>
|
||||||
|
The Matrix Paginate Room node allows you to retrieve historical or future events from a Matrix room, moving forwards or backwards through the event timeline.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>msg</strong> (<em>default</em>): Triggers the pagination action based on the provided room and parameters.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Output 1 (Paginated Data)</strong>: Returns an array of events if more messages are available. Each event contains details such as:
|
||||||
|
<ul>
|
||||||
|
<li><code>msg.encrypted</code> (boolean) - Indicates if the message was encrypted (end-to-end encryption).</li>
|
||||||
|
<li><code>msg.redacted</code> (boolean) - Indicates if the message was redacted (deleted or hidden).</li>
|
||||||
|
<li><code>msg.payload</code> (string) - The message body or content.</li>
|
||||||
|
<li><code>msg.userId</code> (string) - The user ID of the message sender.</li>
|
||||||
|
<li><code>msg.topic</code> (string) - The room ID of the message's origin.</li>
|
||||||
|
<li><code>msg.eventId</code> (string) - The event ID.</li>
|
||||||
|
<li><code>msg.type</code> (string) - The type of message (e.g., <code>m.text</code>, <code>m.image</code>, <code>m.reaction</code>, etc.).</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Output 2 (Error)</strong>: If an error occurs during pagination, the error message is sent to this output.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Dynamic Properties</h3>
|
||||||
|
<p>Some inputs like <strong>Room</strong>, <strong>Pagination Key</strong>, and <strong>Page Size</strong> can be dynamically set using message, flow, or global context variables.</p>
|
||||||
|
|
||||||
|
<h3>Usage</h3>
|
||||||
|
<p>To paginate through a room's timeline, trigger this node with a <code>msg</code> input. The first run will start the timeline, and a unique pagination key will be generated. Future calls can use this key to continue from where you left off. Use the "Paginate Backwards" option to move through older events or set it to false to move forwards through newer events.</p>
|
||||||
|
</script>
|
156
src/matrix-paginate-room.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
const sdkPromise = import("matrix-js-sdk");
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixPaginateRoom(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
let node = this;
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomType = n.roomType;
|
||||||
|
this.roomValue = n.roomValue;
|
||||||
|
this.paginateBackwardsType = n.paginateBackwardsType;
|
||||||
|
this.paginateBackwardsValue = n.paginateBackwardsValue;
|
||||||
|
this.paginateKeyType = n.paginateKeyType;
|
||||||
|
this.paginateKeyValue = n.paginateKeyValue;
|
||||||
|
this.pageSizeType = n.pageSizeType;
|
||||||
|
this.pageSizeValue = n.pageSizeValue;
|
||||||
|
this.timelineWindows = new Map();
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.error("No configuration node", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("input", async function (msg) {
|
||||||
|
if (!node.server || !node.server.matrixClient) {
|
||||||
|
node.error("No matrix server selected", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if (type === 'flow' || type === 'global') {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if (type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if (type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToValue(value, type, property) {
|
||||||
|
if (type === 'global' || type === 'flow') {
|
||||||
|
var contextKey = RED.util.parseContextStore(property);
|
||||||
|
if (/\[msg/.test(contextKey.key)) {
|
||||||
|
// The key has a nested msg. reference that must be evaluated first
|
||||||
|
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true);
|
||||||
|
}
|
||||||
|
var target = node.context()[type];
|
||||||
|
target.set(contextKey.key, value, contextKey.store, err => {
|
||||||
|
if (err) {
|
||||||
|
node.error(err, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (type === 'msg') {
|
||||||
|
if (!RED.util.setMessageProperty(msg, property, value)) {
|
||||||
|
node.warn(RED._("change.errors.no-override", { property: property }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dynamically load the SDK
|
||||||
|
const sdk = await sdkPromise;
|
||||||
|
const TimelineWindow = sdk.TimelineWindow;
|
||||||
|
const RelationType = sdk.RelationType;
|
||||||
|
// (Filter was imported originally but is not used, so we omit it.)
|
||||||
|
|
||||||
|
let roomId = getToValue(msg, node.roomType, node.roomValue),
|
||||||
|
paginateBackwards = getToValue(msg, node.paginateBackwardsType, node.paginateBackwardsValue),
|
||||||
|
pageSize = getToValue(msg, node.pageSizeType, node.pageSizeValue),
|
||||||
|
pageKey = getToValue(msg, node.paginateKeyType, node.paginateKeyValue);
|
||||||
|
|
||||||
|
let room = node.server.matrixClient.getRoom(roomId);
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
throw new Error(`Room ${roomId} does not exist`);
|
||||||
|
}
|
||||||
|
if (pageSize > node.server.initialSyncLimit) {
|
||||||
|
throw new Error(`Page size=${pageSize} cannot exceed initialSyncLimit=${node.server.initialSyncLimit}`);
|
||||||
|
}
|
||||||
|
if (!pageKey) {
|
||||||
|
pageKey = crypto.randomUUID();
|
||||||
|
setToValue(pageKey, node.paginateKeyType, node.paginateKeyValue);
|
||||||
|
}
|
||||||
|
let timelineWindow = node.timelineWindows.get(pageKey),
|
||||||
|
moreMessages = true;
|
||||||
|
if (!timelineWindow) {
|
||||||
|
let timelineSet = room.getUnfilteredTimelineSet();
|
||||||
|
// MatrixClient's option initialSyncLimit gets set to the filter we are using,
|
||||||
|
// so override that value with our pageSize.
|
||||||
|
timelineWindow = new TimelineWindow(node.server.matrixClient, timelineSet);
|
||||||
|
await timelineWindow.load(msg.eventId || null, pageSize);
|
||||||
|
node.timelineWindows.set(pageKey, timelineWindow);
|
||||||
|
} else {
|
||||||
|
moreMessages = await timelineWindow.paginate(paginateBackwards ? 'b' : 'f', pageSize); // 'b' for backwards, 'f' for forwards
|
||||||
|
if (moreMessages) {
|
||||||
|
await timelineWindow.unpaginate(pageSize, !paginateBackwards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid errors converting massive MatrixEvent objects to JSON, we omit them.
|
||||||
|
msg.payload = false;
|
||||||
|
msg.start = timelineWindow.getTimelineIndex('b')?.index;
|
||||||
|
msg.end = timelineWindow.getTimelineIndex('f')?.index;
|
||||||
|
if (moreMessages) {
|
||||||
|
msg.payload = timelineWindow.getEvents().map(function(event) {
|
||||||
|
return {
|
||||||
|
encrypted : event.isEncrypted(),
|
||||||
|
redacted : event.isRedacted(),
|
||||||
|
content : event.getContent(),
|
||||||
|
type : (event.getContent()['msgtype'] || event.getType()) || null,
|
||||||
|
payload : (event.getContent()['body'] || event.getContent()) || null,
|
||||||
|
isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread,
|
||||||
|
mentions : event.getContent()["m.mentions"] || null,
|
||||||
|
userId : event.getSender(),
|
||||||
|
// user : node.matrixClient.getUser(event.getSender()),
|
||||||
|
topic : event.getRoomId(),
|
||||||
|
eventId : event.getId(),
|
||||||
|
event : event.getEffectiveEvent(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
node.send([msg, null]);
|
||||||
|
} catch(e) {
|
||||||
|
msg.error = `Room pagination error: ${e}`;
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-paginate-room", MatrixPaginateRoom);
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
reaction: { value: null }
|
reaction: { value: null }
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -26,30 +27,31 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on("input", function (msg) {
|
node.on("input", function (msg) {
|
||||||
if (!node.server || !node.server.matrixClient) {
|
if (!node.server || !node.server.matrixClient) {
|
||||||
node.error("No matrix server selected");
|
node.error("No matrix server selected", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error("Room must be specified in msg.topic or in configuration");
|
node.error("Room must be specified in msg.topic or in configuration", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = n.reaction || msg.payload;
|
let payload = n.reaction || msg.payload;
|
||||||
if(!payload) {
|
if(!payload) {
|
||||||
node.error('msg.payload must be defined or the reaction configured on the node.');
|
node.error('msg.payload must be defined or the reaction configured on the node.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventId = msg.referenceEventId || msg.eventId;
|
let eventId = msg.referenceEventId || msg.eventId;
|
||||||
if(!eventId) {
|
if(!eventId) {
|
||||||
node.error('Either msg.referenceEventId or msg.eventId must be defined to react to a message.');
|
node.error('Either msg.referenceEventId or msg.eventId must be defined to react to a message.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +77,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-react", MatrixReact);
|
RED.nodes.registerType("matrix-react", MatrixReact);
|
||||||
}
|
}
|
@ -8,10 +8,12 @@
|
|||||||
outputs:1,
|
outputs:1,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: {"value": null},
|
roomId: {"value": null},
|
||||||
|
acceptOwnEvents: {"value": false},
|
||||||
acceptText: {"value": true},
|
acceptText: {"value": true},
|
||||||
acceptEmotes: {"value": true},
|
acceptEmotes: {"value": true},
|
||||||
|
acceptNotices: {"value": true},
|
||||||
acceptStickers: {"value": true},
|
acceptStickers: {"value": true},
|
||||||
acceptReactions: {"value": true},
|
acceptReactions: {"value": true},
|
||||||
acceptFiles: {"value": true},
|
acceptFiles: {"value": true},
|
||||||
@ -45,7 +47,17 @@
|
|||||||
<div class="form-row" style="margin-left: 100px;margin-top:10px;font-weight:bold;">
|
<div class="form-row" style="margin-left: 100px;margin-top:10px;font-weight:bold;">
|
||||||
Timeline event filters
|
Timeline event filters
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-acceptOwnEvents"
|
||||||
|
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||||
|
/>
|
||||||
|
<label for="node-input-acceptOwnEvents" style="width: auto">
|
||||||
|
Receive own events
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptText"
|
id="node-input-acceptText"
|
||||||
@ -55,7 +67,17 @@
|
|||||||
Accept text <code style="text-transform: none;">m.text</code>
|
Accept text <code style="text-transform: none;">m.text</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-acceptNotices"
|
||||||
|
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||||
|
/>
|
||||||
|
<label for="node-input-acceptNotices" style="width: auto">
|
||||||
|
Accept notices <code style="text-transform: none;">m.notice</code>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptEmotes"
|
id="node-input-acceptEmotes"
|
||||||
@ -65,7 +87,7 @@
|
|||||||
Accept emotes <code style="text-transform: none;">m.emote</code>
|
Accept emotes <code style="text-transform: none;">m.emote</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptStickers"
|
id="node-input-acceptStickers"
|
||||||
@ -75,7 +97,7 @@
|
|||||||
Accept stickers <code style="text-transform: none;">m.sticker</code>
|
Accept stickers <code style="text-transform: none;">m.sticker</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptReactions"
|
id="node-input-acceptReactions"
|
||||||
@ -85,7 +107,7 @@
|
|||||||
Accept reactions <code style="text-transform: none;">m.reaction</code>
|
Accept reactions <code style="text-transform: none;">m.reaction</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptFiles"
|
id="node-input-acceptFiles"
|
||||||
@ -95,7 +117,7 @@
|
|||||||
Accept files <code style="text-transform: none;">m.file</code>
|
Accept files <code style="text-transform: none;">m.file</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptAudio"
|
id="node-input-acceptAudio"
|
||||||
@ -105,7 +127,7 @@
|
|||||||
Accept files <code style="text-transform: none;">m.audio</code>
|
Accept files <code style="text-transform: none;">m.audio</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptImages"
|
id="node-input-acceptImages"
|
||||||
@ -115,7 +137,7 @@
|
|||||||
Accept images <code style="text-transform: none;">m.image</code>
|
Accept images <code style="text-transform: none;">m.image</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptVideos"
|
id="node-input-acceptVideos"
|
||||||
@ -125,7 +147,7 @@
|
|||||||
Accept videos <code style="text-transform: none;">m.video</code>
|
Accept videos <code style="text-transform: none;">m.video</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="node-input-acceptLocations"
|
id="node-input-acceptLocations"
|
||||||
|
@ -6,8 +6,10 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
this.name = n.name;
|
this.name = n.name;
|
||||||
this.server = RED.nodes.getNode(n.server);
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.acceptOwnEvents = n.acceptOwnEvents;
|
||||||
this.acceptText = n.acceptText;
|
this.acceptText = n.acceptText;
|
||||||
this.acceptEmotes = n.acceptEmotes;
|
this.acceptEmotes = n.acceptEmotes;
|
||||||
|
this.acceptNotices = n.acceptNotices;
|
||||||
this.acceptStickers = n.acceptStickers;
|
this.acceptStickers = n.acceptStickers;
|
||||||
this.acceptReactions = n.acceptReactions;
|
this.acceptReactions = n.acceptReactions;
|
||||||
this.acceptFiles = n.acceptFiles;
|
this.acceptFiles = n.acceptFiles;
|
||||||
@ -16,14 +18,15 @@ module.exports = function(RED) {
|
|||||||
this.acceptVideos = n.acceptVideos;
|
this.acceptVideos = n.acceptVideos;
|
||||||
this.acceptLocations = n.acceptLocations;
|
this.acceptLocations = n.acceptLocations;
|
||||||
this.roomId = n.roomId;
|
this.roomId = n.roomId;
|
||||||
this.roomIds = this.roomId ? this.roomId.split(',') : [];
|
this.roomIds = this.roomId ? this.roomId.split(',').map(s => s.trim()) : [];
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
if (!node.server) {
|
if (!node.server) {
|
||||||
node.error("No configuration node");
|
node.error("No configuration node", {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.server.on("disconnected", function(){
|
node.server.on("disconnected", function(){
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
@ -35,61 +38,64 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
|
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
|
||||||
// if node has a room ID set we only listen on that room
|
// if node has a room ID set we only listen on that room
|
||||||
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) {
|
if (node.roomIds.length && !node.roomIds.includes(room.roomId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!node.acceptOwnEvents && (!event.getSender() || event.getSender().toLowerCase() === node.server.matrixClient.getUserId().toLowerCase())) {
|
||||||
|
node.log(`Ignoring${msg.encrypted ? ' encrypted' : ''} timeline event [${msg.type}]: (${room.name}) ${event.getId()} for reason: own event`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUrls = (urlKey, encryptedKey) => {
|
||||||
|
const url = msg.encrypted ? msg.content[encryptedKey]?.url : msg.content[urlKey];
|
||||||
|
if (url) {
|
||||||
|
msg.url = node.server.matrixClient.mxcUrlToHttp(url);
|
||||||
|
msg.mxc_url = url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setThumbnailUrls = (infoKey) => {
|
||||||
|
const thumbnailFile = msg.content.info?.[infoKey];
|
||||||
|
const thumbnailUrl = thumbnailFile?.url;
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(thumbnailUrl);
|
||||||
|
msg.thumbnail_mxc_url = thumbnailUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'm.emote':
|
case 'm.emote':
|
||||||
if (!node.acceptEmotes) return;
|
if (!node.acceptEmotes) return;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'm.notice':
|
||||||
|
if (!node.acceptNotices) return;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'm.text':
|
case 'm.text':
|
||||||
if (!node.acceptText) return;
|
if (!node.acceptText) return;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
if (!node.acceptStickers) return;
|
if (!node.acceptStickers) return;
|
||||||
if(msg.content.info) {
|
setThumbnailUrls('thumbnail_url');
|
||||||
if(msg.content.info.thumbnail_url) {
|
setUrls('url', 'url');
|
||||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
|
|
||||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(msg.content.url) {
|
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
|
||||||
msg.mxc_url = msg.content.url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'm.file':
|
case 'm.file':
|
||||||
if (!node.acceptFiles) return;
|
if (!node.acceptFiles) return;
|
||||||
msg.filename = msg.content.filename || msg.content.body;
|
msg.filename = msg.content.filename || msg.content.body;
|
||||||
if(msg.encrypted) {
|
setUrls('url', 'file');
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
|
|
||||||
msg.mxc_url = msg.content.file.url;
|
|
||||||
} else {
|
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
|
||||||
msg.mxc_url = msg.content.url;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'm.audio':
|
case 'm.audio':
|
||||||
if (!node.acceptAudio) return;
|
if (!node.acceptAudio) return;
|
||||||
if(msg.encrypted) {
|
setUrls('url', 'file');
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
|
|
||||||
msg.mxc_url = msg.content.file.url;
|
|
||||||
} else {
|
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
|
||||||
msg.mxc_url = msg.content.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('org.matrix.msc1767.file' in msg.content) {
|
if ('org.matrix.msc1767.file' in msg.content) {
|
||||||
msg.filename = msg.content['org.matrix.msc1767.file'].name;
|
msg.filename = msg.content['org.matrix.msc1767.file'].name;
|
||||||
msg.mimetype = msg.content['org.matrix.msc1767.file'].mimetype;
|
msg.mimetype = msg.content['org.matrix.msc1767.file'].mimetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('org.matrix.msc1767.audio' in msg.content) {
|
if ('org.matrix.msc1767.audio' in msg.content) {
|
||||||
msg.duration = msg.content['org.matrix.msc1767.audio'].duration;
|
msg.duration = msg.content['org.matrix.msc1767.audio'].duration;
|
||||||
msg.waveform = msg.content['org.matrix.msc1767.audio'].waveform;
|
msg.waveform = msg.content['org.matrix.msc1767.audio'].waveform;
|
||||||
@ -99,34 +105,15 @@ module.exports = function(RED) {
|
|||||||
case 'm.image':
|
case 'm.image':
|
||||||
if (!node.acceptImages) return;
|
if (!node.acceptImages) return;
|
||||||
msg.filename = msg.content.filename || msg.content.body;
|
msg.filename = msg.content.filename || msg.content.body;
|
||||||
if(msg.encrypted) {
|
setUrls('url', 'file');
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
|
setThumbnailUrls('thumbnail_file');
|
||||||
msg.mxc_url = msg.content.file.url;
|
|
||||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_file.url);
|
|
||||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_file.url;
|
|
||||||
} else {
|
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
|
||||||
msg.mxc_url = msg.content.url;
|
|
||||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
|
|
||||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case 'm.video':
|
case 'm.video':
|
||||||
if (!node.acceptVideos) return;
|
if (!node.acceptVideos) return;
|
||||||
msg.filename = msg.content.filename || msg.content.body;
|
msg.filename = msg.content.filename || msg.content.body;
|
||||||
if(msg.encrypted) {
|
setUrls('url', 'file');
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
|
setThumbnailUrls('thumbnail_file');
|
||||||
msg.mxc_url = msg.content.file.url;
|
|
||||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_file.url);
|
|
||||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_file.url;
|
|
||||||
} else {
|
|
||||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
|
||||||
msg.mxc_url = msg.content.url;
|
|
||||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
|
|
||||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'm.location':
|
case 'm.location':
|
||||||
@ -143,12 +130,15 @@ module.exports = function(RED) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// node.warn("Unknown event type: " + msg.type);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.send(msg);
|
node.send(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-receive", MatrixReceiveMessage);
|
RED.nodes.registerType("matrix-receive", MatrixReceiveMessage);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
reason: { value: null }
|
reason: { value: null }
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -26,23 +27,24 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on("input", function (msg) {
|
node.on("input", function (msg) {
|
||||||
if (! node.server || ! node.server.matrixClient) {
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
node.error("No matrix server selected");
|
node.error("No matrix server selected", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error("Room must be specified in msg.topic or in configuration");
|
node.error("Room must be specified in msg.topic or in configuration", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
if(!msg.userId) {
|
||||||
node.error("msg.userId was not set.");
|
node.error("msg.userId was not set.", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +55,15 @@ module.exports = function(RED) {
|
|||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
node.error("Error trying to ban " + msg.userId + " from " + msg.topic);
|
node.error("Error trying to ban " + msg.userId + " from " + msg.topic, msg);
|
||||||
msg.error = e;
|
msg.error = e;
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-room-ban", MatrixBan);
|
RED.nodes.registerType("matrix-room-ban", MatrixBan);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs: 1,
|
outputs: 1,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
|
@ -9,9 +9,10 @@ module.exports = function(RED) {
|
|||||||
this.roomId = n.roomId;
|
this.roomId = n.roomId;
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -26,6 +27,10 @@ module.exports = function(RED) {
|
|||||||
node.server.on("Room.invite", async function(msg) {
|
node.server.on("Room.invite", async function(msg) {
|
||||||
node.send(msg);
|
node.send(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-room-invite", MatrixRoomInvite);
|
RED.nodes.registerType("matrix-room-invite", MatrixRoomInvite);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
reason: { value: null }
|
reason: { value: null }
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -26,23 +27,24 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on("input", function (msg) {
|
node.on("input", function (msg) {
|
||||||
if (! node.server || ! node.server.matrixClient) {
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
node.error("No matrix server selected");
|
node.error("No matrix server selected", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error("Room must be specified in msg.topic or in configuration");
|
node.error("Room must be specified in msg.topic or in configuration", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
if(!msg.userId) {
|
||||||
node.error("msg.userId was not set.");
|
node.error("msg.userId was not set.", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +55,15 @@ module.exports = function(RED) {
|
|||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
node.error("Error trying to kick " + msg.userId + " from " + msg.topic);
|
node.error("Error trying to kick " + msg.userId + " from " + msg.topic, msg);
|
||||||
msg.error = e;
|
msg.error = e;
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-room-kick", MatrixKick);
|
RED.nodes.registerType("matrix-room-kick", MatrixKick);
|
||||||
}
|
}
|
369
src/matrix-room-state-events.html
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
<script type="text/html" data-template-name="matrix-room-state-events">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||||
|
<input type="text" id="node-input-room">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
|
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row node-input-rule-container-row">
|
||||||
|
<ol id="node-input-rule-container"></ol>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-room-state-events">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>
|
||||||
|
Set and Get a list of room state events for a given room. Allows you to set/get room name, topic, avatar, etc.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt class="optional">msg.topic
|
||||||
|
<span class="property-type">string | null</span>
|
||||||
|
</dt>
|
||||||
|
<dd> The room to set/get settings for.</dd>
|
||||||
|
|
||||||
|
<dt class="optional">dynamic
|
||||||
|
<span class="property-type">string|object</span>
|
||||||
|
</dt>
|
||||||
|
<dd> You configure what room state events in the node configuration. <code style="white-space: normal;">m.room.name</code>, <code style="white-space: normal;">m.room.avatar</code>, and <code style="white-space: normal;">m.room.guest_access</code> allow you to pass a string to set their value but all other room state events will require the full content object (find this by referencing the <a href="https://spec.matrix.org/latest/client-server-api" target="_blank">Matrix Client-Server docs</a>)</dd>
|
||||||
|
|
||||||
|
<dt class="optional">msg.state_key
|
||||||
|
<span class="property-type">string</span>
|
||||||
|
</dt>
|
||||||
|
<dd> Required for some events such as <code style="white-space: normal;">m.space.parent</code> and <code style="white-space: normal;">m.room.child</code> to set the referenced child/parent room</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ol class="node-ports">
|
||||||
|
<li>Success
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg <span class="property-type">object</span></dt>
|
||||||
|
<dd>Original message object with modifications based on config.</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.setter_errors <span class="property-type">undefined|object</span></dt>
|
||||||
|
<dd>Returned if setting a room state event failed. The key of the object is the room state event type and the value is the error that occurred.</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.getter_errors <span class="property-type">undefined|object</span></dt>
|
||||||
|
<dd>Returned if getting a room state event failed. The key of the object is the room state event type and the value is the error that occurred. Note that you will get an error if you try getting a room state event that doesn't exist (such as fetching avatar on a room that doesn't have one).</dd>
|
||||||
|
</dl>
|
||||||
|
<dt class="optional">dynamic
|
||||||
|
<span class="property-type">string|object</span>
|
||||||
|
</dt>
|
||||||
|
<dd> You configure what room state events to output in the node configuration. <code style="white-space: normal;">m.room.name</code>, <code style="white-space: normal;">m.room.avatar</code>, and <code style="white-space: normal;">m.room.guest_access</code> will come back as strings otherwise you will get the full content object of the event (find this by referencing the <a href="https://spec.matrix.org/latest/client-server-api" target="_blank">Matrix Client-Server docs</a>). Additionally there is a setting when configuring a getter called "Fetch from local storage" that if enabled will search the local storage for the room and try to fetch the state event that way and fallback to hitting the server if that isn't possible.</dd>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(){
|
||||||
|
var roomEventTypeOptions = [
|
||||||
|
{ value: "m.room.name", label: "m.room.name" },
|
||||||
|
{ value: "m.room.topic", label: "m.room.topic" },
|
||||||
|
{ value: "m.room.avatar", label: "m.room.avatar" },
|
||||||
|
{ value: "m.room.power_levels", label: "m.room.power_levels" },
|
||||||
|
{ value: "m.room.guest_access", label: "m.room.guest_access" },
|
||||||
|
{ value: "m.room.join_rules", label: "m.room.join_rules" },
|
||||||
|
{ value: "m.room.canonical_alias", label: "m.room.canonical_alias" },
|
||||||
|
{ value: "m.room.history_visibility", label: "m.room.history_visibility" },
|
||||||
|
{ value: "m.room.server_acl", label: "m.room.server_acl" },
|
||||||
|
{ value: "m.room.pinned_events", label: "m.room.pinned_events"},
|
||||||
|
{ value: "m.space.child", label: "m.space.child" },
|
||||||
|
{ value: "m.space.parent", label: "m.space.parent" },
|
||||||
|
];
|
||||||
|
var defaultRules = [{
|
||||||
|
t: "set",
|
||||||
|
p: roomEventTypeOptions[0].value,
|
||||||
|
to: "payload",
|
||||||
|
tot: "msg"
|
||||||
|
}];
|
||||||
|
|
||||||
|
function isInvalidProperty(v,vt) {
|
||||||
|
if (/msg|flow|global/.test(vt)) {
|
||||||
|
if (!RED.utils.validatePropertyExpression(v)) {
|
||||||
|
return "Invalid property: " + v;
|
||||||
|
}
|
||||||
|
} else if (vt === "jsonata") {
|
||||||
|
try{ jsonata(v); } catch(e) {
|
||||||
|
return "Invalid expression: " + e.message;
|
||||||
|
}
|
||||||
|
} else if (vt === "json") {
|
||||||
|
try{ JSON.parse(v); } catch(e) {
|
||||||
|
return "Invalid JSON data: " + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType('matrix-room-state-events',{
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
inputs:1,
|
||||||
|
outputs:2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomType: { value: "msg" },
|
||||||
|
roomValue: { value: "topic" },
|
||||||
|
rules: {
|
||||||
|
value: defaultRules,
|
||||||
|
validate: function(rules, opt) {
|
||||||
|
let msg;
|
||||||
|
const errors = []
|
||||||
|
if (!rules || rules.length === 0) { return true }
|
||||||
|
for (let i=0;i<rules.length;i++) {
|
||||||
|
const opt = { label: "Rule"+' '+(i+1) }
|
||||||
|
const r = rules[i];
|
||||||
|
if (r.t === 'set' || r.t === 'get') {
|
||||||
|
if ((msg = isInvalidProperty(r.p,r.pt)) !== false) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
if ((msg = isInvalidProperty(r.to,r.tot)) !== false) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.length ? errors : true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
$("#node-input-room").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.roomValue)
|
||||||
|
.typedInput('type', this.roomType);
|
||||||
|
|
||||||
|
var set = "Set";
|
||||||
|
var to = "to the value";
|
||||||
|
var toValueLabel = "to the property";
|
||||||
|
var search = this._("change.action.search");
|
||||||
|
var replace = this._("change.action.replace");
|
||||||
|
var regex = this._("change.label.regex");
|
||||||
|
|
||||||
|
function createPropertyValue(row2_1, row2_2, type, defaultType) {
|
||||||
|
var propValInput = $('<input/>',{class:"node-input-rule-property-value",type:"text"})
|
||||||
|
.appendTo(row2_1)
|
||||||
|
.typedInput({
|
||||||
|
default: defaultType || (type === 'set' ? 'str' : 'msg'),
|
||||||
|
types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])
|
||||||
|
});
|
||||||
|
|
||||||
|
var lsLabel = $('<label style="padding-left: 130px;"></label>').appendTo(row2_2);
|
||||||
|
var localStorageEl = $('<input type="checkbox" class="node-input-rule-property-localStorage" style="width: auto; margin: 0 6px 0 0">').appendTo(lsLabel);
|
||||||
|
$('<span>').text("Fetch from local storage").appendTo(lsLabel);
|
||||||
|
|
||||||
|
propValInput.on("change", function(evt,type,val) {
|
||||||
|
row2_2.toggle(type === "msg" || type === "flow" || type === "global" || type === "env");
|
||||||
|
})
|
||||||
|
return [propValInput, localStorageEl];
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({
|
||||||
|
addItem: function(container,i,opt) {
|
||||||
|
var rule = opt;
|
||||||
|
if (!rule.hasOwnProperty('t')) {
|
||||||
|
rule = {t:"set",p:roomEventTypeOptions[0].value,to:"payload",tot:"msg"};
|
||||||
|
}
|
||||||
|
if (rule.t === "set" && !rule.tot) {
|
||||||
|
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
|
||||||
|
rule.to = rule.to.substring(4);
|
||||||
|
rule.tot = "msg";
|
||||||
|
} else {
|
||||||
|
rule.tot = "str";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.css({
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
});
|
||||||
|
let fragment = document.createDocumentFragment();
|
||||||
|
var row1 = $('<div/>',{style:"display:flex; align-items: center"}).appendTo(fragment);
|
||||||
|
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
|
||||||
|
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
|
||||||
|
var row4 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(fragment);
|
||||||
|
|
||||||
|
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width:110px; margin-right:10px;"}).appendTo(row1);
|
||||||
|
var selectOptions = [
|
||||||
|
{v:"set",l:"Set"},
|
||||||
|
{v:"get",l:"Get"}
|
||||||
|
];
|
||||||
|
for (var x=0; x<selectOptions.length; x++) {
|
||||||
|
selectField.append($("<option></option>").val(selectOptions[x].v).text(selectOptions[x].l));
|
||||||
|
}
|
||||||
|
|
||||||
|
var propertyName = $('<input/>',{class:"node-input-rule-property-name",type:"text"})
|
||||||
|
.appendTo(row1)
|
||||||
|
.autoComplete({
|
||||||
|
minLength:0,
|
||||||
|
search: function(val) {
|
||||||
|
if(!val) {
|
||||||
|
return roomEventTypeOptions.sort(function(A,B){return A.i-B.i});
|
||||||
|
}
|
||||||
|
var matches = [];
|
||||||
|
roomEventTypeOptions.map((x) => x.value).forEach(v => {
|
||||||
|
var i = v.toLowerCase().indexOf(val.toLowerCase());
|
||||||
|
if (i > -1) {
|
||||||
|
matches.push({
|
||||||
|
value: v,
|
||||||
|
label: v,
|
||||||
|
i: i
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
matches.sort(function(A,B){return A.i-B.i})
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('focus', function(evt){
|
||||||
|
// following is a fix so autocomplete will show list on focus
|
||||||
|
if(!evt.isTrigger) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
$(this).trigger("keyup.red-ui-autoComplete");
|
||||||
|
}
|
||||||
|
}).on("keyup", function(evt) {
|
||||||
|
// following allows autocomplete to display even when backspace/delete is used
|
||||||
|
if (evt.keyCode === 8 || evt.keyCode === 46) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
$(this).trigger("keyup.red-ui-autoComplete");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var row2_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row2);
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(toValueLabel)
|
||||||
|
.appendTo(row2_1);
|
||||||
|
|
||||||
|
var row2_2 = $('<div/>', {style:"margin-top: 4px;"}).appendTo(row2);
|
||||||
|
|
||||||
|
var row3_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row3);
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(search)
|
||||||
|
.appendTo(row3_1);
|
||||||
|
|
||||||
|
var row3_2 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(row3);
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(replace)
|
||||||
|
.appendTo(row3_2);
|
||||||
|
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(to)
|
||||||
|
.appendTo(row4);
|
||||||
|
|
||||||
|
let propertyValue = null;
|
||||||
|
let localStorageEl = null;
|
||||||
|
let fromValue = null;
|
||||||
|
let toValue = null;
|
||||||
|
|
||||||
|
selectField.on("change", function() {
|
||||||
|
var type = $(this).val();
|
||||||
|
if (propertyValue) {
|
||||||
|
propertyValue.typedInput('hide');
|
||||||
|
}
|
||||||
|
if (fromValue) {
|
||||||
|
fromValue.typedInput('hide');
|
||||||
|
}
|
||||||
|
if (toValue) {
|
||||||
|
toValue.typedInput('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!propertyValue) {
|
||||||
|
var parts = createPropertyValue(row2_1, row2_2, type);
|
||||||
|
propertyValue = parts[0];
|
||||||
|
localStorageEl = parts[1];
|
||||||
|
} else {
|
||||||
|
propertyValue.typedInput('types', (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global']));
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyValue.typedInput('show');
|
||||||
|
row2.show();
|
||||||
|
if(type === 'get') {
|
||||||
|
localStorageEl.parent().show();
|
||||||
|
} else {
|
||||||
|
localStorageEl.parent().hide();
|
||||||
|
}
|
||||||
|
row3.hide();
|
||||||
|
row4.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectField.val(rule.t);
|
||||||
|
propertyName.val(rule.p);
|
||||||
|
if (rule.t === "set" || rule.t === "get") {
|
||||||
|
var parts = createPropertyValue(row2_1, row2_2, rule.t, rule.tot);
|
||||||
|
propertyValue = parts[0];
|
||||||
|
localStorageEl = parts[1];
|
||||||
|
propertyValue.typedInput('value',rule.to);
|
||||||
|
localStorageEl.prop("checked", !!rule.ls);
|
||||||
|
if(rule.t === 'get') {
|
||||||
|
localStorageEl.parent().show();
|
||||||
|
} else {
|
||||||
|
localStorageEl.parent().hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectField.change();
|
||||||
|
container[0].appendChild(fragment);
|
||||||
|
},
|
||||||
|
removable: true,
|
||||||
|
sortable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i=0; i<this.rules.length; i++) {
|
||||||
|
var rule = this.rules[i];
|
||||||
|
$("#node-input-rule-container").editableList('addItem',rule);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.roomType = $("#node-input-room").typedInput('type');
|
||||||
|
this.roomValue = $("#node-input-room").typedInput('value');
|
||||||
|
|
||||||
|
var rules = $("#node-input-rule-container").editableList('items');
|
||||||
|
var node = this;
|
||||||
|
node.rules= [];
|
||||||
|
rules.each(function(i) {
|
||||||
|
var rule = $(this);
|
||||||
|
var type = rule.find(".node-input-rule-type").val();
|
||||||
|
var r = {
|
||||||
|
t:type,
|
||||||
|
p:rule.find(".node-input-rule-property-name").val(),
|
||||||
|
to:rule.find(".node-input-rule-property-value").typedInput('value'),
|
||||||
|
tot:rule.find(".node-input-rule-property-value").typedInput('type')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (r.t === "get" && rule.find(".node-input-rule-property-localStorage").prop("checked")) {
|
||||||
|
r.ls = true;
|
||||||
|
}
|
||||||
|
node.rules.push(r);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
oneditresize: function(size) {
|
||||||
|
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
|
||||||
|
var height = size.height;
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
height -= $(rows[i]).outerHeight(true);
|
||||||
|
}
|
||||||
|
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
|
||||||
|
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
||||||
|
height += 16;
|
||||||
|
$("#node-input-rule-container").editableList('height',height);
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Room State Events";
|
||||||
|
},
|
||||||
|
paletteLabel: 'Room State Events'
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
293
src/matrix-room-state-events.js
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixRoomStateEvents(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomId = n.roomId;
|
||||||
|
this.rules = n.rules;
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.warn("No configuration node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("input", async function (msg) {
|
||||||
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
|
msg.error = "No matrix server selected";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!node.server.isConnected()) {
|
||||||
|
msg.error = "Matrix server connection is currently closed";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.topic = node.roomId || msg.topic;
|
||||||
|
if(!msg.topic) {
|
||||||
|
msg.error = "Room must be specified in msg.topic or in configuration";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let getterErrors = {},
|
||||||
|
setterErrors = {};
|
||||||
|
|
||||||
|
if(!Array.isArray(node.rules) || !node.rules.length) {
|
||||||
|
node.warn("No rules configured, skipping", msg);
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, rule) {
|
||||||
|
var value = rule.to;
|
||||||
|
if (rule.tot === 'json') {
|
||||||
|
try {
|
||||||
|
value = JSON.parse(rule.to);
|
||||||
|
} catch(e) {
|
||||||
|
throw new Error("Invalid JSON");
|
||||||
|
}
|
||||||
|
} else if (rule.tot === 'bin') {
|
||||||
|
try {
|
||||||
|
value = Buffer.from(JSON.parse(rule.to))
|
||||||
|
} catch(e) {
|
||||||
|
throw new Error("Invalid Binary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rule.tot === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg,rule.to);
|
||||||
|
} else if ((rule.tot === 'flow') || (rule.tot === 'global')) {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if (rule.tot === 'date') {
|
||||||
|
value = Date.now();
|
||||||
|
} else if (rule.tot === 'jsonata') {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateJSONataExpression(rule.to,msg);
|
||||||
|
} catch(e3) {
|
||||||
|
throw new Error("Invalid expression");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToValue(value, rule) {
|
||||||
|
if(rule.tot === 'global' || rule.tot === 'flow') {
|
||||||
|
var contextKey = RED.util.parseContextStore(rule.to);
|
||||||
|
if (/\[msg/.test(contextKey.key)) {
|
||||||
|
// The key has a nest msg. reference to evaluate first
|
||||||
|
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true)
|
||||||
|
}
|
||||||
|
var target = node.context()[rule.tot];
|
||||||
|
var callback = err => {
|
||||||
|
if (err) {
|
||||||
|
node.error(err, msg);
|
||||||
|
getterErrors[rule.p] = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.set(contextKey.key, value, contextKey.store, callback);
|
||||||
|
} else if(rule.tot === 'msg') {
|
||||||
|
if (!RED.util.setMessageProperty(msg, rule.to, value)) {
|
||||||
|
node.warn(RED._("change.errors.no-override",{property:rule.to}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let rule of node.rules) {
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "t": "set",
|
||||||
|
// "p": "m.room.topic",
|
||||||
|
// "to": "asdf",
|
||||||
|
// "tot": "str"
|
||||||
|
// }, ...
|
||||||
|
// ]
|
||||||
|
|
||||||
|
let cachedGetters = {};
|
||||||
|
if(rule.t === 'set') {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
value = getToValue(msg, rule);
|
||||||
|
switch(rule.p) {
|
||||||
|
case "m.room.name":
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.name",
|
||||||
|
typeof value === "string"
|
||||||
|
? {name: value}
|
||||||
|
: value);
|
||||||
|
break;
|
||||||
|
case "m.room.topic":
|
||||||
|
if (typeof value === "string") {
|
||||||
|
await node.server.matrixClient.setRoomTopic(msg.topic, value);
|
||||||
|
} else {
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.topic",
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "m.room.avatar":
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.avatar",
|
||||||
|
typeof value === "string"
|
||||||
|
? {"url": value}
|
||||||
|
: value,
|
||||||
|
"");
|
||||||
|
break;
|
||||||
|
case "m.room.join_rules":
|
||||||
|
if(typeof value !== 'object') {
|
||||||
|
setterErrors[rule.p] = "m.room.join_rules content must be object";
|
||||||
|
} else {
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.join_rules",
|
||||||
|
value,
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "m.room.canonical_alias":
|
||||||
|
if(typeof value !== 'object') {
|
||||||
|
setterErrors[rule.p] = "m.room.canonical_alias content must be object";
|
||||||
|
} else {
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.canonical_alias",
|
||||||
|
value,
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "m.space.parent":
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
setterErrors[rule.p] = "m.space.parent content must be object";
|
||||||
|
} else if (!msg.state_key) {
|
||||||
|
setterErrors[rule.p] = "m.space.parent required msg.state_key input to be set to the child roomId";
|
||||||
|
}else {
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.power_levels",
|
||||||
|
value,
|
||||||
|
msg.state_key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "m.space.child":
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
setterErrors[rule.p] = "m.space.child content must be object";
|
||||||
|
} else if (!msg.state_key) {
|
||||||
|
setterErrors[rule.p] = "m.space.child required msg.state_key input to be set to the parent roomId";
|
||||||
|
}else {
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.power_levels",
|
||||||
|
value,
|
||||||
|
msg.state_key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "m.room.guest_access":
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
"m.room.guest_access",
|
||||||
|
typeof value === "string"
|
||||||
|
? { "guest_access": value }
|
||||||
|
: value,
|
||||||
|
"");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if(typeof value !== 'object') {
|
||||||
|
setterErrors[rule.p] = `${rule.p} content must be object`;
|
||||||
|
} else {
|
||||||
|
await node.server.matrixClient.sendStateEvent(
|
||||||
|
msg.topic,
|
||||||
|
rule.p,
|
||||||
|
value,
|
||||||
|
msg.state_key || "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
setterErrors[rule.p] = e.message;
|
||||||
|
}
|
||||||
|
} else if(rule.t === 'get') {
|
||||||
|
let value;
|
||||||
|
if(cachedGetters.hasOwnProperty(rule.p)) {
|
||||||
|
value = cachedGetters[rule.p];
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if(rule.ls) {
|
||||||
|
// we opted to lookup from local storage, will fallback to server if necessary
|
||||||
|
let room = node.server.matrixClient.getRoom(msg.topic);
|
||||||
|
if(room) {
|
||||||
|
value = await room.getLiveTimeline().getState("f").getStateEvents(rule.p, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!value) {
|
||||||
|
// fetch the latest state event by type from server
|
||||||
|
value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, "");
|
||||||
|
if(value) {
|
||||||
|
// normalize some simpler events for easier access
|
||||||
|
switch(rule.p) {
|
||||||
|
case "m.room.name":
|
||||||
|
value = value?.name
|
||||||
|
break;
|
||||||
|
case "m.room.topic":
|
||||||
|
value = value?.topic
|
||||||
|
break;
|
||||||
|
case "m.room.avatar":
|
||||||
|
value = value?.url
|
||||||
|
break;
|
||||||
|
case "m.room.guest_access":
|
||||||
|
value = value?.guest_access;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setToValue(value, rule);
|
||||||
|
cachedGetters[rule.p] = value;
|
||||||
|
} catch(e) {
|
||||||
|
getterErrors[rule.p] = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Object.keys(setterErrors).length) {
|
||||||
|
msg.setter_errors = setterErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Object.keys(getterErrors).length) {
|
||||||
|
msg.getter_errors = getterErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.send([msg, null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-room-state-events", MatrixRoomStateEvents);
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: "" }
|
roomId: { value: "" }
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -31,13 +32,14 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomId = node.roomId || msg.topic;
|
let roomId = node.roomId || msg.topic;
|
||||||
if(!roomId) {
|
if(!roomId) {
|
||||||
node.error("msg.topic is required. Specify in the input or configure the room ID on the node.");
|
node.error("msg.topic is required. Specify in the input or configure the room ID on the node.", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +67,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-room-users", MatrixRoomUsers);
|
RED.nodes.registerType("matrix-room-users", MatrixRoomUsers);
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
RED.nodes.registerType('matrix-send-file',{
|
RED.nodes.registerType('matrix-send-file',{
|
||||||
category: 'matrix',
|
category: 'matrix (deprecated)',
|
||||||
color: '#00b7ca',
|
color: '#00b7ca',
|
||||||
icon: "matrix.png",
|
icon: "matrix.png",
|
||||||
outputLabels: ["success", "error"],
|
outputLabels: ["success", "error"],
|
||||||
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
contentType: { value: null }
|
contentType: { value: null }
|
||||||
},
|
},
|
||||||
@ -20,6 +20,26 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/html" data-template-name="matrix-send-file">
|
<script type="text/html" data-template-name="matrix-send-file">
|
||||||
|
<style>
|
||||||
|
.matrix-chat-alert {
|
||||||
|
color: #b94a48;
|
||||||
|
background-color: #f2dede;
|
||||||
|
border-color: #eed3d7;
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 8px 35px 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-chat-alert a, .matrix-chat-alert a:active, .matrix-chat-alert a:hover {
|
||||||
|
color: #b94a48;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="matrix-chat-alert">
|
||||||
|
<strong>Deprecation Warning!</strong> This node will be removed in a future release. We have consolidated the Send File & Send Image nodes into a single Upload File node (with more options). Read <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/issues/102" target="_blank">here</a> for more info.<br>
|
||||||
|
<br>
|
||||||
|
Replace this node with a File Upload node attached to a Send Message node to accomplish the same behavior.
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
<input type="text" id="node-input-name" placeholder="Name">
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -31,8 +32,9 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
@ -57,7 +59,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.payload) {
|
if(!msg.payload) {
|
||||||
node.error('msg.payload is required');
|
node.error('msg.payload is required', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +96,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-send-file", MatrixSendFile);
|
RED.nodes.registerType("matrix-send-file", MatrixSendFile);
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
RED.nodes.registerType('matrix-send-image',{
|
RED.nodes.registerType('matrix-send-image',{
|
||||||
category: 'matrix',
|
category: 'matrix (deprecated)',
|
||||||
color: '#00b7ca',
|
color: '#00b7ca',
|
||||||
icon: "matrix.png",
|
icon: "matrix.png",
|
||||||
outputLabels: ["success", "error"],
|
outputLabels: ["success", "error"],
|
||||||
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
contentType: { value: null }
|
contentType: { value: null }
|
||||||
},
|
},
|
||||||
@ -20,6 +20,26 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/html" data-template-name="matrix-send-image">
|
<script type="text/html" data-template-name="matrix-send-image">
|
||||||
|
<style>
|
||||||
|
.matrix-chat-alert {
|
||||||
|
color: #b94a48;
|
||||||
|
background-color: #f2dede;
|
||||||
|
border-color: #eed3d7;
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 8px 35px 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-chat-alert a, .matrix-chat-alert a:active, .matrix-chat-alert a:hover {
|
||||||
|
color: #b94a48;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="matrix-chat-alert">
|
||||||
|
<strong>Deprecation Warning!</strong> This node will be removed in a future release. We have consolidated the Send File & Send Image nodes into a single Upload File node (with more options). Read <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/issues/102" target="_blank">here</a> for more info.<br>
|
||||||
|
<br>
|
||||||
|
Replace this node with a File Upload node attached to a Send Message node to accomplish the same behavior.
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
<input type="text" id="node-input-name" placeholder="Name">
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
@ -13,6 +13,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -31,8 +32,9 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
@ -57,7 +59,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.payload) {
|
if(!msg.payload) {
|
||||||
node.error('msg.payload is required');
|
node.error('msg.payload is required', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +100,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-send-image", MatrixSendImage);
|
RED.nodes.registerType("matrix-send-image", MatrixSendImage);
|
||||||
}
|
}
|
@ -6,19 +6,32 @@
|
|||||||
outputLabels: ["success", "error"],
|
outputLabels: ["success", "error"],
|
||||||
inputs:1,
|
inputs:1,
|
||||||
outputs:2,
|
outputs:2,
|
||||||
|
paletteLabel: 'Send Message',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
message: { value: null },
|
message: { value: null },
|
||||||
messageType: { value: 'm.text' },
|
messageType: { value: 'm.text' },
|
||||||
messageFormat: { value: '' },
|
messageFormat: { value: '' },
|
||||||
replaceMessage : { value: false }
|
replaceMessage : { value: false },
|
||||||
|
threadReplyType: { value: "msg" },
|
||||||
|
threadReplyValue: { value: "isThread" },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "Send Message";
|
return this.name || "Send Message";
|
||||||
},
|
},
|
||||||
paletteLabel: 'Send Message'
|
oneditprepare: function() {
|
||||||
|
$("#node-input-threadReply").typedInput({
|
||||||
|
types:['msg','flow','global','bool'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.threadReplyValue || "isThread")
|
||||||
|
.typedInput('type', this.threadReplyType || "msg");
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.threadReplyType = $("#node-input-threadReply").typedInput('type');
|
||||||
|
this.threadReplyValue = $("#node-input-threadReply").typedInput('value');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -43,6 +56,9 @@
|
|||||||
<label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
|
<label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
|
||||||
<textarea id="node-input-message" placeholder="msg.payload" style="width: 70%;"></textarea>
|
<textarea id="node-input-message" placeholder="msg.payload" style="width: 70%;"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row form-tips">
|
||||||
|
If message is an object it sets the full content of the message.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input
|
<input
|
||||||
@ -69,6 +85,14 @@
|
|||||||
It's recommended to use m.notice for bots because the message will render in a lighter text (at least in Element client) for users to distinguish bot and real user messages.
|
It's recommended to use m.notice for bots because the message will render in a lighter text (at least in Element client) for users to distinguish bot and real user messages.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-threadReply"><i class="fa fa-commenting-o"></i> Thread Reply</label>
|
||||||
|
<input type="text" id="node-input-threadReply">
|
||||||
|
</div>
|
||||||
|
<div class="form-row form-tips">
|
||||||
|
If true and <code>msg.content.['m.relates_to'].event_id</code> or <code>msg.eventId</code> (parsed in that order) is provided the message will be a thread reply to the original event.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-messageFormat">
|
<label for="node-input-messageFormat">
|
||||||
Message Format
|
Message Format
|
||||||
@ -104,9 +128,9 @@
|
|||||||
<dd> Room ID to send image to. Optional if configured on the node. If configured on the node this input will be overridden.</dd>
|
<dd> Room ID to send image to. Optional if configured on the node. If configured on the node this input will be overridden.</dd>
|
||||||
|
|
||||||
<dt>msg.payload
|
<dt>msg.payload
|
||||||
<span class="property-type">string</span>
|
<span class="property-type">string|object</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd> the message text. If configured on the node this is ignored otherwise it required. </dd>
|
<dd> the message text or an object to customize the full content. If configured on the node this is ignored otherwise it required. </dd>
|
||||||
|
|
||||||
<dt>msg.replace
|
<dt>msg.replace
|
||||||
<span class="property-type">bool</span>
|
<span class="property-type">bool</span>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
const {RelationType} = require("matrix-js-sdk");
|
const sdkPromise = import("matrix-js-sdk");
|
||||||
|
|
||||||
module.exports = function(RED) {
|
module.exports = function(RED) {
|
||||||
function MatrixSendImage(n) {
|
function MatrixSendImage(n) {
|
||||||
RED.nodes.createNode(this, n);
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
var node = this;
|
var node = this;
|
||||||
|
|
||||||
this.name = n.name;
|
this.name = n.name;
|
||||||
@ -13,6 +12,8 @@ module.exports = function(RED) {
|
|||||||
this.messageFormat = n.messageFormat;
|
this.messageFormat = n.messageFormat;
|
||||||
this.replaceMessage = n.replaceMessage;
|
this.replaceMessage = n.replaceMessage;
|
||||||
this.message = n.message;
|
this.message = n.message;
|
||||||
|
this.threadReplyType = n.threadReplyType || null;
|
||||||
|
this.threadReplyValue = n.threadReplyValue || null;
|
||||||
|
|
||||||
// taken from https://github.com/matrix-org/synapse/blob/master/synapse/push/mailer.py
|
// taken from https://github.com/matrix-org/synapse/blob/master/synapse/push/mailer.py
|
||||||
this.allowedTags = [
|
this.allowedTags = [
|
||||||
@ -54,6 +55,7 @@ module.exports = function(RED) {
|
|||||||
node.warn("No configuration node");
|
node.warn("No configuration node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
@ -65,25 +67,33 @@ module.exports = function(RED) {
|
|||||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
});
|
});
|
||||||
|
|
||||||
node.on("input", function (msg) {
|
// Make the input handler async so we can await the dynamic import.
|
||||||
|
node.on("input", async function (msg) {
|
||||||
|
// Await the SDK import and get the RelationType constant.
|
||||||
|
const sdk = await sdkPromise;
|
||||||
|
const RelationType = sdk.RelationType;
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if (type === 'flow' || type === 'global') {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if(type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if(type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
let msgType = node.messageType,
|
let msgType = node.messageType,
|
||||||
msgFormat = node.messageFormat;
|
msgFormat = node.messageFormat,
|
||||||
|
threadReply = getToValue(msg, node.threadReplyType, node.threadReplyValue);
|
||||||
if(msgType === 'msg.type') {
|
|
||||||
if(!msg.type) {
|
|
||||||
node.error("msg.type type is set to be passed in via msg.type but was not defined");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
msgType = msg.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(msgFormat === 'msg.format') {
|
|
||||||
if(!msg.format) {
|
|
||||||
node.error("Message format is set to be passed in via msg.format but was not defined");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
msgFormat = msg.format;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!node.server || !node.server.matrixClient) {
|
if (!node.server || !node.server.matrixClient) {
|
||||||
node.warn("No matrix server selected");
|
node.warn("No matrix server selected");
|
||||||
@ -91,7 +101,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -104,11 +114,31 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
let payload = n.message || msg.payload;
|
let payload = n.message || msg.payload;
|
||||||
if(!payload) {
|
if(!payload) {
|
||||||
node.error('msg.payload must be defined or the message configured on the node.');
|
node.error('msg.payload must be defined or the message configured on the node.', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = {
|
let content = null;
|
||||||
|
if (typeof payload === 'object') {
|
||||||
|
content = payload;
|
||||||
|
} else {
|
||||||
|
if (msgType === 'msg.type') {
|
||||||
|
if (!msg.type) {
|
||||||
|
node.error("msg.type is set to be passed in via msg.type but was not defined", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msgType = msg.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgFormat === 'msg.format') {
|
||||||
|
if (!Object.hasOwn(msg, 'format')) {
|
||||||
|
node.error("Message format is set to be passed in via msg.format but was not defined", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msgFormat = msg.format;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = {
|
||||||
msgtype: msgType,
|
msgtype: msgType,
|
||||||
body: payload.toString()
|
body: payload.toString()
|
||||||
};
|
};
|
||||||
@ -138,20 +168,44 @@ module.exports = function(RED) {
|
|||||||
event_id: msg.eventId
|
event_id: msg.eventId
|
||||||
};
|
};
|
||||||
content['body'] = ' * ' + content['body'];
|
content['body'] = ' * ' + content['body'];
|
||||||
|
} else if (threadReply) {
|
||||||
|
// If incoming message is a reply to a thread we fetch the thread parent from m.relates_to,
|
||||||
|
// otherwise fallback to msg.eventId.
|
||||||
|
let threadParent = (msg?.content?.['m.relates_to']?.rel_type === RelationType.Thread
|
||||||
|
? msg?.content?.['m.relates_to']?.event_id
|
||||||
|
: null) || msg.eventId;
|
||||||
|
if (threadParent) {
|
||||||
|
content["m.relates_to"] = {
|
||||||
|
"rel_type": RelationType.Thread,
|
||||||
|
"event_id": threadParent,
|
||||||
|
"is_falling_back": true,
|
||||||
|
};
|
||||||
|
if (msg.eventId !== threadParent) {
|
||||||
|
content["m.relates_to"]["m.in_reply_to"] = {
|
||||||
|
"event_id": msg.eventId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node.server.matrixClient.sendMessage(msg.topic, content)
|
node.server.matrixClient.sendMessage(msg.topic, content)
|
||||||
.then(function(e) {
|
.then(function(e) {
|
||||||
node.log("Message sent: " + payload);
|
node.log("Message sent: " + payload);
|
||||||
msg.eventId = e.event_id;
|
msg.eventId = e.event_id;
|
||||||
|
node.log(JSON.stringify(e));
|
||||||
node.send([msg, null]);
|
node.send([msg, null]);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
node.warn("Error sending message " + e);
|
node.error("Error sending message: " + e, {});
|
||||||
msg.error = e;
|
msg.error = e;
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-send-message", MatrixSendImage);
|
RED.nodes.registerType("matrix-send-message", MatrixSendImage);
|
||||||
}
|
};
|
||||||
|
@ -36,7 +36,8 @@
|
|||||||
name: { value: null },
|
name: { value: null },
|
||||||
autoAcceptRoomInvites: { value: true },
|
autoAcceptRoomInvites: { value: true },
|
||||||
enableE2ee: { type: "checkbox", value: true },
|
enableE2ee: { type: "checkbox", value: true },
|
||||||
global: { type: "checkbox", value: true }
|
global: { type: "checkbox", value: true },
|
||||||
|
allowUnknownDevices: { type: "checkbox", value: false }
|
||||||
},
|
},
|
||||||
icon: "matrix.png",
|
icon: "matrix.png",
|
||||||
label: function() {
|
label: function() {
|
||||||
@ -130,6 +131,20 @@
|
|||||||
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
|
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-config-input-allowUnknownDevices"
|
||||||
|
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||||
|
/>
|
||||||
|
<label for="node-config-input-allowUnknownDevices" style="width: auto">
|
||||||
|
Allow unverified devices in rooms
|
||||||
|
</label>
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Allow sending messages to a room with unknown devices which have not been verified.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$("#matrix-login-btn").on("click", function() {
|
$("#matrix-login-btn").on("click", function() {
|
||||||
function prettyPrintJson(json) {
|
function prettyPrintJson(json) {
|
||||||
|
@ -1,18 +1,34 @@
|
|||||||
global.Olm = require('olm');
|
global.Olm = require('olm');
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const sdk = require("matrix-js-sdk");
|
let RelationType, TimelineWindow, sdk, LocalStorageCryptoStore, RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, MemoryStore;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const mod = await import("matrix-js-sdk");
|
||||||
|
RelationType = mod.RelationType;
|
||||||
|
TimelineWindow = mod.TimelineWindow;
|
||||||
|
// matrix-js-sdk doesn't export a default – the top-level export is the same object:
|
||||||
|
sdk = mod;
|
||||||
|
|
||||||
|
RoomEvent = mod.RoomEvent;
|
||||||
|
RoomMemberEvent = mod.RoomMemberEvent;
|
||||||
|
HttpApiEvent = mod.HttpApiEvent;
|
||||||
|
ClientEvent = mod.ClientEvent;
|
||||||
|
MemoryStore = mod.MemoryStore;
|
||||||
|
|
||||||
|
// For LocalStorageCryptoStore, specify the file extension for Node 20+:
|
||||||
|
const cmod = await import("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store.js");
|
||||||
|
LocalStorageCryptoStore = cmod.LocalStorageCryptoStore;
|
||||||
|
})();
|
||||||
|
|
||||||
const { resolve } = require('path');
|
const { resolve } = require('path');
|
||||||
const { LocalStorage } = require('node-localstorage');
|
const { LocalStorage } = require('node-localstorage');
|
||||||
const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
|
|
||||||
const {RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, MemoryStore} = require("matrix-js-sdk");
|
|
||||||
const request = require("request");
|
|
||||||
require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it
|
require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it
|
||||||
if (!globalThis.fetch) {
|
if (!globalThis.fetch) {
|
||||||
// polyfill fetch if we don't have it
|
// polyfill fetch if we don't have it
|
||||||
if (!globalThis.fetch) {
|
if (!globalThis.fetch) {
|
||||||
import('node-fetch').then(({ default: fetch, Headers, Request, Response }) => {
|
import('node-fetch').then(({ default: fetch, Headers, Request, Response }) => {
|
||||||
Object.assign(globalThis, { fetch, Headers, Request, Response })
|
Object.assign(globalThis, { fetch, Headers, Request, Response });
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,8 +40,13 @@ module.exports = function(RED) {
|
|||||||
typeof loggingSettings.console.level !== 'undefined' &&
|
typeof loggingSettings.console.level !== 'undefined' &&
|
||||||
['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0
|
['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0
|
||||||
) {
|
) {
|
||||||
const { logger } = require('matrix-js-sdk/lib/logger');
|
import('matrix-js-sdk/lib/logger.js')
|
||||||
|
.then(({ logger }) => {
|
||||||
logger.disableAll();
|
logger.disableAll();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading logger module:", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function MatrixFolderNameFromUserId(name) {
|
function MatrixFolderNameFromUserId(name) {
|
||||||
@ -44,6 +65,7 @@ module.exports = function(RED) {
|
|||||||
this.credentials = {};
|
this.credentials = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.users = {};
|
||||||
this.connected = null;
|
this.connected = null;
|
||||||
this.name = n.name;
|
this.name = n.name;
|
||||||
this.userId = this.credentials.userId;
|
this.userId = this.credentials.userId;
|
||||||
@ -52,9 +74,18 @@ module.exports = function(RED) {
|
|||||||
this.url = this.credentials.url;
|
this.url = this.credentials.url;
|
||||||
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
|
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
|
||||||
this.e2ee = n.enableE2ee || false;
|
this.e2ee = n.enableE2ee || false;
|
||||||
|
|
||||||
this.globalAccess = n.global;
|
this.globalAccess = n.global;
|
||||||
|
this.allowUnknownDevices = n.allowUnknownDevices || false;
|
||||||
this.initializedAt = new Date();
|
this.initializedAt = new Date();
|
||||||
|
node.initialSyncLimit = 25;
|
||||||
|
|
||||||
|
// Keep track of all consumers of this node to catch errors
|
||||||
|
node.register = function(consumerNode) {
|
||||||
|
node.users[consumerNode.id] = consumerNode;
|
||||||
|
};
|
||||||
|
node.deregister = function(consumerNode) {
|
||||||
|
delete node.users[consumerNode.id];
|
||||||
|
};
|
||||||
|
|
||||||
if(!this.userId) {
|
if(!this.userId) {
|
||||||
node.log("Matrix connection failed: missing user ID in configuration.");
|
node.log("Matrix connection failed: missing user ID in configuration.");
|
||||||
@ -68,9 +99,9 @@ module.exports = function(RED) {
|
|||||||
let retryStartTimeout = null;
|
let retryStartTimeout = null;
|
||||||
|
|
||||||
if(!this.credentials.accessToken) {
|
if(!this.credentials.accessToken) {
|
||||||
node.error("Matrix connection failed: missing access token in configuration.");
|
node.error("Matrix connection failed: missing access token in configuration.", {});
|
||||||
} else if(!this.url) {
|
} else if(!this.url) {
|
||||||
node.error("Matrix connection failed: missing server URL in configuration.");
|
node.error("Matrix connection failed: missing server URL in configuration.", {});
|
||||||
} else {
|
} else {
|
||||||
node.setConnected = async function(connected, cb) {
|
node.setConnected = async function(connected, cb) {
|
||||||
if (node.connected !== connected) {
|
if (node.connected !== connected) {
|
||||||
@ -88,7 +119,7 @@ module.exports = function(RED) {
|
|||||||
device_id = this.matrixClient.getDeviceId();
|
device_id = this.matrixClient.getDeviceId();
|
||||||
|
|
||||||
if(!device_id && node.enableE2ee) {
|
if(!device_id && node.enableE2ee) {
|
||||||
node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.")
|
node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.", {});
|
||||||
} else {
|
} else {
|
||||||
if(!stored_device_id || stored_device_id !== device_id) {
|
if(!stored_device_id || stored_device_id !== device_id) {
|
||||||
node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`);
|
node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`);
|
||||||
@ -105,15 +136,15 @@ module.exports = function(RED) {
|
|||||||
node.matrixClient.setDeviceDetails(device_id, {
|
node.matrixClient.setDeviceDetails(device_id, {
|
||||||
display_name: node.deviceLabel
|
display_name: node.deviceLabel
|
||||||
}).then(
|
}).then(
|
||||||
function(response) {},
|
function() {},
|
||||||
function(error) {
|
function(error) {
|
||||||
node.error("Failed to set device label: " + error);
|
node.error("Failed to set device label: " + error, {});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
node.error("Failed to fetch device: " + error);
|
node.error("Failed to fetch device: " + error, {});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -142,11 +173,11 @@ module.exports = function(RED) {
|
|||||||
localStorage: localStorage,
|
localStorage: localStorage,
|
||||||
}),
|
}),
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined,
|
deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined
|
||||||
request
|
|
||||||
// verificationMethods: ["m.sas.v1"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`);
|
||||||
|
|
||||||
// set globally if configured to do so
|
// set globally if configured to do so
|
||||||
if(this.globalAccess) {
|
if(this.globalAccess) {
|
||||||
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
|
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
|
||||||
@ -165,6 +196,13 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.on('close', function(done) {
|
node.on('close', function(done) {
|
||||||
stopClient();
|
stopClient();
|
||||||
|
if(node.globalAccess) {
|
||||||
|
try {
|
||||||
|
node.context().global.set('matrixClient["'+node.userId+'"]', undefined);
|
||||||
|
} catch(e) {
|
||||||
|
node.error(e.message, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,22 +212,22 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) {
|
node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) {
|
||||||
if (toStartOfTimeline) {
|
if (toStartOfTimeline) {
|
||||||
return; // ignore paginated results
|
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result");
|
||||||
}
|
return;
|
||||||
if (!event.getSender() || event.getSender() === node.userId) {
|
|
||||||
return; // ignore our own messages
|
|
||||||
}
|
}
|
||||||
if (!data || !data.liveEvent) {
|
if (!data || !data.liveEvent) {
|
||||||
return; // ignore old message (we only want live events)
|
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if(node.initializedAt > event.getDate()) {
|
if(node.initializedAt > event.getDate()) {
|
||||||
return; // skip events that occurred before our client initialized
|
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message before init");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await node.matrixClient.decryptEventIfNeeded(event);
|
await node.matrixClient.decryptEventIfNeeded(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
node.error(error);
|
node.error(error, {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,13 +253,23 @@ module.exports = function(RED) {
|
|||||||
type : (event.getContent()['msgtype'] || event.getType()) || null,
|
type : (event.getContent()['msgtype'] || event.getType()) || null,
|
||||||
payload : (event.getContent()['body'] || event.getContent()) || null,
|
payload : (event.getContent()['body'] || event.getContent()) || null,
|
||||||
isDM : isDmRoom(room),
|
isDM : isDmRoom(room),
|
||||||
|
isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread,
|
||||||
|
mentions : event.getContent()["m.mentions"] || null,
|
||||||
userId : event.getSender(),
|
userId : event.getSender(),
|
||||||
|
user : node.matrixClient.getUser(event.getSender()),
|
||||||
topic : event.getRoomId(),
|
topic : event.getRoomId(),
|
||||||
eventId : event.getId(),
|
eventId : event.getId(),
|
||||||
event : event
|
event : event,
|
||||||
};
|
};
|
||||||
|
|
||||||
node.log("Received" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getSender() + " :: " + msg.content.body + (toStartOfTimeline ? ' [PAGINATED]' : ''));
|
// remove keys from user property that start with an underscore
|
||||||
|
Object.keys(msg.user).forEach(function (key) {
|
||||||
|
if (/^_/.test(key)) {
|
||||||
|
delete msg.user[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.log(`Received ${msg.encrypted ? 'encrypted ' : ''}timeline event [${msg.type}]: (${room.name}) ${event.getSender()} :: ${msg.content.body} ${toStartOfTimeline ? ' [PAGINATED]' : ''}`);
|
||||||
node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
|
node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -260,7 +308,6 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
if (member.membership === "invite" && member.userId === node.userId) {
|
if (member.membership === "invite" && member.userId === node.userId) {
|
||||||
node.log("Got invite to join room " + member.roomId);
|
node.log("Got invite to join room " + member.roomId);
|
||||||
console.log(event);
|
|
||||||
if(node.autoAcceptRoomInvites) {
|
if(node.autoAcceptRoomInvites) {
|
||||||
node.matrixClient.joinRoom(member.roomId).then(function() {
|
node.matrixClient.joinRoom(member.roomId).then(function() {
|
||||||
node.log("Automatically accepted invitation to join room " + member.roomId);
|
node.log("Automatically accepted invitation to join room " + member.roomId);
|
||||||
@ -292,7 +339,7 @@ module.exports = function(RED) {
|
|||||||
} else if(prevState === null && state === "ERROR") {
|
} else if(prevState === null && state === "ERROR") {
|
||||||
// Occurs when the initial sync failed first time.
|
// Occurs when the initial sync failed first time.
|
||||||
node.setConnected(false, function(){
|
node.setConnected(false, function(){
|
||||||
node.error("Failed to connect to Matrix server");
|
node.error("Failed to connect to Matrix server", {});
|
||||||
});
|
});
|
||||||
} else if(prevState === "ERROR" && state === "PREPARED") {
|
} else if(prevState === "ERROR" && state === "PREPARED") {
|
||||||
// Occurs when the initial sync succeeds
|
// Occurs when the initial sync succeeds
|
||||||
@ -309,18 +356,18 @@ module.exports = function(RED) {
|
|||||||
} else if(prevState === "SYNCING" && state === "RECONNECTING") {
|
} else if(prevState === "SYNCING" && state === "RECONNECTING") {
|
||||||
// Occurs when the live update fails.
|
// Occurs when the live update fails.
|
||||||
node.setConnected(false, function(){
|
node.setConnected(false, function(){
|
||||||
node.error("Connection to Matrix server lost");
|
node.error("Connection to Matrix server lost", {});
|
||||||
});
|
});
|
||||||
} else if(prevState === "RECONNECTING" && state === "RECONNECTING") {
|
} else if(prevState === "RECONNECTING" && state === "RECONNECTING") {
|
||||||
// Can occur if the update calls continue to fail,
|
// Can occur if the update calls continue to fail,
|
||||||
// but the keepalive calls (to /versions) succeed.
|
// but the keepalive calls (to /versions) succeed.
|
||||||
node.setConnected(false, function(){
|
node.setConnected(false, function(){
|
||||||
node.error("Connection to Matrix server lost");
|
node.error("Connection to Matrix server lost", {});
|
||||||
});
|
});
|
||||||
} else if(prevState === "RECONNECTING" && state === "ERROR") {
|
} else if(prevState === "RECONNECTING" && state === "ERROR") {
|
||||||
// Occurs when the keepalive call also fails
|
// Occurs when the keepalive call also fails
|
||||||
node.setConnected(false, function(){
|
node.setConnected(false, function(){
|
||||||
node.error("Connection to Matrix server lost");
|
node.error("Connection to Matrix server lost", {});
|
||||||
});
|
});
|
||||||
} else if(prevState === "ERROR" && state === "SYNCING") {
|
} else if(prevState === "ERROR" && state === "SYNCING") {
|
||||||
// Occurs when the client has performed a
|
// Occurs when the client has performed a
|
||||||
@ -332,7 +379,7 @@ module.exports = function(RED) {
|
|||||||
// Occurs when the client has failed to
|
// Occurs when the client has failed to
|
||||||
// keepalive for a second time or more.
|
// keepalive for a second time or more.
|
||||||
node.setConnected(false, function(){
|
node.setConnected(false, function(){
|
||||||
node.error("Connection to Matrix server lost");
|
node.error("Connection to Matrix server lost", {});
|
||||||
});
|
});
|
||||||
} else if(prevState === "SYNCING" && state === "SYNCING") {
|
} else if(prevState === "SYNCING" && state === "SYNCING") {
|
||||||
// Occurs when the client has performed a live update.
|
// Occurs when the client has performed a live update.
|
||||||
@ -344,12 +391,11 @@ module.exports = function(RED) {
|
|||||||
// Occurs once the client has stopped syncing or
|
// Occurs once the client has stopped syncing or
|
||||||
// trying to sync after stopClient has been called.
|
// trying to sync after stopClient has been called.
|
||||||
node.setConnected(false, function(){
|
node.setConnected(false, function(){
|
||||||
node.error("Connection to Matrix server lost");
|
node.error("Connection to Matrix server lost", {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
node.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){
|
node.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){
|
||||||
// Example if user auth token incorrect:
|
// Example if user auth token incorrect:
|
||||||
// {
|
// {
|
||||||
@ -362,7 +408,7 @@ module.exports = function(RED) {
|
|||||||
// httpStatus: 401
|
// httpStatus: 401
|
||||||
// }
|
// }
|
||||||
|
|
||||||
node.error("Authentication failure: " + errorObj);
|
node.error("Authentication failure: " + errorObj, {});
|
||||||
stopClient();
|
stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -371,14 +417,15 @@ module.exports = function(RED) {
|
|||||||
if(node.e2ee){
|
if(node.e2ee){
|
||||||
node.log("Initializing crypto...");
|
node.log("Initializing crypto...");
|
||||||
await node.matrixClient.initCrypto();
|
await node.matrixClient.initCrypto();
|
||||||
node.matrixClient.setGlobalErrorOnUnknownDevices(false);
|
node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices
|
||||||
|
node.matrixClient.getCrypto().globalErrorOnUnknownDevices = !node.allowUnknownDevices;
|
||||||
}
|
}
|
||||||
node.log("Connecting to Matrix server...");
|
node.log("Connecting to Matrix server...");
|
||||||
await node.matrixClient.startClient({
|
await node.matrixClient.startClient({
|
||||||
initialSyncLimit: 8
|
initialSyncLimit: node.initialSyncLimit
|
||||||
});
|
});
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
node.error(error);
|
node.error(error, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,7 +446,7 @@ module.exports = function(RED) {
|
|||||||
.then(
|
.then(
|
||||||
function(data) {
|
function(data) {
|
||||||
if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) {
|
if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) {
|
||||||
node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched.");
|
node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched.", {});
|
||||||
}
|
}
|
||||||
if('device_id' in data && data['device_id'] && !node.deviceId) {
|
if('device_id' in data && data['device_id'] && !node.deviceId) {
|
||||||
// if we have no device_id configured lets use the one
|
// if we have no device_id configured lets use the one
|
||||||
@ -409,7 +456,7 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
// make sure our userId matches the access token's
|
// make sure our userId matches the access token's
|
||||||
if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) {
|
if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) {
|
||||||
node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`);
|
node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`, {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
run().catch((error) => node.error(error));
|
run().catch((error) => node.error(error));
|
||||||
@ -418,10 +465,10 @@ module.exports = function(RED) {
|
|||||||
// if the error isn't authentication related retry in a little bit
|
// if the error isn't authentication related retry in a little bit
|
||||||
if(err.code !== "M_UNKNOWN_TOKEN") {
|
if(err.code !== "M_UNKNOWN_TOKEN") {
|
||||||
retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000);
|
retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000);
|
||||||
node.error("Auth check failed: " + err);
|
node.error("Auth check failed: " + err, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -446,16 +493,25 @@ module.exports = function(RED) {
|
|||||||
deviceId = req.body.deviceId || undefined,
|
deviceId = req.body.deviceId || undefined,
|
||||||
displayName = req.body.displayName || undefined;
|
displayName = req.body.displayName || undefined;
|
||||||
|
|
||||||
const matrixClient = sdk.createClient({
|
(async () => {
|
||||||
|
const mod = await import("matrix-js-sdk");
|
||||||
|
const matrixClient = mod.createClient({
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
localTimeoutMs: '30000',
|
timelineSupport: true,
|
||||||
request
|
localTimeoutMs: '30000'
|
||||||
});
|
});
|
||||||
|
|
||||||
matrixClient.login(
|
new TimelineWindow(); // from our top-level variable, but to keep minimal changes,
|
||||||
'm.login.password', {
|
// you can just do: (await import("matrix-js-sdk")).TimelineWindow();
|
||||||
|
|
||||||
|
matrixClient.timelineSupport = true;
|
||||||
|
|
||||||
|
matrixClient.login('m.login.password', {
|
||||||
|
identifier: {
|
||||||
|
type: 'm.id.user',
|
||||||
user: userId,
|
user: userId,
|
||||||
|
},
|
||||||
password: password,
|
password: password,
|
||||||
initial_device_display_name: displayName
|
initial_device_display_name: displayName
|
||||||
})
|
})
|
||||||
@ -475,7 +531,11 @@ module.exports = function(RED) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
})().catch(err => {
|
||||||
|
res.json({ result: 'error', message: err.toString() });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function upgradeDirectoryIfNecessary(node, storageDir) {
|
function upgradeDirectoryIfNecessary(node, storageDir) {
|
||||||
let oldStorageDir = './matrix-local-storage',
|
let oldStorageDir = './matrix-local-storage',
|
||||||
@ -494,7 +554,7 @@ module.exports = function(RED) {
|
|||||||
fs.copySync(oldStorageDir, dir);
|
fs.copySync(oldStorageDir, dir);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
node.error(err);
|
node.error(err, {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -516,9 +576,6 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If a device ID is stored we will use that for the client
|
|
||||||
*/
|
|
||||||
function getStoredDeviceId(localStorage) {
|
function getStoredDeviceId(localStorage) {
|
||||||
let deviceId = localStorage.getItem('my_device_id');
|
let deviceId = localStorage.getItem('my_device_id');
|
||||||
if(deviceId === "null" || !deviceId) {
|
if(deviceId === "null" || !deviceId) {
|
||||||
@ -534,4 +591,4 @@ module.exports = function(RED) {
|
|||||||
localStorage.setItem('my_device_id', deviceId);
|
localStorage.setItem('my_device_id', deviceId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "Synapse Add/Edit User";
|
return this.name || "Synapse Add/Edit User";
|
||||||
|
@ -8,9 +8,10 @@ module.exports = function(RED) {
|
|||||||
this.server = RED.nodes.getNode(n.server);
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
this.encodeUri = function(pathTemplate, variables) {
|
this.encodeUri = function(pathTemplate, variables) {
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
@ -41,12 +42,13 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
if(!msg.userId) {
|
||||||
node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)");
|
node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +71,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser);
|
RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "Synapse Deactivate User";
|
return this.name || "Synapse Deactivate User";
|
||||||
|
@ -8,9 +8,10 @@ module.exports = function(RED) {
|
|||||||
this.server = RED.nodes.getNode(n.server);
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
this.encodeUri = function(pathTemplate, variables) {
|
this.encodeUri = function(pathTemplate, variables) {
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
@ -41,12 +42,13 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
if(!msg.userId) {
|
||||||
node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)");
|
node.error("msg.userId must be set to edit/create a user (ex: @user:server.com)", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +72,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-synapse-deactivate-user", MatrixSynapseDeactivateUser);
|
RED.nodes.registerType("matrix-synapse-deactivate-user", MatrixSynapseDeactivateUser);
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs: 2,
|
outputs: 2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
roomId: { value: null },
|
roomId: { value: null },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
|
@ -9,10 +9,9 @@ module.exports = function(RED) {
|
|||||||
this.roomId = n.roomId;
|
this.roomId = n.roomId;
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.encodeUri = function(pathTemplate, variables) {
|
this.encodeUri = function(pathTemplate, variables) {
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
if (!variables.hasOwnProperty(key)) {
|
if (!variables.hasOwnProperty(key)) {
|
||||||
@ -42,18 +41,19 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.topic = node.roomId || msg.topic;
|
msg.topic = node.roomId || msg.topic;
|
||||||
if(!msg.topic) {
|
if(!msg.topic) {
|
||||||
node.error("room must be defined in either msg.topic or in node config");
|
node.error("room must be defined in either msg.topic or in node config", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
if(!msg.userId) {
|
||||||
node.error("msg.userId is required to set user into a room");
|
node.error("msg.userId is required to set user into a room", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +77,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-synapse-join-room", MatrixJoinRoom);
|
RED.nodes.registerType("matrix-synapse-join-room", MatrixJoinRoom);
|
||||||
}
|
}
|
@ -12,12 +12,12 @@ module.exports = function(RED) {
|
|||||||
this.sharedSecret = this.credentials.sharedSecret;
|
this.sharedSecret = this.credentials.sharedSecret;
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server URL must be configured on the node.');
|
node.error('Server URL must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.sharedSecret) {
|
if(!this.sharedSecret) {
|
||||||
node.error('Shared registration secret must be configured on the node.');
|
node.error('Shared registration secret must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,12 +25,12 @@ module.exports = function(RED) {
|
|||||||
const { got } = await import('got');
|
const { got } = await import('got');
|
||||||
|
|
||||||
if(!msg.payload.username) {
|
if(!msg.payload.username) {
|
||||||
node.error("msg.payload.username is required");
|
node.error("msg.payload.username is required", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.payload.password) {
|
if(!msg.payload.password) {
|
||||||
node.error("msg.payload.password is required");
|
node.error("msg.payload.password is required", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
var nonce = response.body.nonce;
|
var nonce = response.body.nonce;
|
||||||
if(!nonce) {
|
if(!nonce) {
|
||||||
node.error('Could not get nonce from /_synapse/admin/v1/register');
|
node.error('Could not get nonce from /_synapse/admin/v1/register', msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +96,10 @@ module.exports = function(RED) {
|
|||||||
node.send(msg);
|
node.send(msg);
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-synapse-register", MatrixSynapseRegister, {
|
RED.nodes.registerType("matrix-synapse-register", MatrixSynapseRegister, {
|
||||||
credentials: {
|
credentials: {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" }
|
server: { type: "matrix-server-config" }
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "Synapse User List";
|
return this.name || "Synapse User List";
|
||||||
|
@ -12,6 +12,8 @@ module.exports = function(RED) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
node.server.on("disconnected", function(){
|
node.server.on("disconnected", function(){
|
||||||
@ -29,8 +31,9 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let queryParams = {
|
let queryParams = {
|
||||||
@ -62,6 +65,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-synapse-users", MatrixSynapseUsers);
|
RED.nodes.registerType("matrix-synapse-users", MatrixSynapseUsers);
|
||||||
}
|
}
|
114
src/matrix-typing.html
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('matrix-typing', {
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomType: { value: "msg" },
|
||||||
|
roomValue: { value: "topic" },
|
||||||
|
typingType: { value: "bool" },
|
||||||
|
typingValue: { value: "true" },
|
||||||
|
timeoutMsType: { value: "num" },
|
||||||
|
timeoutMsValue: { value: 20000 },
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Typing";
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
$("#node-input-room").typedInput({
|
||||||
|
type: this.roomType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.roomValue)
|
||||||
|
.typedInput('type', this.roomType);
|
||||||
|
|
||||||
|
$("#node-input-typing").typedInput({
|
||||||
|
types:['msg','flow','global','bool'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.typingValue)
|
||||||
|
.typedInput('type', this.typingType);
|
||||||
|
|
||||||
|
$("#node-input-timeoutMs").typedInput({
|
||||||
|
types:['msg','flow','global','num'],
|
||||||
|
})
|
||||||
|
.typedInput('value', this.timeoutMsValue)
|
||||||
|
.typedInput('type', this.timeoutMsType);
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.roomType = $("#node-input-room").typedInput('type');
|
||||||
|
this.roomValue = $("#node-input-room").typedInput('value');
|
||||||
|
this.typingType = $("#node-input-typing").typedInput('type');
|
||||||
|
this.typingValue = $("#node-input-typing").typedInput('value');
|
||||||
|
this.timeoutMsType = $("#node-input-timeoutMs").typedInput('type');
|
||||||
|
this.timeoutMsValue = $("#node-input-timeoutMs").typedInput('value');
|
||||||
|
},
|
||||||
|
paletteLabel: 'Typing'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-typing">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||||
|
<input type="text" id="node-input-room">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-typing"><i class="fa fa-commenting-o"></i> Is Typing</label>
|
||||||
|
<input type="text" id="node-input-typing">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-timeoutMs"><i class="fa fa-clock-o"></i> Timeout Milliseconds</label>
|
||||||
|
<input type="text" id="node-input-timeoutMs">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row form-tips">
|
||||||
|
Timeout Milliseconds is how many milliseconds the server should show the user typing for. Ignored if setting typing to false.
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-typing">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>
|
||||||
|
Sends typing event to a room
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>dynamic
|
||||||
|
<span class="property-type">any</span>
|
||||||
|
</dt>
|
||||||
|
<dd> The inputs are configurable on the node.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ol class="node-ports">
|
||||||
|
<li>Success
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dd>Returns from first output on success</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
<li>Error
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.error <span class="property-type">string</span></dt>
|
||||||
|
<dd>the error that occurred.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
</script>
|
88
src/matrix-typing.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixTyping(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
let node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomId = n.roomId;
|
||||||
|
this.roomType = n.roomType;
|
||||||
|
this.roomValue = n.roomValue;
|
||||||
|
this.typingType = n.typingType;
|
||||||
|
this.typingValue = n.typingValue;
|
||||||
|
this.timeoutMsType = n.timeoutMsType;
|
||||||
|
this.timeoutMsValue = n.timeoutMsValue;
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.error("No configuration node", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on('input', async function(msg) {
|
||||||
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
|
node.error("No matrix server selected", msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!node.server.isConnected()) {
|
||||||
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if ((type === 'flow') || (type === 'global')) {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if(type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if(type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let roomId = getToValue(msg, node.roomType, node.roomValue),
|
||||||
|
typing = getToValue(msg, node.typingType, node.typingValue),
|
||||||
|
timeoutMs = getToValue(msg, node.timeoutMsType, node.timeoutMsValue);
|
||||||
|
|
||||||
|
if(!roomId) {
|
||||||
|
node.error('No room provided in msg.topic', msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await node.server.matrixClient.sendTyping(roomId, typing, timeoutMs);
|
||||||
|
node.send([msg, null]);
|
||||||
|
} catch(e) {
|
||||||
|
node.error("Failed to send typing event " + msg.topic + ": " + e, msg);
|
||||||
|
msg.payload = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-typing", MatrixTyping);
|
||||||
|
}
|
173
src/matrix-upload-file.html
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('matrix-upload-file',{
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
inputs:1,
|
||||||
|
outputs:2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
inputType: { value: "msg" },
|
||||||
|
inputValue: { value: "payload" },
|
||||||
|
fileNameType: { value: "msg" },
|
||||||
|
fileNameValue: { value: "filename" },
|
||||||
|
contentType: { value: null },
|
||||||
|
generateThumbnails: { type: "checkbox", value: true },
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Upload File";
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
$("#node-input-input").typedInput({
|
||||||
|
type: this.inputType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
}).typedInput('value', this.inputValue);
|
||||||
|
|
||||||
|
$("#node-input-file-name").typedInput({
|
||||||
|
type: this.fileNameType,
|
||||||
|
types:['msg','flow','global','str'],
|
||||||
|
}).typedInput('value', this.fileNameValue);
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
this.inputType = $("#node-input-input").typedInput('type');
|
||||||
|
this.inputValue = $("#node-input-input").typedInput('value');
|
||||||
|
this.fileNameType = $("#node-input-file-name").typedInput('type');
|
||||||
|
this.fileNameValue = $("#node-input-file-name").typedInput('value');
|
||||||
|
},
|
||||||
|
paletteLabel: 'Upload File'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="matrix-upload-file">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-input"><i class="fa fa-file"></i> File Input</label>
|
||||||
|
<input type="text" id="node-input-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Must be a buffer or string. If it is a string it assumes it is a path to a file on the filesystem.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-file-name"><i class="fa fa-file"></i> File Name</label>
|
||||||
|
<input type="text" id="node-input-file-name">
|
||||||
|
</div>
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Name to give file on remote server. Required if file input is a buffer otherwise it uses the original file name.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-contentType"><i class="fa fa-user"></i> Content-Type</label>
|
||||||
|
<input type="text" id="node-input-contentType" placeholder="msg.contentType">
|
||||||
|
</div>
|
||||||
|
<div class="form-tips" style="margin-bottom: 12px;">
|
||||||
|
Must be a valid <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" target="_blank">MIME Type</a> (ex: application/pdf) or left undefined to auto detect
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-generateThumbnails"
|
||||||
|
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||||
|
/>
|
||||||
|
<label for="node-input-generateThumbnails" style="width: auto;max-width:50%;">
|
||||||
|
Generate m.video thumbnails using ffmpeg
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-tips">
|
||||||
|
ffmpeg & ffprobe must be installed for thumbnail generation, calculating duration of video or audio file, and getting width & height for videos. This functionality is disabled otherwise.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("#node-input-roomId").on("keyup", function() {
|
||||||
|
if($(this).val() && !$(this).val().startsWith("!")) {
|
||||||
|
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
|
||||||
|
} else {
|
||||||
|
$("#node-input-roomId-error").hide();
|
||||||
|
}
|
||||||
|
}).trigger('keyup');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-upload-file">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>Upload a file to the Matrix homeserver. Returns a payload that can then be chained to a Send Message node to post the file to a room, or you can use the mxc_url to update a user or room avatar. The node supports uploading files, auto-detecting their types, and generating optional thumbnails for images and videos. Encrypted uploads are also supported.</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.payload
|
||||||
|
<span class="property-type">string | Buffer</span>
|
||||||
|
</dt>
|
||||||
|
<dd> A path to the file (string) or a file buffer (Buffer) to upload. If a buffer is provided, you must specify the filename for accurate type detection.</dd>
|
||||||
|
|
||||||
|
<dt>msg.filename
|
||||||
|
<span class="property-type">string</span>
|
||||||
|
</dt>
|
||||||
|
<dd> The name of the file to upload. If not provided, the filename is auto-detected from the path, but this may lead to inaccuracies in file type detection.</dd>
|
||||||
|
|
||||||
|
<dt>msg.body
|
||||||
|
<span class="property-type">string | null</span>
|
||||||
|
</dt>
|
||||||
|
<dd> The display name for the file when rendered in the chat. If left empty, the default display name is "Attachment".</dd>
|
||||||
|
|
||||||
|
<dt class="optional">msg.contentType
|
||||||
|
<span class="property-type">string | null</span>
|
||||||
|
</dt>
|
||||||
|
<dd> Optional MIME Type of the file. If not provided, the type will be auto-detected based on the filename or buffer. If the detection fails, it will default to "text/plain".</dd>
|
||||||
|
|
||||||
|
<dt class="optional">msg.encrypted
|
||||||
|
<span class="property-type">boolean</span>
|
||||||
|
</dt>
|
||||||
|
<dd> Set to true if the file should be uploaded with encryption. The node handles encryption automatically if this is enabled.</dd>
|
||||||
|
|
||||||
|
<dt class="optional">msg.generateThumbnails
|
||||||
|
<span class="property-type">boolean</span>
|
||||||
|
</dt>
|
||||||
|
<dd> Set to true to generate thumbnails for image or video uploads. Thumbnails are generated automatically for supported types.</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>An object containing details of the uploaded file, including the URL (mxc URI), the MIME type, size, and any generated thumbnail information.</dd>
|
||||||
|
|
||||||
|
<dt>msg.eventId <span class="property-type">string</span></dt>
|
||||||
|
<dd>The eventId of the uploaded file message.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
<li>Error
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.error <span class="property-type">string</span></dt>
|
||||||
|
<dd>A description of the error that occurred during the upload.</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Additional Notes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>The node supports generating thumbnails for images and video files if configured. For videos, the thumbnail is generated from the first frame.</li>
|
||||||
|
<li>Encrypted uploads require no additional configuration beyond setting msg.encrypted to true. The node handles encryption and returns the necessary decryption information.</li>
|
||||||
|
<li>The file type is auto-detected based on the filename or the content buffer. You can override this by setting the msg.contentType manually.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>References</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">MIME Types</a> - description of <code>msg.contentType</code> format</li>
|
||||||
|
</ul>
|
||||||
|
</script>
|
534
src/matrix-upload-file.js
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
const crypto = require("isomorphic-webcrypto");
|
||||||
|
const ffmpeg = require('fluent-ffmpeg');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const getImageSize = require('image-size');
|
||||||
|
const tmp = require('tmp');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixUploadFile(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
let node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.inputType = n.inputType;
|
||||||
|
this.inputValue = n.inputValue;
|
||||||
|
this.fileNameType = n.fileNameType;
|
||||||
|
this.fileNameValue = n.fileNameValue;
|
||||||
|
this.contentType = n.contentType;
|
||||||
|
this.generateThumbnails = n.generateThumbnails;
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.warn("No configuration node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function detectFileType(filename, bufferOrPath, msg) {
|
||||||
|
const Mime = require('mime');
|
||||||
|
let file = Buffer.isBuffer(bufferOrPath) ? filename : bufferOrPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(file) {
|
||||||
|
let type = Mime.getType(file);
|
||||||
|
let ext = Mime.getExtension(file);
|
||||||
|
if(type) {
|
||||||
|
return {ext: ext, mime: type};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error detecting file type for ${filename}: ${error.message}`, msg);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileBuffer(data, msg) {
|
||||||
|
try {
|
||||||
|
if (Buffer.isBuffer(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && RED.settings.fileWorkingDirectory && !path.isAbsolute(data)) {
|
||||||
|
return fs.readFileSync(path.resolve(path.join(RED.settings.fileWorkingDirectory, data)));
|
||||||
|
}
|
||||||
|
return fs.readFileSync(data);
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error reading file buffer: ${error.message}`, msg);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, type, property) {
|
||||||
|
let value = property;
|
||||||
|
try {
|
||||||
|
if (type === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg, property);
|
||||||
|
} else if ((type === 'flow') || (type === 'global')) {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if(type === "bool") {
|
||||||
|
value = (property === 'true');
|
||||||
|
} else if(type === "num") {
|
||||||
|
value = Number(property);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error evaluating value for type ${type}: ${error.message}`);
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.on("input", onInput);
|
||||||
|
async function onInput(msg) {
|
||||||
|
try {
|
||||||
|
if (!node.server || !node.server.matrixClient) {
|
||||||
|
node.warn("No matrix server selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.server.isConnected()) {
|
||||||
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
|
msg.error = "Matrix server connection is currently closed";
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bufferOrPath = getToValue(msg, node.inputType, node.inputValue);
|
||||||
|
if (!bufferOrPath) {
|
||||||
|
node.error('Missing file path/buffer input', msg);
|
||||||
|
msg.error = 'Missing file path/buffer input';
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = getToValue(msg, node.fileNameType, node.fileNameValue);
|
||||||
|
if (!filename || typeof filename !== 'string') {
|
||||||
|
if (!Buffer.isBuffer(bufferOrPath)) {
|
||||||
|
filename = path.basename(bufferOrPath);
|
||||||
|
} else {
|
||||||
|
node.error('Missing filename, this is required if input is a file buffer', msg);
|
||||||
|
msg.error = 'Missing filename, this is required if input is a file buffer';
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.contentType = node.contentType || msg.contentType || null;
|
||||||
|
let detectedFileType = await detectFileType(filename, bufferOrPath, msg);
|
||||||
|
node.log("Detected file type " + JSON.stringify(detectedFileType) + " for " + (Buffer.isBuffer(bufferOrPath) ? 'buffer' : `file ${bufferOrPath}`), msg);
|
||||||
|
|
||||||
|
let contentType = msg.contentType || detectedFileType?.mime || null,
|
||||||
|
msgtype = msg.msgtype || null;
|
||||||
|
if (!contentType) {
|
||||||
|
node.warn("Content-type failed to detect, falling back to text/plain", msg);
|
||||||
|
contentType = 'text/plain';
|
||||||
|
}
|
||||||
|
if (!msgtype) {
|
||||||
|
msgtype = autoDetectMatrixMessageType(detectedFileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
let encryptedFile = null;
|
||||||
|
if (msg.encrypted) {
|
||||||
|
encryptedFile = await encryptAttachment(getFileBuffer(bufferOrPath, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
node.log("Uploading file ", msg);
|
||||||
|
let file;
|
||||||
|
try {
|
||||||
|
file = await node.server.matrixClient.uploadContent(
|
||||||
|
encryptedFile?.data || getFileBuffer(bufferOrPath, msg),
|
||||||
|
{
|
||||||
|
name: filename, // Name to give the file on the server.
|
||||||
|
rawResponse: false, // Return the raw body, rather than parsing the JSON.
|
||||||
|
type: contentType, // Content-type for the upload. Defaults to file.type, or applicaton/octet-stream.
|
||||||
|
onlyContentUri: false // Just return the content URI, rather than the whole body. Defaults to false. Ignored if opts.rawResponse is true.
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
node.error("Upload content error " + e, msg);
|
||||||
|
msg.error = e;
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we call this method when we need a file and cannot use the buffer
|
||||||
|
// so if we get passed a buffer we write it to a tmp file and return that
|
||||||
|
// otherwise we just return the string because it's already a file
|
||||||
|
let tempFile = null;
|
||||||
|
|
||||||
|
function getFile(bufferOrFile) {
|
||||||
|
try {
|
||||||
|
if (!Buffer.isBuffer(bufferOrFile)) {
|
||||||
|
return bufferOrFile; // already a file
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempFile) {
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write buffer to tmp file and return path
|
||||||
|
let tmpObj = tmp.fileSync({postfix: `.${detectedFileType.ext}`});
|
||||||
|
fs.writeFileSync(tmpObj.name, bufferOrFile);
|
||||||
|
tempFile = tmpObj.name;
|
||||||
|
return tmpObj.name;
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error creating temp file: ${error.message}`, msg);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTempFile() {
|
||||||
|
if (!tempFile) return null;
|
||||||
|
try {
|
||||||
|
fs.rmSync(tempFile);
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error deleting temp file: ${error.message}`, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get size of a buffer or file in bytes
|
||||||
|
function getFileSize(bufferOrPath) {
|
||||||
|
try {
|
||||||
|
if (Buffer.isBuffer(bufferOrPath)) {
|
||||||
|
return Buffer.byteLength(bufferOrPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.statSync(bufferOrPath).size;
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error getting file size: ${error.message}`, msg);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addThumbnail(buffer) {
|
||||||
|
try {
|
||||||
|
let imageSize = getImageSize(Buffer.isBuffer(buffer) ? buffer : buffer.data);
|
||||||
|
msg.payload.info.thumbnail_info = {
|
||||||
|
w: imageSize.width,
|
||||||
|
h: imageSize.height,
|
||||||
|
size: getFileSize(Buffer.isBuffer(buffer) ? buffer : buffer.data)
|
||||||
|
}
|
||||||
|
let uploadedThumbnail = await node.server.matrixClient.uploadContent(
|
||||||
|
Buffer.isBuffer(buffer) ? buffer : buffer.data,
|
||||||
|
{
|
||||||
|
name: "thumbnail.png", // Name to give the file on the server.
|
||||||
|
rawResponse: false, // Return the raw body, rather than parsing the JSON.
|
||||||
|
type: "image/png", // Content-type for the upload. Defaults to file.type, or applicaton/octet-stream.
|
||||||
|
onlyContentUri: false // Just return the content URI, rather than the whole body. Defaults to false. Ignored if opts.rawResponse is true.
|
||||||
|
});
|
||||||
|
// delete local file
|
||||||
|
if (msg.encrypted) {
|
||||||
|
msg.payload.info.thumbnail_file.url = uploadedThumbnail.content_uri;
|
||||||
|
} else {
|
||||||
|
msg.payload.info.thumbnail_url = uploadedThumbnail.content_uri;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error adding thumbnail: ${error.message}`, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createImageThumbnail(bufferOrPath) {
|
||||||
|
try {
|
||||||
|
if (Buffer.isBuffer(bufferOrPath)) {
|
||||||
|
return sharp(bufferOrPath).resize({width: 320}).toBuffer();
|
||||||
|
}
|
||||||
|
return sharp(fs.readFileSync(bufferOrPath)).resize({width: 320}).toBuffer();
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Error creating image thumbnail: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ffmpegVideoThumbnail(filepath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let filename = `${msg._msgid}-screenshot.png`;
|
||||||
|
ffmpeg(filepath)
|
||||||
|
.on('end', async function () {
|
||||||
|
let path = `/tmp/${filename}`;
|
||||||
|
let buffer = getFileBuffer(path, msg);
|
||||||
|
let encryptedThumbnail = null;
|
||||||
|
if (msg.encrypted) {
|
||||||
|
encryptedThumbnail = await encryptAttachment(buffer);
|
||||||
|
msg.payload.info.thumbnail_file = encryptedFile.info;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await addThumbnail(encryptedThumbnail || buffer);
|
||||||
|
fs.rmSync(path); // delete temporary thumbnail file
|
||||||
|
resolve();
|
||||||
|
} catch (e) {
|
||||||
|
return reject(new Error("Thumbnail upload failure: " + e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', function (err) {
|
||||||
|
return reject(err);
|
||||||
|
})
|
||||||
|
.screenshots({
|
||||||
|
timestamps: [0],
|
||||||
|
filename: filename,
|
||||||
|
folder: '/tmp',
|
||||||
|
size: '320x?'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.payload = {};
|
||||||
|
if (msg.encrypted) {
|
||||||
|
msg.payload.file = encryptedFile?.info || {};
|
||||||
|
msg.payload.file.url = file.content_uri;
|
||||||
|
} else {
|
||||||
|
msg.payload.url = file.content_uri;
|
||||||
|
}
|
||||||
|
msg.payload.msgtype = msgtype;
|
||||||
|
msg.payload.body = msg.body || msg.filename || "";
|
||||||
|
msg.payload.info = {
|
||||||
|
"mimetype": contentType,
|
||||||
|
"size": getFileSize(bufferOrPath),
|
||||||
|
};
|
||||||
|
if (msgtype === 'm.image') {
|
||||||
|
// detect size of image
|
||||||
|
try {
|
||||||
|
let imageSize = getImageSize(bufferOrPath);
|
||||||
|
msg.payload.info.h = imageSize.height;
|
||||||
|
msg.payload.info.w = imageSize.width;
|
||||||
|
|
||||||
|
// Generate thumbnail for image
|
||||||
|
if (node.generateThumbnails) {
|
||||||
|
let thumbnailBuffer = await createImageThumbnail(bufferOrPath);
|
||||||
|
let encryptedThumbnail = null;
|
||||||
|
if (msg.encrypted) {
|
||||||
|
encryptedThumbnail = await encryptAttachment(thumbnailBuffer);
|
||||||
|
msg.payload.info.thumbnail_file = encryptedThumbnail.info;
|
||||||
|
}
|
||||||
|
await addThumbnail(encryptedThumbnail || thumbnailBuffer);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
node.error("thumbnail error: " + e, msg);
|
||||||
|
}
|
||||||
|
} else if (msgtype === 'm.audio' && detectedFileType) {
|
||||||
|
try {
|
||||||
|
// detect duration of audio clip
|
||||||
|
let filepath = getFile(bufferOrPath);
|
||||||
|
let metadata = await _ffprobe(filepath);
|
||||||
|
let audioStream = metadata?.streams.filter(function (stream) {
|
||||||
|
return stream.codec_type === "audio" || false;
|
||||||
|
})[0];
|
||||||
|
if (audioStream?.duration) {
|
||||||
|
msg.payload.info.duration = audioStream?.duration * 1000;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
node.error(e, msg);
|
||||||
|
}
|
||||||
|
deleteTempFile();
|
||||||
|
} else if (msgtype === 'm.video' && detectedFileType) {
|
||||||
|
let filepath = getFile(bufferOrPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// detect duration & width/height of video clip
|
||||||
|
let metadata = await _ffprobe(filepath);
|
||||||
|
let videoStream = metadata?.streams.filter(function (stream) {
|
||||||
|
return stream.codec_type === "video" || false;
|
||||||
|
})[0];
|
||||||
|
if (videoStream) {
|
||||||
|
msg.payload.info.duration = videoStream.duration * 1000;
|
||||||
|
msg.payload.info.w = videoStream.width;
|
||||||
|
msg.payload.info.h = videoStream.height;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
node.error("ffprobe error: " + e, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.generateThumbnails) {
|
||||||
|
try {
|
||||||
|
await _ffmpegVideoThumbnail(filepath);
|
||||||
|
} catch (e) {
|
||||||
|
node.error("Screenshot generation error: " + e, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteTempFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
node.send(msg, null);
|
||||||
|
} catch (error) {
|
||||||
|
node.error(`Unhandled error: ${error.message}`, msg);
|
||||||
|
node.log(error, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-upload-file", MatrixUploadFile);
|
||||||
|
|
||||||
|
// the following was taken & modified from https://github.com/matrix-org/browser-encrypt-attachment/blob/master/index.js
|
||||||
|
/**
|
||||||
|
* Encrypt an attachment.
|
||||||
|
* @param {ArrayBuffer} plaintextBuffer The attachment data buffer.
|
||||||
|
* @return {Promise} A promise that resolves with an object when the attachment is encrypted.
|
||||||
|
* The object has a "data" key with an ArrayBuffer of encrypted data and an "info" key
|
||||||
|
* with an object containing the info needed to decrypt the data.
|
||||||
|
*/
|
||||||
|
function encryptAttachment(plaintextBuffer) {
|
||||||
|
let cryptoKey; // The AES key object.
|
||||||
|
let exportedKey; // The AES key exported as JWK.
|
||||||
|
let ciphertextBuffer; // ArrayBuffer of encrypted data.
|
||||||
|
let sha256Buffer; // ArrayBuffer of digest.
|
||||||
|
let ivArray; // Uint8Array of AES IV
|
||||||
|
// Generate an IV where the first 8 bytes are random and the high 8 bytes
|
||||||
|
// are zero. We set the counter low bits to 0 since it makes it unlikely
|
||||||
|
// that the 64 bit counter will overflow.
|
||||||
|
ivArray = new Uint8Array(16);
|
||||||
|
crypto.getRandomValues(ivArray.subarray(0,8));
|
||||||
|
// Load the encryption key.
|
||||||
|
return crypto.subtle.generateKey(
|
||||||
|
{"name": "AES-CTR", length: 256}, true, ["encrypt", "decrypt"]
|
||||||
|
).then(function(generateKeyResult) {
|
||||||
|
cryptoKey = generateKeyResult;
|
||||||
|
// Export the Key as JWK.
|
||||||
|
return crypto.subtle.exportKey("jwk", cryptoKey);
|
||||||
|
}).then(function(exportKeyResult) {
|
||||||
|
exportedKey = exportKeyResult;
|
||||||
|
// Encrypt the input ArrayBuffer.
|
||||||
|
// Use half of the iv as the counter by setting the "length" to 64.
|
||||||
|
return crypto.subtle.encrypt(
|
||||||
|
{name: "AES-CTR", counter: ivArray, length: 64}, cryptoKey, plaintextBuffer
|
||||||
|
);
|
||||||
|
}).then(function(encryptResult) {
|
||||||
|
ciphertextBuffer = encryptResult;
|
||||||
|
// SHA-256 the encrypted data.
|
||||||
|
return crypto.subtle.digest("SHA-256", ciphertextBuffer);
|
||||||
|
}).then(function (digestResult) {
|
||||||
|
sha256Buffer = digestResult;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: ciphertextBuffer,
|
||||||
|
info: {
|
||||||
|
v: "v2",
|
||||||
|
key: exportedKey,
|
||||||
|
iv: encodeBase64(ivArray),
|
||||||
|
hashes: {
|
||||||
|
sha256: encodeBase64(new Uint8Array(sha256Buffer)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an attachment.
|
||||||
|
* @param {ArrayBuffer} ciphertextBuffer The encrypted attachment data buffer.
|
||||||
|
* @param {Object} info The information needed to decrypt the attachment.
|
||||||
|
* @param {Object} info.key AES-CTR JWK key object.
|
||||||
|
* @param {string} info.iv Base64 encoded 16 byte AES-CTR IV.
|
||||||
|
* @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext.
|
||||||
|
* @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted.
|
||||||
|
*/
|
||||||
|
function decryptAttachment(ciphertextBuffer, info) {
|
||||||
|
|
||||||
|
if (info === undefined || info.key === undefined || info.iv === undefined
|
||||||
|
|| info.hashes === undefined || info.hashes.sha256 === undefined) {
|
||||||
|
throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cryptoKey; // The AES key object.
|
||||||
|
let ivArray = decodeBase64(info.iv);
|
||||||
|
let expectedSha256base64 = info.hashes.sha256;
|
||||||
|
// Load the AES from the "key" key of the info object.
|
||||||
|
return crypto.subtle.importKey(
|
||||||
|
"jwk", info.key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]
|
||||||
|
).then(function (importKeyResult) {
|
||||||
|
cryptoKey = importKeyResult;
|
||||||
|
// Check the sha256 hash
|
||||||
|
return crypto.subtle.digest("SHA-256", ciphertextBuffer);
|
||||||
|
}).then(function (digestResult) {
|
||||||
|
if (encodeBase64(new Uint8Array(digestResult)) !== expectedSha256base64) {
|
||||||
|
throw new Error("Mismatched SHA-256 digest (expected: " + encodeBase64(new Uint8Array(digestResult)) + ") got (" + expectedSha256base64 + ")");
|
||||||
|
}
|
||||||
|
let counterLength;
|
||||||
|
if (info.v.toLowerCase() === "v1" || info.v.toLowerCase() === "v2") {
|
||||||
|
// Version 1 and 2 use a 64 bit counter.
|
||||||
|
counterLength = 64;
|
||||||
|
} else {
|
||||||
|
// Version 0 uses a 128 bit counter.
|
||||||
|
counterLength = 128;
|
||||||
|
}
|
||||||
|
return crypto.subtle.decrypt(
|
||||||
|
{name: "AES-CTR", counter: ivArray, length: counterLength}, cryptoKey, ciphertextBuffer
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a typed array of uint8 as base64.
|
||||||
|
* @param {Uint8Array} uint8Array The data to encode.
|
||||||
|
* @return {string} The base64 without padding.
|
||||||
|
*/
|
||||||
|
function encodeBase64(uint8Array) {
|
||||||
|
// Misinterpt the Uint8Array as Latin-1.
|
||||||
|
// window.btoa expects a unicode string with codepoints in the range 0-255.
|
||||||
|
// var latin1String = String.fromCharCode.apply(null, uint8Array);
|
||||||
|
// Use the builtin base64 encoder.
|
||||||
|
var paddedBase64 = btoa(uint8Array);
|
||||||
|
// Calculate the unpadded length.
|
||||||
|
var inputLength = uint8Array.length;
|
||||||
|
var outputLength = 4 * Math.floor((inputLength + 2) / 3) + (inputLength + 2) % 3 - 2;
|
||||||
|
// Return the unpadded base64.
|
||||||
|
return paddedBase64.slice(0, outputLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a base64 string to a typed array of uint8.
|
||||||
|
* This will decode unpadded base64, but will also accept base64 with padding.
|
||||||
|
* @param {string} base64 The unpadded base64 to decode.
|
||||||
|
* @return {Uint8Array} The decoded data.
|
||||||
|
*/
|
||||||
|
function decodeBase64(base64) {
|
||||||
|
// Pad the base64 up to the next multiple of 4.
|
||||||
|
var paddedBase64 = base64 + "===".slice(0, (4 - base64.length % 4) % 4);
|
||||||
|
// Decode the base64 as a misinterpreted Latin-1 string.
|
||||||
|
// window.atob returns a unicode string with codepoints in the range 0-255.
|
||||||
|
var latin1String = atob(paddedBase64);
|
||||||
|
// Encode the string as a Uint8Array as Latin-1.
|
||||||
|
var uint8Array = new Uint8Array(latin1String.length);
|
||||||
|
for (var i = 0; i < latin1String.length; i++) {
|
||||||
|
uint8Array[i] = latin1String.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoDetectMatrixMessageType(fileType) {
|
||||||
|
switch(fileType ? fileType.mime.split('/')[0].toLowerCase() : undefined) {
|
||||||
|
case 'video': return 'm.video';
|
||||||
|
case 'image': return 'm.image';
|
||||||
|
case 'audio': return 'm.audio';
|
||||||
|
default: return 'm.file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ffprobe method for getting metadata from a file wrapped in a promise
|
||||||
|
function _ffprobe(filepath){
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
ffmpeg.ffprobe(filepath, function(err, metadata) {
|
||||||
|
if(err) {
|
||||||
|
return reject(new Error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(metadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
329
src/matrix-user-settings.html
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
<script type="text/html" data-template-name="matrix-user-settings">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||||
|
<input type="text" id="node-input-server">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
|
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row node-input-rule-container-row">
|
||||||
|
<ol id="node-input-rule-container"></ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("#node-input-roomId").on("keyup", function() {
|
||||||
|
if($(this).val() && !$(this).val().startsWith("!")) {
|
||||||
|
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
|
||||||
|
} else {
|
||||||
|
$("#node-input-roomId-error").hide();
|
||||||
|
}
|
||||||
|
}).trigger('keyup');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="matrix-user-settings">
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>
|
||||||
|
Set and get the current user's display name or avatar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt class="optional">dynamic
|
||||||
|
<span class="property-type">string|object</span>
|
||||||
|
</dt>
|
||||||
|
<dd> The properties used to set the avatar_url or display name are configured on the node.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<ol class="node-ports">
|
||||||
|
<li>Success
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg <span class="property-type">object</span></dt>
|
||||||
|
<dd>Original message object with modifications based on config.</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.setter_errors <span class="property-type">undefined|object</span></dt>
|
||||||
|
<dd>Returned if saving a setting failed.</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>msg.getter_errors <span class="property-type">undefined|object</span></dt>
|
||||||
|
<dd>Returned if there is an error thrown getting a user setting</dd>
|
||||||
|
</dl>
|
||||||
|
<dt class="optional">dynamic
|
||||||
|
<span class="property-type">string|object</span>
|
||||||
|
</dt>
|
||||||
|
<dd> You configure what user settings to output in the node configuration.</dd>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(){
|
||||||
|
var userSettingOptions = [
|
||||||
|
{ value: "display_name", label: "Display name" },
|
||||||
|
{ value: "avatar_url", label: "Avatar URL" },
|
||||||
|
];
|
||||||
|
var defaultRules = [{
|
||||||
|
t: "set",
|
||||||
|
p: userSettingOptions[0].value,
|
||||||
|
to: "payload",
|
||||||
|
tot: "msg"
|
||||||
|
}];
|
||||||
|
|
||||||
|
function isInvalidProperty(v,vt) {
|
||||||
|
if (/msg|flow|global/.test(vt)) {
|
||||||
|
if (!RED.utils.validatePropertyExpression(v)) {
|
||||||
|
return "Invalid property: " + v;
|
||||||
|
}
|
||||||
|
} else if (vt === "jsonata") {
|
||||||
|
try{ jsonata(v); } catch(e) {
|
||||||
|
return "Invalid expression: " + e.message;
|
||||||
|
}
|
||||||
|
} else if (vt === "json") {
|
||||||
|
try{ JSON.parse(v); } catch(e) {
|
||||||
|
return "Invalid JSON data: " + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType('matrix-user-settings',{
|
||||||
|
category: 'matrix',
|
||||||
|
color: '#00b7ca',
|
||||||
|
icon: "matrix.png",
|
||||||
|
outputLabels: ["success", "error"],
|
||||||
|
inputs:1,
|
||||||
|
outputs:2,
|
||||||
|
defaults: {
|
||||||
|
name: { value: null },
|
||||||
|
server: { type: "matrix-server-config" },
|
||||||
|
roomId: { value: null },
|
||||||
|
rules: {
|
||||||
|
value: defaultRules,
|
||||||
|
validate: function(rules, opt) {
|
||||||
|
let msg;
|
||||||
|
const errors = []
|
||||||
|
if (!rules || rules.length === 0) { return true }
|
||||||
|
for (let i=0;i<rules.length;i++) {
|
||||||
|
const opt = { label: "Rule"+' '+(i+1) }
|
||||||
|
const r = rules[i];
|
||||||
|
if (r.t === 'set' || r.t === 'get') {
|
||||||
|
if ((msg = isInvalidProperty(r.p,r.pt)) !== false) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
if ((msg = isInvalidProperty(r.to,r.tot)) !== false) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.length ? errors : true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
var set = "Set";
|
||||||
|
var to = "to the value";
|
||||||
|
var toValueLabel = "to the property";
|
||||||
|
var search = this._("change.action.search");
|
||||||
|
var replace = this._("change.action.replace");
|
||||||
|
var regex = this._("change.label.regex");
|
||||||
|
|
||||||
|
function createPropertyValue(row2_1, row2_2, type, defaultType) {
|
||||||
|
var propValInput = $('<input/>',{class:"node-input-rule-property-value",type:"text"})
|
||||||
|
.appendTo(row2_1)
|
||||||
|
.typedInput({
|
||||||
|
default: defaultType || (type === 'set' ? 'str' : 'msg'),
|
||||||
|
types: (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global'])
|
||||||
|
});
|
||||||
|
|
||||||
|
var lsLabel = $('<label style="padding-left: 130px;"></label>').appendTo(row2_2);
|
||||||
|
var localStorageEl = $('<input type="checkbox" class="node-input-rule-property-localStorage" style="width: auto; margin: 0 6px 0 0">').appendTo(lsLabel);
|
||||||
|
$('<span>').text("Fetch from local storage").appendTo(lsLabel);
|
||||||
|
|
||||||
|
propValInput.on("change", function(evt,type,val) {
|
||||||
|
row2_2.toggle(type === "msg" || type === "flow" || type === "global" || type === "env");
|
||||||
|
})
|
||||||
|
return [propValInput, localStorageEl];
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#node-input-rule-container').css('min-height','150px').css('min-width','450px').editableList({
|
||||||
|
addItem: function(container,i,opt) {
|
||||||
|
var rule = opt;
|
||||||
|
if (!rule.hasOwnProperty('t')) {
|
||||||
|
rule = {t:"set",p:userSettingOptions[0].value,to:"payload",tot:"msg"};
|
||||||
|
}
|
||||||
|
if (rule.t === "set" && !rule.tot) {
|
||||||
|
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
|
||||||
|
rule.to = rule.to.substring(4);
|
||||||
|
rule.tot = "msg";
|
||||||
|
} else {
|
||||||
|
rule.tot = "str";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.css({
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
});
|
||||||
|
let fragment = document.createDocumentFragment();
|
||||||
|
var row1 = $('<div/>',{style:"display:flex; align-items: center"}).appendTo(fragment);
|
||||||
|
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
|
||||||
|
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
|
||||||
|
var row4 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(fragment);
|
||||||
|
|
||||||
|
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width:110px; margin-right:10px;"}).appendTo(row1);
|
||||||
|
var selectOptions = [
|
||||||
|
{v:"set",l:"Set"},
|
||||||
|
{v:"get",l:"Get"}
|
||||||
|
];
|
||||||
|
for (var x=0; x<selectOptions.length; x++) {
|
||||||
|
selectField.append($("<option></option>").val(selectOptions[x].v).text(selectOptions[x].l));
|
||||||
|
}
|
||||||
|
|
||||||
|
var propertyName = $('<input/>',{class:"node-input-rule-property-name",type:"text"})
|
||||||
|
.appendTo(row1)
|
||||||
|
.typedInput({type:"property", types:[{
|
||||||
|
value: userSettingOptions[0]['value'],
|
||||||
|
options: userSettingOptions
|
||||||
|
}]})
|
||||||
|
.on('focus', function(evt){
|
||||||
|
// following is a fix so autocomplete will show list on focus
|
||||||
|
if(!evt.isTrigger) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
$(this).trigger("keyup.red-ui-autoComplete");
|
||||||
|
}
|
||||||
|
}).on("keyup", function(evt) {
|
||||||
|
// following allows autocomplete to display even when backspace/delete is used
|
||||||
|
if (evt.keyCode === 8 || evt.keyCode === 46) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
$(this).trigger("keyup.red-ui-autoComplete");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var row2_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row2);
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(toValueLabel)
|
||||||
|
.appendTo(row2_1);
|
||||||
|
|
||||||
|
var row2_2 = $('<div/>', {style:"margin-top: 4px;"}).appendTo(row2);
|
||||||
|
|
||||||
|
var row3_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row3);
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(search)
|
||||||
|
.appendTo(row3_1);
|
||||||
|
|
||||||
|
var row3_2 = $('<div/>',{style:"display:flex;margin-top:8px;align-items: baseline"}).appendTo(row3);
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(replace)
|
||||||
|
.appendTo(row3_2);
|
||||||
|
|
||||||
|
$('<div/>',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
|
||||||
|
.text(to)
|
||||||
|
.appendTo(row4);
|
||||||
|
|
||||||
|
let propertyValue = null;
|
||||||
|
let localStorageEl = null;
|
||||||
|
let fromValue = null;
|
||||||
|
let toValue = null;
|
||||||
|
|
||||||
|
selectField.on("change", function() {
|
||||||
|
var type = $(this).val();
|
||||||
|
if (propertyValue) {
|
||||||
|
propertyValue.typedInput('hide');
|
||||||
|
}
|
||||||
|
if (fromValue) {
|
||||||
|
fromValue.typedInput('hide');
|
||||||
|
}
|
||||||
|
if (toValue) {
|
||||||
|
toValue.typedInput('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!propertyValue) {
|
||||||
|
var parts = createPropertyValue(row2_1, row2_2, type);
|
||||||
|
propertyValue = parts[0];
|
||||||
|
localStorageEl = parts[1];
|
||||||
|
} else {
|
||||||
|
propertyValue.typedInput('types', (type === 'set' ? ['msg','flow','global','str','json','jsonata'] : ['msg', 'flow', 'global']));
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyValue.typedInput('show');
|
||||||
|
row2.show();
|
||||||
|
if(type === 'get') {
|
||||||
|
localStorageEl.parent().show();
|
||||||
|
} else {
|
||||||
|
localStorageEl.parent().hide();
|
||||||
|
}
|
||||||
|
row3.hide();
|
||||||
|
row4.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectField.val(rule.t);
|
||||||
|
propertyName.val(rule.p);
|
||||||
|
if (rule.t === "set" || rule.t === "get") {
|
||||||
|
var parts = createPropertyValue(row2_1, row2_2, rule.t, rule.tot);
|
||||||
|
propertyValue = parts[0];
|
||||||
|
localStorageEl = parts[1];
|
||||||
|
propertyValue.typedInput('value',rule.to);
|
||||||
|
localStorageEl.prop("checked", !!rule.ls);
|
||||||
|
if(rule.t === 'get') {
|
||||||
|
localStorageEl.parent().show();
|
||||||
|
} else {
|
||||||
|
localStorageEl.parent().hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectField.change();
|
||||||
|
container[0].appendChild(fragment);
|
||||||
|
},
|
||||||
|
removable: true,
|
||||||
|
sortable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i=0; i<this.rules.length; i++) {
|
||||||
|
var rule = this.rules[i];
|
||||||
|
$("#node-input-rule-container").editableList('addItem',rule);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
var rules = $("#node-input-rule-container").editableList('items');
|
||||||
|
var node = this;
|
||||||
|
node.rules= [];
|
||||||
|
rules.each(function(i) {
|
||||||
|
var rule = $(this);
|
||||||
|
var type = rule.find(".node-input-rule-type").val();
|
||||||
|
var r = {
|
||||||
|
t:type,
|
||||||
|
p:rule.find(".node-input-rule-property-name").val(),
|
||||||
|
to:rule.find(".node-input-rule-property-value").typedInput('value'),
|
||||||
|
tot:rule.find(".node-input-rule-property-value").typedInput('type')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (r.t === "get" && rule.find(".node-input-rule-property-localStorage").prop("checked")) {
|
||||||
|
r.ls = true;
|
||||||
|
}
|
||||||
|
node.rules.push(r);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
oneditresize: function(size) {
|
||||||
|
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
|
||||||
|
var height = size.height;
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
height -= $(rows[i]).outerHeight(true);
|
||||||
|
}
|
||||||
|
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
|
||||||
|
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
||||||
|
height += 16;
|
||||||
|
$("#node-input-rule-container").editableList('height',height);
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "User Settings";
|
||||||
|
},
|
||||||
|
paletteLabel: 'User Settings'
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
176
src/matrix-user-settings.js
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
function MatrixUserSettings(n) {
|
||||||
|
RED.nodes.createNode(this, n);
|
||||||
|
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
this.name = n.name;
|
||||||
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
this.roomId = n.roomId;
|
||||||
|
this.rules = n.rules;
|
||||||
|
|
||||||
|
if (!node.server) {
|
||||||
|
node.warn("No configuration node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
|
||||||
|
node.server.on("disconnected", function(){
|
||||||
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.server.on("connected", function() {
|
||||||
|
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("input", async function (msg) {
|
||||||
|
if (! node.server || ! node.server.matrixClient) {
|
||||||
|
msg.error = "No matrix server selected";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!node.server.isConnected()) {
|
||||||
|
msg.error = "Matrix server connection is currently closed";
|
||||||
|
node.error(msg.error, msg);
|
||||||
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let getterErrors = {},
|
||||||
|
setterErrors = {};
|
||||||
|
|
||||||
|
if(!Array.isArray(node.rules) || !node.rules.length) {
|
||||||
|
node.warn("No rules configured, skipping", msg);
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToValue(msg, rule) {
|
||||||
|
var value = rule.to;
|
||||||
|
if (rule.tot === 'json') {
|
||||||
|
try {
|
||||||
|
value = JSON.parse(rule.to);
|
||||||
|
} catch(e) {
|
||||||
|
throw new Error("Invalid JSON");
|
||||||
|
}
|
||||||
|
} else if (rule.tot === 'bin') {
|
||||||
|
try {
|
||||||
|
value = Buffer.from(JSON.parse(rule.to))
|
||||||
|
} catch(e) {
|
||||||
|
throw new Error("Invalid Binary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rule.tot === "msg") {
|
||||||
|
value = RED.util.getMessageProperty(msg,rule.to);
|
||||||
|
} else if ((rule.tot === 'flow') || (rule.tot === 'global')) {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg);
|
||||||
|
} catch(e2) {
|
||||||
|
throw new Error("Invalid value evaluation");
|
||||||
|
}
|
||||||
|
} else if (rule.tot === 'date') {
|
||||||
|
value = Date.now();
|
||||||
|
} else if (rule.tot === 'jsonata') {
|
||||||
|
try {
|
||||||
|
value = RED.util.evaluateJSONataExpression(rule.to,msg);
|
||||||
|
} catch(e3) {
|
||||||
|
throw new Error("Invalid expression");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToValue(value, rule) {
|
||||||
|
if(rule.tot === 'global' || rule.tot === 'flow') {
|
||||||
|
var contextKey = RED.util.parseContextStore(rule.to);
|
||||||
|
if (/\[msg/.test(contextKey.key)) {
|
||||||
|
// The key has a nest msg. reference to evaluate first
|
||||||
|
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true)
|
||||||
|
}
|
||||||
|
var target = node.context()[rule.tot];
|
||||||
|
var callback = err => {
|
||||||
|
if (err) {
|
||||||
|
node.error(err, msg);
|
||||||
|
getterErrors[rule.p] = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.set(contextKey.key, value, contextKey.store, callback);
|
||||||
|
} else if(rule.tot === 'msg') {
|
||||||
|
if (!RED.util.setMessageProperty(msg, rule.to, value)) {
|
||||||
|
node.warn(RED._("change.errors.no-override",{property:rule.to}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let rule of node.rules) {
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "t": "set",
|
||||||
|
// "p": "m.room.topic",
|
||||||
|
// "to": "asdf",
|
||||||
|
// "tot": "str"
|
||||||
|
// }, ...
|
||||||
|
// ]
|
||||||
|
|
||||||
|
let cachedGetters = {};
|
||||||
|
if(rule.t === 'set') {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
value = getToValue(msg, rule);
|
||||||
|
switch(rule.p) {
|
||||||
|
case "display_name":
|
||||||
|
await node.server.matrixClient.setDisplayName(value);
|
||||||
|
break;
|
||||||
|
case "avatar_url":
|
||||||
|
await node.server.matrixClient.setAvatarUrl(typeof value === "string" ? value : "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
setterErrors[rule.p] = e.message;
|
||||||
|
}
|
||||||
|
} else if(rule.t === 'get') {
|
||||||
|
let value;
|
||||||
|
if(cachedGetters.hasOwnProperty(rule.p)) {
|
||||||
|
value = cachedGetters[rule.p];
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// normalize some simpler events for easier access
|
||||||
|
switch(rule.p) {
|
||||||
|
case "display_name":
|
||||||
|
value = (await node.server.matrixClient.getProfileInfo(node.server.matrixClient.getUserId(), 'displayname')).displayname || false;
|
||||||
|
break;
|
||||||
|
case "avatar_url":
|
||||||
|
value = (await node.server.matrixClient.getProfileInfo(node.server.matrixClient.getUserId(), 'avatar_url')).avatar_url || false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setToValue(value, rule);
|
||||||
|
cachedGetters[rule.p] = value;
|
||||||
|
} catch(e) {
|
||||||
|
getterErrors[rule.p] = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Object.keys(setterErrors).length) {
|
||||||
|
msg.setter_errors = setterErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Object.keys(getterErrors).length) {
|
||||||
|
msg.getter_errors = getterErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.send([msg, null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("matrix-user-settings", MatrixUserSettings);
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
outputs:2,
|
outputs:2,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: null },
|
name: { value: null },
|
||||||
server: { value: "", type: "matrix-server-config" },
|
server: { type: "matrix-server-config" },
|
||||||
},
|
},
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name || "WhoIs User";
|
return this.name || "WhoIs User";
|
||||||
|
@ -8,10 +8,12 @@ module.exports = function(RED) {
|
|||||||
this.server = RED.nodes.getNode(n.server);
|
this.server = RED.nodes.getNode(n.server);
|
||||||
|
|
||||||
if(!this.server) {
|
if(!this.server) {
|
||||||
node.error('Server must be configured on the node.');
|
node.error('Server must be configured on the node.', {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node.server.register(node);
|
||||||
|
|
||||||
this.encodeUri = function(pathTemplate, variables) {
|
this.encodeUri = function(pathTemplate, variables) {
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
if (!variables.hasOwnProperty(key)) {
|
if (!variables.hasOwnProperty(key)) {
|
||||||
@ -41,12 +43,13 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!node.server.isConnected()) {
|
if(!node.server.isConnected()) {
|
||||||
node.error("Matrix server connection is currently closed");
|
node.error("Matrix server connection is currently closed", msg);
|
||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!msg.userId) {
|
if(!msg.userId) {
|
||||||
node.error("msg.userId must be set to get user whois data");
|
node.error("msg.userId must be set to get user whois data", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +73,10 @@ module.exports = function(RED) {
|
|||||||
node.send([null, msg]);
|
node.send([null, msg]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.on("close", function() {
|
||||||
|
node.server.deregister(node);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
RED.nodes.registerType("matrix-whois-user", MatrixWhoIsUser);
|
RED.nodes.registerType("matrix-whois-user", MatrixWhoIsUser);
|
||||||
}
|
}
|