diff --git a/README.md b/README.md index 2bddaa6..2099c5e 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,72 @@ # node-red-contrib-matrix-chat [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) ### Features -The following is supported from this package: +Supported functionality in this package includes: -- End-to-end encryption - - [Currently a WIP](#end-to-end-encryption-notes) - - Can also use [pantalaimon](https://github.com/matrix-org/pantalaimon) as an alternative solution to E2EE (if you need multiple sessions synced up with keys) -- Receive events from a room (messages, reactions, images, audio, locations, and files) whether encrypted or not -- Send Images/Files (sending files to e2ee room doesn't currently encrypt them yet) -- Edit messages -- Send typing events (Bot is typing ...) -- Delete events (messages, reactions, etc) -- Decrypt files in e2ee rooms -- Send HTML/Plain Text Message/Notice -- React to messages -- Register user's on closed registration Synapse servers using `registration_shared_secret` (Admin Only) -- List out users on a Synapse server (Admin Only) -- Get WhoIs info for a Synapse user (Admin Only) -- Add/Edit Synapse users using the v2 API (requires a pre-existing admin account) -- Deactivate users on Synapse servers (Admin Only) -- Get a user list from a room -- Kick user from room -- Ban user from room -- Join, Create, Invite, and Leave rooms -- Synapse admin API to force add user to room (requires bot to be in same room already) +- **End-to-end encryption (E2EE)** + - [Work in progress](#end-to-end-encryption-notes) + - Alternative: Use [Pantalaimon](https://github.com/matrix-org/pantalaimon) for E2EE key synchronization across sessions +- **Receive events** from rooms: Messages, reactions, images, audio, locations, files, encrypted or unencrypted +- **Fetch/modify room state**: Update room settings +- **Paginate room history** +- **Send files** (encryption support for files coming soon) +- **Send/edit messages** (supports plain text and HTML formats) +- **Send typing notifications** +- **Delete events** (messages, reactions, etc.) +- **Decrypt files** in E2EE rooms +- **React to messages** +- **Admin tools**: + - Register users on closed Synapse servers (`registration_shared_secret`) + - Manage users, including listing, adding, editing, deactivating (Synapse API) + - Force-add users to rooms +- **Room management**: Invite, kick, ban, join, create, and leave rooms - -Therefore, you can easily build a bot, chat relay, or administrate your Matrix server from within [Node-RED](https://nodered.org/). +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/). ### 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 npm install node-red-contrib-matrix-chat ``` ### Usage -We have examples! [Check them out](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) -#### Extra functionality -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. +Explore our [examples](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) to see the module in action. -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 -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 -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 +### Registering a User +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. +[See how to register a user here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme). ### 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 -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!** \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 9385a2e..8521c45 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,267 +1,466 @@ # 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! +## 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 -- [Create User with Shared Secret Registration](#create-user-with-shared-secret-registration) -- [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 " by joining mentioned room](#respond-to-joinroom-room_id_or_alias-by-joining-mentioned-room) -- [Respond to "rooms " with user's rooms (list server's rooms if is left blank)](#respond-to-rooms-user_id-with-users-rooms-list-servers-rooms-if-user_id-is-left-blank) -- [Respond to "whois " 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) -- [Sending typing events to a room](#sending-typing-events-to-a-room) -- [Download & store all received files/images](#download--store-all-received-filesimages) -- [Kick/Ban user from room](#kickban-user-from-room) -- [Deactivate user](#deactivate-user) +**Click the ▶ example to ▼ expand** +### User Management -### Create user with Shared Secret Registration +
+Get or set current user display name + +[View JSON](get-set-displayname.json) + +This flow lets you get or set the displayname for the current user. + +![get-set-displayname.png](get-set-displayname.png) + +
+ +
+Set user avatar using URL + +[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. + +![set-avatar-from-url.png](set-avatar-from-url.png) + +
+ +
+Fetch user info by userId + +[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. + +![get-user.png](get-user.png) + +
+ +
+Create User with Shared Secret Registration [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. -![shared-secret-registration.png](shared-secret-registration.png) +![Shared Secret Registration](shared-secret-registration.png) +
-### Create/Edit Synapse User +
+Create/Edit Synapse User [View JSON](add-user-with-admin-user.json) Allows an administrator to create or modify a user account with a specified `msg.userId`. -![add-user-with-admin-user.png](add-user-with-admin-user.png) +![Create/Edit Synapse User](add-user-with-admin-user.png) +
-### Use function node to run any command - -[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). - -![custom-redact-function-node.png](custom-redact-function-node.png) - - - -### 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-ping-pong.png](respond-ping-pong.png) - - - -### 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-html-with-html.png](respond-to-html-with-html.png) - - - -### 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-image-with-image.png](respond-image-with-image.png) - - - -### 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-file-with-file.png](respond-file-with-file.png) - - - -### Respond to "react" with a reaction - -[View JSON](respond-react-with-reaction.json) - -Give a 👍 reaction when someone says "react" - -![respond-react-with-reaction.png](respond-react-with-reaction.png) - - - -### Accept room invites from specific user - -[View JSON](accept-room-invites.json) - -Accept room invites from specific user. - -![accept-room-invites.png](accept-room-invites.png) - - - -### Leave room when someone says bye - -[View JSON](leave-room-bye.json) - -Leave room when someone says "bye". - -![leave-room-bye.png](leave-room-bye.png) - - - -### Remove messages containing "delete" - -[View JSON](delete-event.json) - -Any messages containing "delete" will try to be removed by the client. - -![respond-react-with-reaction.png](delete-event.png) - - - -### 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-users-list.png](respond-users-list.png) - - -### Respond to "newroom" by creating new room and inviting user - -[View JSON](respond-users-list.json) - -When someone sends "newroom" a new room will be created and the user that said the message will be invited. The client will also send a welcome message into the new room. - -![respond-newroom-invite.png](respond-newroom-invite.png) - - -### Respond to "joinroom " by joining mentioned room - -[View JSON](respond-joinroom.json) - -When someone sends "newroom" a new room will be created and the user that said the message will be invited. The client will also send a welcome message into the new room. - -![respond-joinroom.png](respond-joinroom.png) - -### Respond to "rooms " with user's rooms (list server's rooms if is left blank) - -[View JSON](respond-rooms.json) - -Responds to "rooms " with that user's rooms. If the message is just "rooms" it responds with a list of all rooms the server is participating in. - -Note: If there are a lot of rooms this may fail to send the message as it is too large. This also only works for user's that are on the current server. - -This requires admin privileges. - -![respond-rooms.png](respond-rooms.png) - - -### Respond to "whois " with information about the user's session - -[View JSON](respond-whois.json) - -This lists out the user's session info. Each session contains the IP address, when it was last seen, and the user agent. Useful to find out more about a specific user on your server. - -Note: If there are a lot of sessions this may fail to send the message as it is too large. This also only works for user's that are on the current server. - -This requires admin privileges. - -![respond-whois.png](respond-whois.png) - - -### Respond to "room_users" with current room's users - -[View JSON](respond-room-users.json) - -List out the users participating in a room. - -Note: If there are a lot of users in the room this will fail to send due to a large message error. - -![respond-room-users.png](respond-room-users.png) - - -### Download & store all received files/images - -[View JSON](store-received-files.json) - -Download received files/images. If the file is encrypted it will decrypt it for you. The decrypt node downloads the file for you otherwise you need to use a HTTP Request node to download the file. - -Note: You may need to edit the storage directory for this to work. Default action is to create a `downloads` folder in the Node-RED directory and places files in that but there is a good chance your Node-RED instance doesn't have access to write to this directory. - -![store-received-files.png](store-received-files.png) - - -### Sending typing events to a room - -[View JSON](send-typing-events.json) - -You can tell a room that Node-RED is writing a message and also cancel the writing event. This can be useful for making bots feel more interactive (show typing while requesting API endpoint for example). - -![store-received-files.png](send-typing-events.png) - - -### Kick/Ban user from room - -[View JSON](room-kick-ban.json) - -If you say "kick @test:example.com" the bot will kick @test:example.com from the current room. - -If you say "ban @test:example.com" the bot will ban @test:example.com from the current room. - -Note: This requires the bot to have permissions to kick/ban in the current room. - -![room-kick-ban.png](room-kick-ban.png) - - -### Deactivate user +
+Deactivate User [View JSON](deactivate-user.json) -If you say "deactivate_user @test:example.com" the bot will deactivate the @test:example.com account on the server. +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. -![room-kick-ban.png](deactivate-user.png) +![Deactivate User](deactivate-user.png) -### Force user to join room +
+ +
+Force User to Join Room [View JSON](force-join-room.json) -If you say "force_join @test:example.com !320j90mf0394f:example.com" the bot will force the user `@test:example.com` into room `!320j90mf0394f:example.com` +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:** -![room-kick-ban.png](force-join-room.png) \ No newline at end of file +- This requires the bot to be a server admin. +- This only works for rooms on the same server. + +![Force User to Join Room](force-join-room.png) + +
+ +### Message Handling + +
+Upload file and send to room + +[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) + +![img.png](send-image-to-room.png) + +
+ +
+Respond to "ping" with "pong" + +[View JSON](respond-ping-pong.json) + +Use this flow to respond to anyone who says "ping" with "pong" in the same room. + +![Respond to "ping" with "pong"](respond-ping-pong.png) + +
+ +
+Respond to "html" with an HTML Message + +[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. + +![Respond to "html" with HTML Message](respond-to-html-with-html.png) + +
+ +
+Respond to "react" with a Reaction + +[View JSON](respond-react-with-reaction.json) + +Gives a 👍 reaction when someone says "react". + +![Respond to "react" with Reaction](respond-react-with-reaction.png) + +
+ +
+Remove Messages Containing "delete" + +[View JSON](delete-event.json) + +Any messages containing "delete" will be removed by the client. + +**Note:** The bot needs appropriate permissions to remove messages. + +![Remove Messages Containing "delete"](delete-event.png) + +
+ +### Event Handling + +
+Sending Typing Events to a Room + +[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). + +![Sending Typing Events](send-typing-events.png) + +
+ +
+Mark all received events as read + +[View JSON](mark-all-read.json) + +With this flow anytime an event is received by the bot it will mark it as read. + +![mark-all-read.png](mark-all-read.png) + +
+ +
+Fetch event by eventId and roomId + +[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 + +![get-event.png](get-event.png) + +
+ +
+Paginate the entire history of a given room + +[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. + +![paginate-room-history.png](paginate-room-history.png) + +
+ +
+Paginate related events to a given eventId + +[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 + +![fetch-event-relations.png](fetch-event-relations.png) + +
+ +### Room Management + +
+Set room name and topic + +[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. + +![set-room-name-and-topic.png](set-room-name-and-topic.png) + +
+ +
+Accept Room Invites from Specific User + +[View JSON](accept-room-invites.json) + +Automatically accept room invites from a specific user. + +![Accept Room Invites from Specific User](accept-room-invites.png) + +
+ +
+Leave Room When Someone Says "bye" + +[View JSON](leave-room-bye.json) + +Leaves the room when someone says "bye". + +![Leave Room When Someone Says "bye"](leave-room-bye.png) + +
+ +
+Respond to "newroom" by Creating a New Room and Inviting User + +[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. + +![Respond to "newroom" by Creating New Room](respond-newroom-invite.png) + +
+ +
+Respond to "joinroom <room_id_or_alias>" by Joining Mentioned Room + +[View JSON](respond-joinroom.json) + +When someone sends "joinroom <room_id_or_alias>", the bot will join the mentioned room. + +![Respond to "joinroom" by Joining Room](respond-joinroom.png) + +
+ +
+Kick/Ban User from Room + +[View JSON](room-kick-ban.json) + +- If you say "kick @test:example.com", the bot will kick `@test:example.com` from the current room. +- If you say "ban @test:example.com", the bot will ban `@test:example.com` from the current room. + +**Note:** This requires the bot to have permissions to kick/ban in the current room. + +![Kick/Ban User from Room](room-kick-ban.png) + +
+ +### User Information + +
+Respond to "users" with Full List of Server Users + +[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. + +![Respond to "users" with User List](respond-users-list.png) + +
+ +
+Respond to "whois <user_id>" with Information about the User's Session + +[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. + +![Respond to "whois" with User Session Info](respond-whois.png) + +
+ +
+Respond to "rooms <user_id>" with User's Rooms + +[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. + +![Respond to "rooms" with Room List](respond-rooms.png) + +
+ +
+Respond to "room_users" with Current Room's Users + +[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. + +![Respond to "room_users" with User List](respond-room-users.png) + +
+ +### Advanced Features + +
+Use Function Node to Run Any Command + +[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). + +![Use Function Node to Run Commands](custom-redact-function-node.png) + +
+ +
+Download & Store All Received Files/Images + +[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. + +![Download & Store Received Files](store-received-files.png) + +
+ +### Deprecated + +
+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 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. + +![Respond to "image" with Uploaded Image](respond-image-with-image.png) + +
+ +
+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 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. + +![Respond to "file" with Uploaded File](respond-file-with-file.png) + +
diff --git a/examples/fetch-event-relations-aggregated.json b/examples/fetch-event-relations-aggregated.json new file mode 100644 index 0000000..ab8099f --- /dev/null +++ b/examples/fetch-event-relations-aggregated.json @@ -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" + ] + ] + } +] \ No newline at end of file diff --git a/examples/fetch-event-relations.png b/examples/fetch-event-relations.png new file mode 100644 index 0000000..84458f0 Binary files /dev/null and b/examples/fetch-event-relations.png differ diff --git a/examples/get-event.json b/examples/get-event.json new file mode 100644 index 0000000..a8ba597 --- /dev/null +++ b/examples/get-event.json @@ -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" + ] + ] + } +] \ No newline at end of file diff --git a/examples/get-event.png b/examples/get-event.png new file mode 100644 index 0000000..7940580 Binary files /dev/null and b/examples/get-event.png differ diff --git a/examples/get-set-displayname.json b/examples/get-set-displayname.json new file mode 100644 index 0000000..2550e1d --- /dev/null +++ b/examples/get-set-displayname.json @@ -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": [] + } +] \ No newline at end of file diff --git a/examples/get-set-displayname.png b/examples/get-set-displayname.png new file mode 100644 index 0000000..fb1c864 Binary files /dev/null and b/examples/get-set-displayname.png differ diff --git a/examples/get-user.json b/examples/get-user.json new file mode 100644 index 0000000..8734ed9 --- /dev/null +++ b/examples/get-user.json @@ -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": [] + } +] \ No newline at end of file diff --git a/examples/get-user.png b/examples/get-user.png new file mode 100644 index 0000000..4d29dd6 Binary files /dev/null and b/examples/get-user.png differ diff --git a/examples/mark-all-read.json b/examples/mark-all-read.json new file mode 100644 index 0000000..a429dfb --- /dev/null +++ b/examples/mark-all-read.json @@ -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": [] + } +] \ No newline at end of file diff --git a/examples/mark-all-read.png b/examples/mark-all-read.png new file mode 100644 index 0000000..fccbc79 Binary files /dev/null and b/examples/mark-all-read.png differ diff --git a/examples/paginate-room-history.json b/examples/paginate-room-history.json new file mode 100644 index 0000000..28a1b76 --- /dev/null +++ b/examples/paginate-room-history.json @@ -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" + ] + ] + } +] \ No newline at end of file diff --git a/examples/paginate-room-history.png b/examples/paginate-room-history.png new file mode 100644 index 0000000..04a7f82 Binary files /dev/null and b/examples/paginate-room-history.png differ diff --git a/examples/send-image-to-room.json b/examples/send-image-to-room.json new file mode 100644 index 0000000..24a4f0b --- /dev/null +++ b/examples/send-image-to-room.json @@ -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": [] + } +] \ No newline at end of file diff --git a/examples/send-image-to-room.png b/examples/send-image-to-room.png new file mode 100644 index 0000000..dbc726a Binary files /dev/null and b/examples/send-image-to-room.png differ diff --git a/examples/set-avatar-from-url.json b/examples/set-avatar-from-url.json new file mode 100644 index 0000000..aaaac84 --- /dev/null +++ b/examples/set-avatar-from-url.json @@ -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" + ], + [] + ] + } +] \ No newline at end of file diff --git a/examples/set-avatar-from-url.png b/examples/set-avatar-from-url.png new file mode 100644 index 0000000..33a5154 Binary files /dev/null and b/examples/set-avatar-from-url.png differ diff --git a/examples/set-room-name-and-topic.json b/examples/set-room-name-and-topic.json new file mode 100644 index 0000000..43cb643 --- /dev/null +++ b/examples/set-room-name-and-topic.json @@ -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": [] + } +] \ No newline at end of file diff --git a/examples/set-room-name-and-topic.png b/examples/set-room-name-and-topic.png new file mode 100644 index 0000000..494acb5 Binary files /dev/null and b/examples/set-room-name-and-topic.png differ diff --git a/examples/view-event-relations.json b/examples/view-event-relations.json new file mode 100644 index 0000000..b344ea6 --- /dev/null +++ b/examples/view-event-relations.json @@ -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": [] + } +] \ No newline at end of file diff --git a/package.json b/package.json index 54afa7c..cf1ba85 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,9 @@ "matrix-synapse-deactivate-user": "src/matrix-synapse-deactivate-user.js", "matrix-synapse-join-room": "src/matrix-synapse-join-room.js", "matrix-whois-user": "src/matrix-whois-user.js", - "matrix-paginate-room": "src/matrix-paginate-room.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": { diff --git a/src/matrix-event-relations.html b/src/matrix-event-relations.html new file mode 100644 index 0000000..d016596 --- /dev/null +++ b/src/matrix-event-relations.html @@ -0,0 +1,230 @@ + + + + + diff --git a/src/matrix-event-relations.js b/src/matrix-event-relations.js new file mode 100644 index 0000000..9e37030 --- /dev/null +++ b/src/matrix-event-relations.js @@ -0,0 +1,108 @@ +const {RelationType, EventType, Direction} = require("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 { + function evaluateNodePropertySafe(value, type, node, msg) { + try { + return RED.util.evaluateNodeProperty(value, type, node, msg); + } catch (e) { + if (e instanceof TypeError) { + return undefined; // Handle TypeError and return undefined + } + throw e; // Re-throw other errors to prevent masking issues + } + } + + 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); +}; diff --git a/src/matrix-get-event.html b/src/matrix-get-event.html new file mode 100644 index 0000000..e958e14 --- /dev/null +++ b/src/matrix-get-event.html @@ -0,0 +1,103 @@ + + + + + + diff --git a/src/matrix-get-event.js b/src/matrix-get-event.js new file mode 100644 index 0000000..4220abb --- /dev/null +++ b/src/matrix-get-event.js @@ -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); +} diff --git a/src/matrix-get-user.html b/src/matrix-get-user.html index 705a50a..ae2b58b 100644 --- a/src/matrix-get-user.html +++ b/src/matrix-get-user.html @@ -12,25 +12,19 @@
- +
This is the property the user data object will be set to
-
- -
-
-
    -
    \ No newline at end of file + +

    Outputs

    +
      +
    • Output 1 (Success): +
        +
      • Triggered when the event is successfully marked as read.
      • +
      +
    • +
    • Output 2 (Failure): +
        +
      • Triggered when there is an error marking the event as read. An error message will be included in msg.error.
      • +
      +
    • +
    + +

    Usage

    +

    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.

    + diff --git a/src/matrix-mark-read.js b/src/matrix-mark-read.js index f9559ca..bc8eebe 100644 --- a/src/matrix-mark-read.js +++ b/src/matrix-mark-read.js @@ -56,7 +56,17 @@ module.exports = function(RED) { let roomId = getToValue(msg, node.roomType, node.roomValue), eventId = getToValue(msg, node.eventIdType, node.eventIdValue); - msg.payload = await node.server.matrixClient.setRoomReadMarkers(roomId, eventId); + 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}`; diff --git a/src/matrix-paginate-room.html b/src/matrix-paginate-room.html index f5fdbe5..a10b3ab 100644 --- a/src/matrix-paginate-room.html +++ b/src/matrix-paginate-room.html @@ -1,9 +1,9 @@ \ No newline at end of file + +

    Dynamic Properties

    +

    Some inputs like Room, Pagination Key, and Page Size can be dynamically set using message, flow, or global context variables.

    + +

    Usage

    +

    To paginate through a room's timeline, trigger this node with a msg 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.

    + diff --git a/src/matrix-paginate-room.js b/src/matrix-paginate-room.js index 8798e6c..7fbab8d 100644 --- a/src/matrix-paginate-room.js +++ b/src/matrix-paginate-room.js @@ -101,7 +101,8 @@ module.exports = function(RED) { moreMessages = true; if(!timelineWindow) { let timelineSet = room.getUnfilteredTimelineSet(); - node.debug(JSON.stringify(timelineSet.getFilter())); + // node.debug(JSON.stringify(timelineSet.getFilter())); + // MatrixClient's option initialSyncLimit gets set to the filter we are using // so override that value with our pageSize timelineWindow = new TimelineWindow(node.server.matrixClient, timelineSet); @@ -135,7 +136,7 @@ module.exports = function(RED) { // user : node.matrixClient.getUser(event.getSender()), topic : event.getRoomId(), eventId : event.getId(), - event : event, + event : event.getEffectiveEvent(), }; }); } diff --git a/src/matrix-send-message.html b/src/matrix-send-message.html index 199b0e2..b35781b 100644 --- a/src/matrix-send-message.html +++ b/src/matrix-send-message.html @@ -89,6 +89,9 @@ +
    + If true and msg.content.['m.relates_to'].event_id or msg.eventId (parsed in that order) is provided the message will be a thread reply to the original event. +