Compare commits

...

113 Commits

Author SHA1 Message Date
04de0b4eb3
Merge pull request #132 from Skylar-Tech/dev
Release v0.9.1
2025-02-09 22:41:41 -07:00
8cb52112c1 Fix module compatibility with Node.js < 22 2025-02-09 22:36:52 -07:00
54a9972bbc
Merge pull request #121 from Skylar-Tech/dev
Release v0.9.0
2025-02-07 20:56:40 -07:00
ad34f018ab
Merge pull request #127 from koosc/allow-unknown
Add option for allowing unknown devices in rooms
2025-02-07 20:52:10 -07:00
20345787d2 README.md change 2025-02-07 20:50:25 -07:00
99c19923c6 Release v0.9.0 2025-02-07 20:27:36 -07:00
093d59893e Fix roomId and eventId inputs not saving field type correctly for get-event node 2025-02-07 20:27:19 -07:00
913f5dfcb9 - Upgrade to matrix-js-sdk 34.11.1 to fix CVE-2024-50336
- Remove request package (no longer needed)
2025-02-05 11:59:39 -07:00
e0947dd3bc
Merge pull request #128 from wuast94/master
Add m.notice to the receive node
2025-02-03 20:54:40 -07:00
8287f3c08a
Merge pull request #130 from LokiMidgard/patch-1
Support default plaintext in msg.format
2025-02-03 20:51:43 -07:00
Patrick Kranz
2a78524a90
use hasOwn instead of keys 2025-01-09 15:28:28 +01:00
Patrick Kranz
d01838ac84
Fix error 2025-01-09 15:14:50 +01:00
Patrick Kranz
2059f8455d
Update matrix-send-message.js 2025-01-09 15:12:09 +01:00
0cb8ecf8aa
Updated README with a link to a guide that explains how to register users via web browser 2025-01-04 12:58:08 -07:00
Marc
77f2c4be46 Add m.notice to the receive node 2025-01-01 05:17:12 +00:00
Chris Koos
cf82daf5da Add option for allowing unknown devices
Allows workaround for sending messages until verification is implemented
2024-11-10 10:57:43 -08:00
487241e097
Merge pull request #120 from Skylar-Tech/new-nodes
New nodes
2024-09-18 22:24:25 -06:00
3b161f1ad9 - Convert examples list examples/README.md to use collapsable github sections
- Add another example for sending an uploading file to a room
- Add form tip for Send Message node's thread reply config option
2024-09-18 22:19:49 -06:00
45ff930518 - Update examples README to make things much easier to find
- Added examples for every node
- Fixed User Settings node requiring a roomId when it's not needed
- Fixed the documentation for Upload File node
- Get User node had unused config code that has been removed
2024-09-18 15:21:13 -06:00
9e3b66f4aa - Fix Mark Read node not working properly and update the docs for this node #111 2024-09-18 09:32:19 -06:00
02826e2769 - Fix Room Pagination node event data being unaccessible #28
- Add new node `Get Event` that will give you room data for a corresponding roomId and eventId #117
- Add new node `Fetch Event Relations` that can paginate through events related to another event #119
- README updates
2024-09-16 23:27:29 -06:00
6bbd1d5119
Update README.md
Update readme
2024-09-03 21:31:39 -06:00
5de1274def
Merge pull request #96 from Skylar-Tech/dev
Release v0.8.0
2024-09-03 21:03:18 -06:00
1b54bc03eb
Merge pull request #114 from squatica/fix-deprecated-login
stop using deprecated login object
2024-09-03 21:00:27 -06:00
65edc94854 Add deprecation notice for Send File and Send Image nodes #102 2024-06-24 19:20:19 -06:00
351679ad77 - Add image thumbnail generation support for file upload node (and improved logging) #102
- Fix Received images missing thumbnail kills Node Red #65
- Trim rooms provided in receive node's Room ID config value
2024-06-13 20:21:20 -06:00
squatica
2b2da4faf7
stop using deprecated login object 2024-05-06 20:53:04 +02:00
5090e4fbb6 - fix 2024-02-24 22:29:52 -07:00
51e649b4cf - #109 get own events
- #28 paginate room history
- #111 manual read markers
- fix clearing global storage
- update node docs for upload file
2024-02-07 09:06:28 -07:00
a08709265e Update node icon 2023-12-15 22:32:20 -07:00
57ba70db6c - Received room timeline events now output msg.user containing info about the user that sent the event. 2023-12-15 04:03:13 -07:00
a3e1381d53 - Update room state events node so topic property can be dynamically configured 2023-12-15 03:56:32 -07:00
6dca3aa70e - #106 Add node for getting data for a user
- couple other fixes
2023-12-15 03:52:28 -07:00
b36286d994 Change version to 0.8.0 2023-12-15 01:39:45 -07:00
f14190d9ea - #105 Add new node for setting or getting user settings (display name or avatar_url)
- Fix class name of matrix-room-state-events node
2023-12-15 01:37:03 -07:00
000c28e3b8 - Reduce margin between checkboxes for matrix-receive node
- matrix-send-message node can now be used to reply in a thread (closes #104)
- matrix-receive node now returns msg.mentions for easier access to who was mentioned in message
- matrix-receive node now returns boolean msg.isThread based on whether the message is a thread reply or not
2023-11-25 05:38:09 -07:00
fd174e32ff
Merge pull request #103 from Skylar-Tech/102-upload-file
New file uploading node
2023-11-05 00:08:24 -06:00
e8506d8887 #102 Added new node for file uploading
#102 File upload node automatically detects mime type from name
#102 File upload node automatically fills in information for m.video, m.audio, and m.image (resolution, duration, etc)
#102 File upload node can generate a thumbnail for videos
#102 Send message node now accepts an object to override the message content
2023-11-05 00:06:52 -06:00
d7c4bc26bb - #100 add documentation for new typing node and update README
- #100 fix typing event causing server error due to number being string
- Mention using pantalaimon as an alternative for E2EE support in README
2023-10-22 16:56:49 -06:00
785e0cd7be - #100 add node for sending typing state to rooms
- fix global and flow variable in getters for Matrix Room States node
2023-10-22 04:32:06 -06:00
2e9633e113 - #97 msg.state_key is now allowed as an input to Room State Events node for events that support it (required for m.space.child and m.space.parent)
- #97 added support for m.room.history_visibility, m.room.server_acl, m.room.pinned_events, m.space.child, and m.space.parent
- #97 fix issue with checkbox being hidden on config page when adding new setters/getters on config page
2023-10-22 03:01:33 -06:00
1859696122 - #97 added option to fetch state event from local storage and fallback to server if necessary (allows for faster lookups and gives the full event object with information about when/who created it, etc)
- #97 remove num, bool, bin, and data from being options you can set to a state event (currently only objects and sometimes strings are allowed)
- Updated Leave Room node so it deletes the room from local storage
- Updated server config node so it deletes the matrix client from storage during shutdown (possibly solution to #94)
2023-10-22 00:29:12 -06:00
fd605005d1 Closes #99
- matrix-server-config now auto populates with first option
2023-10-21 19:47:12 -06:00
85de450a1a
Merge pull request #98 from Skylar-Tech/97-room-settings
97 room settings
2023-10-21 19:43:29 -06:00
e7e0f2967b Issue #97 Room Settings
- Remove unused returnValues config option for Room State Events node
2023-10-21 19:39:32 -06:00
611e23b845 Issue #97 Room Settings
- Room Settings node renamed to Room State Events
- Room State Events node allows configuring inputs/outputs from config
- Fix bug with various nodes allowing execution even though matrix server isn't connected
- Replace deprecated matrixClient.setGlobalErrorOnUnknownDevices method
- Update docs for new Room State Events node
2023-10-21 19:33:44 -06:00
9d050a0d44 Issue #97 Room Settings
- fix join_allow_rules for Room Setting node not getting correctly
- update Room Setting docs
2023-10-15 14:45:16 -06:00
c833a40a84 Issue #97 Room Settings
- Can set room name, topic, and avatar
- Can get name, topic, avatar, encrypted, power_levels, aliases, guest_access, join_rule, and join_allow_rules
2023-10-15 04:17:12 -06:00
0e755bc350 - Remove console.log usage 2023-10-14 23:22:38 -06:00
c920dd12cb - Fix error with matrix-synapse-register node
- Ensure matrix-server-config's register/deregister methods are always available
2023-10-14 22:26:29 -06:00
bvmensvoort
9661922f78 Pass msg object where possible
As described on https://nodered.org/docs/user-guide/writing-functions#handling-errors
2023-10-14 21:32:46 -06:00
bvmensvoort
20c7182511 Make errors catchable and (de)register at config node of all nodes 2023-10-14 21:32:46 -06:00
bvmensvoort
f48ba74a72 Make errors of config node catchable via a catch node 2023-10-14 21:32:46 -06:00
bvmensvoort
124a0cba34 (de)Register consumer nodes at config node
In order for error messages to be catchable
2023-10-14 21:32:46 -06:00
bvmensvoort
8ca11f36d8 Make errors of nodes catchable by a catch node 2023-10-14 21:32:46 -06:00
8a7fba39e8
Merge pull request #95 from Skylar-Tech/dev
Release v0.7.1
2023-09-05 11:22:48 -06:00
c61eadd05d Release 0.7.1
- Upgrade matrix-js-sdk from 22.0.0 to 28.0.0
- Upgrade olm from v3.2.13 to v3.2.15
2023-09-05 11:22:00 -06:00
78f8ab7abb
Merge pull request #74 from Skylar-Tech/dev
v0.7.0
2022-12-06 22:15:49 -07:00
ce8be4a30f Update readme 2022-12-06 22:10:26 -07:00
3e808cabec - Set version to 0.7.0 2022-12-06 21:53:30 -07:00
2fdc7482ce - Add back in persistent storage
- Receive node now accepts video files
2022-12-06 21:49:05 -07:00
c7f9d56df2 - Updated code for new authedRequest argument format
- matrix-js-sdk updated to 22.0.0
- fs-extra updated to 11.1.0
- minimum version set to 14.14
2022-12-06 18:47:41 -07:00
3c042ae47d - rename "Invite Room" node to "Invite to Room" to distinguish from new node
- new node "Invite Room" for receiving room invites
- new node "Leave Room" for leaving a room
- updated examples with new nodes
2022-12-05 23:40:05 -07:00
0a34870fa3
Merge pull request #71 from jacotec/master
Pass invites to the receive object
2022-12-05 21:20:11 -07:00
768a1c8ce0
Merge pull request #73 from WesleyE/master
Verify login on the relative matrix-chat/login URI
2022-12-05 21:19:03 -07:00
22dd9b4ca3 - matrix-js-sdk updated to v21.2.0
- added node-fetch polyfill for older nodejs versions
- added abort-controller polyfill for older nodejs versions
2022-12-05 21:18:16 -07:00
462f9670c2 - matrix-js-sdk updated to v20.0.0
- olm updated to v3.2.13 https://gitlab.matrix.org/matrix-org/olm/-/packages/136
- console messages now only appear if Node-RED's settings.js `logging.console.level` is info, debug, or trace
2022-12-05 19:49:55 -07:00
Wesley Elfring
e4b01c40c2
Verify login on the relative matrix-chat/login URI 2022-11-30 13:11:33 +01:00
Marco Jakobs
908d60835d Pass invites to the receive object
This commit passes a room invite to the receive node, so you can react to it.

Sample invitation:

HDsvcHGiTYFxxxxxxx:mydomain.de : msg : Object
object
type: "r.invite"
payload: "Invitation"
userId: "@relay:mydomain.de"
topic: "!HDsvcHGiTYFxxxxxxx:mydomain.de"
_msgid: "ef1fd7f3c8ae82e6"
2022-11-25 20:56:06 +01:00
4c17a21008
Merge pull request #63 from Skylar-Tech/dev
Version 0.6.1
2022-04-19 13:03:47 -06:00
bd4f6ea486 Update version to 0.6.1
- Fix: Error thrown in folder migration if new and old path are same
2022-04-19 13:01:00 -06:00
5ef0b6a11f Fix: Error thrown in folder migration if new and old path are same 2022-04-19 12:58:52 -06:00
97f27e61c6
Merge pull request #62 from Skylar-Tech/61-audio-messages
61 audio messages
2022-03-28 00:45:22 -06:00
7bdadc0fe9 Closes #209
- Can now receive audio files
- Can now receive locations
- Readme updated
2022-03-28 00:42:52 -06:00
5f129560aa Couple of little changes:
- Fix matrix-receive message types displaying in uppercase when it should be lowercase
- Readme updated with some extra things that are now possible since last release
- Link to matrix.org project in our README.md
2022-03-28 00:42:19 -06:00
00bc14e1c7
Merge pull request #59 from Skylar-Tech/dev
Version 0.5.8
2022-03-18 13:50:28 -06:00
9f41b67174 Version 0.5.8 2022-03-18 13:49:38 -06:00
4e93b7253e Revert examples 2022-03-18 13:37:24 -06:00
ecb4427217 - Revert device verification/e2ee stuff. It's not ready. 2022-03-18 13:34:46 -06:00
3e70369cae WIP 2022-03-18 00:23:28 -06:00
2ce80e2906 Update version to 0.5.5
- `fs-extra` and `got` dependencies updates
2022-03-17 21:06:15 -06:00
0c657caf5f Update version to 0.5.0 2022-03-17 20:05:06 -06:00
848fd0ec9d
Merge pull request #58 from Skylar-Tech/48-key-sharing
48 key sharing
2022-03-17 20:03:56 -06:00
fef40f4ea9 - Update matrix-device-verification node description with super basic info on how to use it 2022-03-17 20:03:48 -06:00
595fbca3df - Update main readme with new verification notes 2022-03-17 19:58:25 -06:00
487f17a439 Update examples to move function example above verification ones. 2022-03-17 19:54:13 -06:00
68cb5a026e Update examples to include one for receiving and requesting device verification 2022-03-17 19:51:14 -06:00
d01733c647 closes #48
- Device verification can now be requested or received
2022-03-17 19:38:29 -06:00
ed146e98d8 Closes #56 Closes #50
- can now specify message in node's configuration for matrix-send-message
- can now specify reaction in node's configuration for matrix-react
- reason can now be configured on both matrix-room-kick and matrix-room-ban
- icons updated for various nodes
- fix tag for name configuration field for various nodes
- roomId input is now validated to ensure it starts with ! and if not shows an error
2022-03-17 17:21:44 -06:00
3dc6363a88 Closes #56
- can now specify message in node's configuration for matrix-send-message
- can now specify reaction in node's configuration for matrix-react
- reason can now be configured on both matrix-room-kick and matrix-room-ban
- icons updated for various nodes
2022-03-17 16:01:27 -06:00
380e548425 Closes #54
- New option for matrix-send-message node that allows replacing existing message if enabled and `msg.eventId` is passed in.
2022-03-17 15:37:10 -06:00
27dd4d81a2 name should default to null 2022-03-17 14:47:01 -06:00
de3c58044f Add example for delete-event node 2022-03-17 14:40:53 -06:00
db1901ed1e Closes #29
- msg.isDM is now returned on received messages to help determine if it originated from a direct message room
2022-03-17 14:30:10 -06:00
ac68060825 - matrix-crypt-file documentation updated (it cannot encrypt, only decrypts)
- matrix-js-sdk update from 15.5.0 to 16.0.0
- Fixed matrix-js-sdk breaking changes
- @matrix-org_olm-3.2.8.tgz added to package since there is no npm release for this. It fixes some errors so we need it.
2022-03-17 14:23:22 -06:00
d304cb95f6
Merge pull request #55 from Skylar-Tech/51-device_id-fix
- We now use /whoami to validate the auth token since it gives us the…
2022-03-17 13:29:48 -06:00
85764c08d3
Merge pull request #52 from aikitori/feature/delete-message
Add Delete Message Node
2022-03-17 12:26:40 -06:00
aikitori
050be29d64 rename node 2022-03-13 10:58:17 +01:00
33823dea25 - We now use /whoami to validate the auth token since it gives us the user_id and device_id for the given auth_token (device_id is only available on Synapse 1.40 onwards)
- Error is now thrown if device_id cannot be automatically detected
- Do not store `null` into my_device_id and ignore if it has already been set. Fixes #51
2022-03-12 16:23:43 -07:00
aikitori
d826c58b51 Add Delete Message Node 2022-03-12 16:48:52 +01:00
d0ba671452 - Fix force-join-room example links 2022-02-09 12:58:32 -07:00
282c8ffc80 - Bump version to 0.4.6
- Added example for force joining a room
- Deactivate a user example was missing json file contents
2022-02-09 12:48:52 -07:00
6dd2ec75f0 - Move client storage to the Node-RED user directory.
- Remove mention of Device ID being required for encryption (since it can now auto generate if not provided this could make people think they have to set it when that isn't the case)
- matrix-js-sdk updated from ^v15.3.0 to ^v15.5.0
- got updated from ^11.8.2 to ^12.0.1 (this also required us to change how we import this library in code)
- Node-RED version requirement added for >=v1.3.0
- NodeJS version requirement added for >=v14.0.0
- removed `process` dependency
- send-image node fixed so error doesn't get thrown (`matrix-js-sdk` updated causing some errors)
- updated send-image node docs to explain that msg.contentType is necessary for some clients to render the image (otherwise it could display as a blank message in the room).
- If a matrix server configuration node was missing it's User ID it would throw a TypeError instead of telling the user the actual issue.
- Updated user list example: it now paginates all users on the server (if you had a lot of users the message would fail to send because it was too large)
- Added example for creating a room and inviting a user
- Added example for joining a mentioned room
- Added example for listing out a user's or server's rooms
- Added example for getting session data from a user via whois info
- Added example for getting a room's user list
- Added example for downloading & storing received files/images
- Added example for kicking/banning user from a room.
- Added example for deactivating a user
- Removed message in the room-users config stating it only works if you are an admin
- Receive node now outputs `msg.filename` for files and images
2022-02-09 12:13:27 -07:00
ae386b90a6 Fix olm library reference. 2022-02-03 11:33:12 -07:00
4d1ec9e65c - Make it clear that manually generating a Device ID is bad behavior
- Make DeviceID no longer required for matrix-server-config
2022-02-03 10:36:09 -07:00
2fcd463eb6 - Update release to 0.4.0 2022-02-03 10:16:19 -07:00
b44629c84e - When checking authentication we should retry startup if the error we receive isn't related to invalid auth token (M_UNKNOWN_TOKEN) 2022-02-03 10:15:52 -07:00
9f0a962991 - Fixed: Reactions weren't working because sendCompleteEvent is a private method. 2022-02-03 09:40:32 -07:00
f3df8c6f97 This comment is no longer necessary since we implemented it. 2022-01-28 11:17:23 -07:00
d6ab098063 Fixed example readme linking to wrong json file Closes #38 2022-01-28 10:57:35 -07:00
614920c40c Fixed: matrix-join-room was requiring userId when it didn't need it Closes #39 2022-01-28 10:55:38 -07:00
58d7d23b2f - Device ID is now stored inside the matrix-client-storage directory within Node-RED's config folder. This way if you leave the Device ID blank it will automatically generate one and save it. Previously if you didn't set a Device ID it would automatically generate one every time the client started which would break encryption (since device_id is tied to your encryption keys).
- You can now generate an Access Token and Device ID from just a username and password (use the Update Access Token button when configuring a client).
- Updated the look of the matrix-server-config node (icons and reorganization)
- Invalid auth token will no longer crash Node-RED on deployment/start. We do an authorized request before starting the client to see if the auth token is valid. If it is not the client will not start and all nodes for this client will show disconnected and an error will display in the debug toolbar.
- Updated matrix olm library from 3.2.1 to 3.2.8
- Setting a Device Label from the server-config node will now update your device's label. If you view the sessions for the client you will see it gets updated after deployment.
2022-01-28 10:53:10 -07:00
25c92b787a - Messages used to get ignored if they were over 1 second old. This caused issues with messages being ignored on servers that took longer than a second to respond (*cough* matrix.org *cough*). Now it accepts all new messages. Moving forward any messages that are sent before Node-RED starts or gets deployed will be ignored (so that old messages don't pop up since the cache clears every deploy/node-red restart so old messages look like new ones).
- When the matrix-server node would shut down (from a deployment) it wasn't properly shutting the old instance of the client down. This would cause messages to start duplicating X times (X being however many times you deployed since you last started Node-RED). This is now fixed.
- Upgraded to matrix-js-sdk 15.3.0
- Message processing is now done on the matrix-server node. Before if you had multiple matrix-receive nodes it would output one line per matrix-receive node into the log. Now it only outputs from the matrix-server node. Note that all messages get logged that are sent to rooms the bot is in whereas before it would only log message in rooms the matrix-receive node was in.
- Fixed Shared secret registration failing if user_type was defined.
2022-01-14 12:56:14 -07:00
115 changed files with 26584 additions and 7865 deletions

View File

@ -1,71 +1,76 @@
# node-red-contrib-matrix-chat # node-red-contrib-matrix-chat
Matrix 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)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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, 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
- Decrypt files in e2ee rooms - **Fetch/modify room state**: Update room settings
- Send HTML/Plain Text Message/Notice - **Paginate room history**
- React to messages - **Send files** (encryption support for files coming soon)
- Register user's on closed registration Synapse servers using `registration_shared_secret` (Admin Only) - **Send/edit messages** (supports plain text and HTML formats)
- List out users on a Synapse server (Admin Only) - **Send typing notifications**
- Get WhoIs info for a Synapse user (Admin Only) - **Delete events** (messages, reactions, etc.)
- Add/Edit Synapse users using the v2 API (requires a pre-existing admin account) - **Decrypt files** in E2EE rooms
- Deactivate users on Synapse servers (Admin Only) - **React to messages**
- Get a user list from a room - **Admin tools**:
- Kick user from room - Register users on closed Synapse servers (`registration_shared_secret`)
- Ban user from room - Manage users, including listing, adding, editing, deactivating (Synapse API)
- Join a room - Force-add users to rooms
- Create a room - **Room management**: Invite, kick, ban, join, create, and leave rooms
- Invite to a room
- 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. Its 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 wont 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!**

View File

@ -1,113 +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)
- [Respond to "users" with full list of server users](#respond-to-users-with-full-list-of-server-users)
### 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.
![get-set-displayname.png](get-set-displayname.png)
</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.
![set-avatar-from-url.png](set-avatar-from-url.png)
</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.
![get-user.png](get-user.png)
</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.
![img.png](shared-secret-registration.png) ![Shared Secret Registration](shared-secret-registration.png)
</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`.
![img.png](add-user-with-admin-user.png) ![Create/Edit Synapse User](add-user-with-admin-user.png)
</details>
### Use function node to run any command <details>
<summary>Deactivate User</summary>
[View JSON](custom-redact-function-node.json) [View JSON](deactivate-user.json)
If we do not have a node for something you want to do (such as redacting events/messages) you can do this manually with a function node. If you send "deactivate_user @test:example.com", the bot will deactivate the `@test:example.com` account on the server.
**Note:** You should make sure to catch any errors in your function node otherwise you could cause Node-RED to crash. **Note:**
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). - 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.
![img.png](custom-redact-function-node.png) ![Deactivate User](deactivate-user.png)
### Respond to "ping" with "pong" </details>
<details>
<summary>Force User to Join Room</summary>
[View JSON](force-join-room.json)
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 only works for rooms on the same server.
![Force User to Join Room](force-join-room.png)
</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)
![img.png](send-image-to-room.png)
</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.
![Respond to "ping" with "pong"](respond-ping-pong.png)
</details>
<details>
<summary>Respond to "html" with an HTML Message</summary>
[View JSON](respond-to-html-with-html.json) [View JSON](respond-to-html-with-html.json)
Use this flow to respond to anyone that says "ping" with "pong" into the same room. 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.
![img.png](respond-ping-pong.png) ![Respond to "html" with HTML Message](respond-to-html-with-html.png)
</details>
<details>
### Respond to "html" with an HTML message <summary>Respond to "react" with a Reaction</summary>
[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.
![img.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.
![img_1.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.
![img.png](respond-file-with-file.png)
### Respond to "react" with a reaction
[View JSON](respond-react-with-reaction.json) [View JSON](respond-react-with-reaction.json)
Give a 👍 reaction when someone says "react" Gives a 👍 reaction when someone says "react".
![img.png](respond-react-with-reaction.png) ![Respond to "react" with Reaction](respond-react-with-reaction.png)
</details>
<details>
<summary>Remove Messages Containing "delete"</summary>
### Respond to "users" with full list of server users [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)
</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).
![Sending Typing Events](send-typing-events.png)
</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.
![mark-all-read.png](mark-all-read.png)
</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
![get-event.png](get-event.png)
</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.
![paginate-room-history.png](paginate-room-history.png)
</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
![fetch-event-relations.png](fetch-event-relations.png)
</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.
![set-room-name-and-topic.png](set-room-name-and-topic.png)
</details>
<details>
<summary>Accept Room Invites from Specific User</summary>
[View JSON](accept-room-invites.json)
Automatically accept room invites from a specific user.
![Accept Room Invites from Specific User](accept-room-invites.png)
</details>
<details>
<summary>Leave Room When Someone Says "bye"</summary>
[View JSON](leave-room-bye.json)
Leaves the room when someone says "bye".
![Leave Room When Someone Says "bye"](leave-room-bye.png)
</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.
![Respond to "newroom" by Creating New Room](respond-newroom-invite.png)
</details>
<details>
<summary>Respond to "joinroom &lt;room_id_or_alias&gt;" by Joining Mentioned Room</summary>
[View JSON](respond-joinroom.json)
When someone sends "joinroom &lt;room_id_or_alias&gt;", the bot will join the mentioned room.
![Respond to "joinroom" by Joining Room](respond-joinroom.png)
</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.
![Kick/Ban User from Room](room-kick-ban.png)
</details>
### User Information
<details>
<summary>Respond to "users" with Full List of Server Users</summary>
[View JSON](respond-users-list.json) [View JSON](respond-users-list.json)
When someone sends the text "users" they get a HTML message back containing all the current users on the server. When someone sends the text "users", they 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.
This requires admin privileges. **Notes:**
![img.png](respond-users-list.png) - 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)
</details>
<details>
<summary>Respond to "whois &lt;user_id&gt;" 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.
![Respond to "whois" with User Session Info](respond-whois.png)
</details>
<details>
<summary>Respond to "rooms &lt;user_id&gt;" with User's Rooms</summary>
[View JSON](respond-rooms.json)
Responds to "rooms &lt;user_id&gt;" 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)
</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.
![Respond to "room_users" with User List](respond-room-users.png)
</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).
![Use Function Node to Run Commands](custom-redact-function-node.png)
</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.
![Download & Store Received Files](store-received-files.png)
</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.
![Respond to "image" with Uploaded Image](respond-image-with-image.png)
</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.
![Respond to "file" with Uploaded File](respond-file-with-file.png)
</details>

View File

@ -0,0 +1,94 @@
[
{
"id": "64f76474ff7a3727",
"type": "matrix-room-invite",
"z": "f025a8b9fbd1b054",
"name": "",
"server": "8da0ef83f77f8e24",
"roomId": null,
"x": 270,
"y": 2380,
"wires": [
[
"22f6056fa5bc5bd0"
]
]
},
{
"id": "6d6f304a0a6342b8",
"type": "matrix-join-room",
"z": "f025a8b9fbd1b054",
"name": "",
"server": "8da0ef83f77f8e24",
"x": 770,
"y": 2380,
"wires": [
[
"1409ebb4a0e65663"
],
[
"1409ebb4a0e65663"
]
]
},
{
"id": "22f6056fa5bc5bd0",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.userId == @skylord123:skylar.tech",
"property": "userId",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "@skylord123:skylar.tech",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 520,
"y": 2380,
"wires": [
[
"6d6f304a0a6342b8"
]
]
},
{
"id": "043f74e6d76b1eb0",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Only accept room invites from specific user",
"info": "",
"x": 360,
"y": 2340,
"wires": []
},
{
"id": "1409ebb4a0e65663",
"type": "debug",
"z": "f025a8b9fbd1b054",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 910,
"y": 2380,
"wires": []
},
{
"id": "8da0ef83f77f8e24",
"type": "matrix-server-config",
"name": null,
"autoAcceptRoomInvites": false,
"enableE2ee": true,
"global": true
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,73 @@
[
{
"id": "fed9197df27197a4",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptImages": true,
"x": 340,
"y": 1560,
"wires": [
[
"b289bb4fed9fa166"
]
]
},
{
"id": "b289bb4fed9fa166",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "cont",
"v": "delete",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 490,
"y": 1560,
"wires": [
[
"48766b632ab2e6a1"
]
]
},
{
"id": "48766b632ab2e6a1",
"type": "matrix-delete-event",
"z": "f025a8b9fbd1b054",
"name": "",
"server": null,
"roomId": "",
"reason": "Requested deletion",
"x": 630,
"y": 1560,
"wires": [
[],
[]
]
},
{
"id": "11f9cbbed7b95c83",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Delete messages containing \"delete\"",
"info": "",
"x": 480,
"y": 1520,
"wires": []
}
]

BIN
examples/delete-event.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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"
]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

103
examples/get-event.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

99
examples/get-user.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,103 @@
[
{
"id": "997c354038202dba",
"type": "comment",
"z": "f025a8b9fbd1b054",
"name": "Leave room when someone says \"bye\"",
"info": "",
"x": 350,
"y": 2520,
"wires": []
},
{
"id": "69c4ea189be94feb",
"type": "matrix-receive",
"z": "f025a8b9fbd1b054",
"name": "",
"server": "8da0ef83f77f8e24",
"roomId": "",
"acceptText": true,
"acceptEmotes": true,
"acceptStickers": true,
"acceptReactions": true,
"acceptFiles": true,
"acceptAudio": true,
"acceptImages": true,
"acceptLocations": true,
"x": 280,
"y": 2560,
"wires": [
[
"19e1d64b63ae8a1f"
]
]
},
{
"id": "19e1d64b63ae8a1f",
"type": "switch",
"z": "f025a8b9fbd1b054",
"name": "msg.payload == bye",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "bye",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 480,
"y": 2560,
"wires": [
[
"db0e51f8e7793f92"
]
]
},
{
"id": "db0e51f8e7793f92",
"type": "matrix-leave-room",
"z": "f025a8b9fbd1b054",
"name": "",
"server": "8da0ef83f77f8e24",
"roomId": null,
"x": 670,
"y": 2560,
"wires": [
[
"3791f551bf0e4fc4"
],
[
"3791f551bf0e4fc4"
]
]
},
{
"id": "3791f551bf0e4fc4",
"type": "debug",
"z": "f025a8b9fbd1b054",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 810,
"y": 2560,
"wires": []
},
{
"id": "8da0ef83f77f8e24",
"type": "matrix-server-config",
"name": null,
"autoAcceptRoomInvites": false,
"enableE2ee": true,
"global": true
}
]

BIN
examples/leave-room-bye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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"
]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

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

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

BIN
examples/respond-rooms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 44 KiB

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

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

BIN
examples/respond-whois.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,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": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View 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": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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"
],
[]
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View 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": []
}
]

25102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,68 @@
{ {
"name": "node-red-contrib-matrix-chat", "name": "node-red-contrib-matrix-chat",
"version": "0.2.7", "version": "0.9.1",
"description": "Matrix chat server client for Node-RED", "description": "Matrix chat server client for Node-RED",
"dependencies": { "dependencies": {
"fs-extra": "^9.1.0", "abort-controller": "^3.0.0",
"got": "^11.8.2", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^11.1.0",
"got": "^12.0.2",
"image-size": "^1.0.2",
"isomorphic-webcrypto": "^2.3.8", "isomorphic-webcrypto": "^2.3.8",
"matrix-js-sdk": "^12.2.0", "matrix-js-sdk": "34.11.1",
"mime": "^3.0.0",
"node-fetch": "^3.3.0",
"node-localstorage": "^2.2.1", "node-localstorage": "^2.2.1",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download",
"process": "^0.11.10", "sharp": "^0.33.4",
"tmp": "^0.2.1",
"utf8": "^3.0.0" "utf8": "^3.0.0"
}, },
"node-red": { "node-red": {
"version": ">=1.3.0",
"nodes": { "nodes": {
"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-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-join-room": "src/matrix-join-room.js", "matrix-join-room": "src/matrix-join-room.js",
"matrix-leave-room": "src/matrix-leave-room.js",
"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": {
"node": ">=14.0.0"
},
"keywords": [ "keywords": [
"node-red", "node-red",
"matrix", "matrix",
"support", "chat",
"bot", "chatbot",
"chat" "federated"
], ],
"repository": { "repository": {
"type": "git", "type": "git",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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";
@ -19,7 +19,7 @@
<script type="text/html" data-template-name="matrix-create-room"> <script type="text/html" data-template-name="matrix-create-room">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>

View File

@ -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);
} }

View File

@ -18,14 +18,14 @@
<script type="text/html" data-template-name="matrix-decrypt-file"> <script type="text/html" data-template-name="matrix-decrypt-file">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
</script> </script>
<script type="text/html" data-help-name="matrix-decrypt-file"> <script type="text/html" data-help-name="matrix-decrypt-file">
<h3>Details</h3> <h3>Details</h3>
<p>Files sent in an encrypted room are themselves encrypted. Use this node to encrypt/decrypt files. Note: This node will download the encrypted file so be cautious of large downloads.</p> <p>Files sent in an encrypted room are themselves encrypted. Use this node to decrypt files. Note: This node will download the encrypted file so be cautious of large downloads.</p>
<h3>Inputs</h3> <h3>Inputs</h3>
<dl class="message-properties"> <dl class="message-properties">

View File

@ -1,5 +1,4 @@
module.exports = function(RED) { module.exports = function(RED) {
const got = require('got');
const crypto = require('isomorphic-webcrypto'); const crypto = require('isomorphic-webcrypto');
function MatrixDecryptFile(n) { function MatrixDecryptFile(n) {
@ -10,23 +9,25 @@ module.exports = function(RED) {
this.name = n.name; this.name = n.name;
node.on("input", async function (msg) { node.on("input", async function (msg) {
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;
} }

View File

@ -0,0 +1,96 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-delete-event',{
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 },
reason: { value: "" },
},
label: function() {
return this.name||"Delete Event";
},
paletteLabel: 'Delete Event'
});
</script>
<script type="text/html" data-template-name="matrix-delete-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-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div>
<div class="form-row">
<label for="node-input-reason"><i class="fa fa-sticky-note"></i> Reason</label>
<input type="text" id="node-input-reason" placeholder="msg.reason">
</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-delete-event">
<h3>Details</h3>
<p>Delete an event in a room</p>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> Room ID from where the event should be deleted from. Optional if configured on the node. If configured on the node this input will be overridden.</dd>
<dt>msg.eventId
<span class="property-type">string</span>
</dt>
<dd>Event ID of the Event which should be deleted.</dd>
<dt>msg.reason
<span class="property-type">string</span>
</dt>
<dd>Reason why the event is deleted. Default an empty string</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.eventId <span class="property-type">string</span></dt>
<dd>the eventId from the deleted event.</dd>
</dl>
<dl class="message-properties">
<dt>msg.deleted <span class="property-type">boolean</span></dt>
<dd>True, if the event is deleted</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 class="message-properties">
<dt>msg.deleted <span class="property-type">boolean</span></dt>
<dd>False, if the event is not deleted</dd>
</dl>
</dl>
</li>
</ol>
</script>

View File

@ -0,0 +1,80 @@
module.exports = function(RED) {
function MatrixDeleteEvent(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.reason = n.reason
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', function(msg) {
if(!msg.eventId) {
node.error("eventId is missing", msg);
node.send([null, msg])
return;
}
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);
node.send([null, msg]);
return;
}
msg.topic = node.roomId || msg.topic;
if(!msg.topic) {
node.warn("Room must be specified in msg.topic or in configuration");
return;
}
msg.reason = node.reason || msg.reason;
if(!msg.reason) {
msg.reason = '';
}
node.server.matrixClient.redactEvent(msg.topic, msg.eventId, undefined,{
reason: msg.reason
})
.then(function(e) {
msg.deleted = true
node.send([msg, null]);
})
.catch(function(e){
node.warn("Error deleting event " + e);
msg.error = e;
msg.deleted = false
node.send([null, msg]);
});
});
node.on("close", function() {
node.server.deregister(node);
});
}
RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent);
}

View 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>

View 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
View File

95
src/matrix-get-event.html Normal file
View 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
View 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
View 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
View 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);
}

View File

@ -8,19 +8,19 @@
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() {
return this.name || "Room Invite"; return this.name || "Invite to Room";
}, },
paletteLabel: 'Room Invite' paletteLabel: 'Invite to Room'
}); });
</script> </script>
<script type="text/html" data-template-name="matrix-invite-room"> <script type="text/html" data-template-name="matrix-invite-room">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
@ -30,9 +30,21 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-invite-room"> <script type="text/html" data-help-name="matrix-invite-room">

View File

@ -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);
} }

View File

@ -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";
@ -19,7 +19,7 @@
<script type="text/html" data-template-name="matrix-join-room"> <script type="text/html" data-template-name="matrix-join-room">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -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,22 +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]);
}
if(!msg.topic) {
node.error("Room must be specified in msg.topic");
return; return;
} }
if(!msg.userId) { if(!msg.topic) {
node.error("msg.userId was not set."); node.error("Room must be specified in msg.topic", msg);
return; return;
} }
@ -51,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);
} }

View File

@ -0,0 +1,63 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-leave-room', {
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 },
},
label: function() {
return this.name || "Leave Room";
},
paletteLabel: 'Leave Room'
});
</script>
<script type="text/html" data-template-name="matrix-leave-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-user"></i> Matrix Server Config</label>
<input type="text" id="node-input-server">
</div>
</script>
<script type="text/html" data-help-name="matrix-leave-room">
<h3>Details</h3>
<p>
This node leaves a room
</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>msg.topic
<span class="property-type">string</span>
</dt>
<dd> The room identifier to leave: for example, <code>!h8zld9j31:example.com</code>.</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>Returns the same message that was received</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>

61
src/matrix-leave-room.js Normal file
View File

@ -0,0 +1,61 @@
module.exports = function(RED) {
function MatrixLeaveRoom(n) {
RED.nodes.createNode(this, n);
let node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
this.roomId = n.roomId;
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', function(msg) {
if (! node.server || ! node.server.matrixClient) {
node.error("No matrix server selected", msg);
return;
}
if(!msg.topic) {
node.error('No room provided in msg.topic', msg);
return;
}
if(!node.server.isConnected()) {
node.error("Matrix server connection is currently closed", msg);
node.send([null, msg]);
return;
}
try {
node.log("Leaving room " + msg.topic);
node.server.matrixClient.leave(msg.topic);
node.server.matrixClient.store.removeRoom(msg.topic);
node.send([msg, null]);
} catch(e) {
node.error("Failed to leave room " + msg.topic + ": " + e, msg);
msg.payload = e;
node.send([null, msg]);
}
});
node.on("close", function() {
node.server.deregister(node);
});
}
RED.nodes.registerType("matrix-leave-room", MatrixLeaveRoom);
}

98
src/matrix-mark-read.html Normal file
View 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
View 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);
}

View 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 servers 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
View 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);
}

View File

@ -8,8 +8,9 @@
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 }
}, },
label: function() { label: function() {
return this.name || "React"; return this.name || "React";
@ -20,7 +21,7 @@
<script type="text/html" data-template-name="matrix-react"> <script type="text/html" data-template-name="matrix-react">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">
@ -28,9 +29,25 @@
<input type="text" id="node-input-server"> <input type="text" id="node-input-server">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-row">
<label for="node-input-reaction"><i class="fa fa-thumbs-up"></i> Reaction</label>
<input type="text" id="node-input-reaction" placeholder="msg.payload">
</div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-react"> <script type="text/html" data-help-name="matrix-react">
@ -42,7 +59,7 @@
<dt>msg.payload <dt>msg.payload
<span class="property-type">string</span> <span class="property-type">string</span>
</dt> </dt>
<dd> Usually an emoji but can also be text. </dd> <dd> Usually an emoji but can also be text. If configured on the node this is ignored otherwise it required. </dd>
<dt>msg.topic <dt>msg.topic
<span class="property-type">string | null</span> <span class="property-type">string | null</span>

View File

@ -7,11 +7,13 @@ 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.roomId = n.roomId; this.roomId = n.roomId;
this.reaction = n.reaction;
if (!node.server) { if (!node.server) {
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" });
@ -25,44 +27,44 @@ 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.payload) { let payload = n.reaction || msg.payload;
node.error('msg.payload is required'); if(!payload) {
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;
} }
msg.type = 'm.reaction'; msg.type = 'm.reaction';
node.server.matrixClient.sendCompleteEvent( node.server.matrixClient.sendEvent(
msg.topic, msg.topic,
'm.reaction',
{ {
type: 'm.reaction', "m.relates_to": {
content: { event_id: eventId,
"m.relates_to": { key: payload,
event_id: eventId, rel_type: "m.annotation"
key: msg.payload,
rel_type: "m.annotation"
}
} }
} }
) )
@ -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);
} }

View File

@ -8,14 +8,19 @@
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},
acceptAudio: {"value": true},
acceptImages: {"value": true}, acceptImages: {"value": true},
acceptVideos: {"value": true},
acceptLocations: {"value": true},
}, },
label: function() { label: function() {
return this.name || "Matrix Receive"; return this.name || "Matrix Receive";
@ -34,73 +39,135 @@
<input type="text" id="node-input-server"> <input type="text" id="node-input-server">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId"> <input type="text" id="node-input-roomId">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-tips">Enter a single room, comma separated list of rooms, or leave blank to get from all</div> <div class="form-tips">Enter a single room, comma separated list of rooms, or leave blank to get from all</div>
<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"
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-input-acceptText" style="width: auto"> <label for="node-input-acceptText" style="width: auto">
Accept text <code>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"
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-input-acceptEmotes" style="width: auto"> <label for="node-input-acceptEmotes" style="width: auto">
Accept emotes <code>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"
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-input-acceptStickers" style="width: auto"> <label for="node-input-acceptStickers" style="width: auto">
Accept stickers <code>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"
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-input-acceptReactions" style="width: auto"> <label for="node-input-acceptReactions" style="width: auto">
Accept reactions <code>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"
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-input-acceptFiles" style="width: auto"> <label for="node-input-acceptFiles" style="width: auto">
Accept files <code>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
type="checkbox"
id="node-input-acceptAudio"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-input-acceptAudio" style="width: auto">
Accept files <code style="text-transform: none;">m.audio</code>
</label>
</div>
<div class="form-row" style="margin-bottom:0;">
<input <input
type="checkbox" type="checkbox"
id="node-input-acceptImages" id="node-input-acceptImages"
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-input-acceptImages" style="width: auto"> <label for="node-input-acceptImages" style="width: auto">
Accept images <code>m.image</code> Accept images <code style="text-transform: none;">m.image</code>
</label> </label>
</div> </div>
<div class="form-row" style="margin-bottom:0;">
<input
type="checkbox"
id="node-input-acceptVideos"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-input-acceptVideos" style="width: auto">
Accept videos <code style="text-transform: none;">m.video</code>
</label>
</div>
<div class="form-row" style="margin-bottom:0;">
<input
type="checkbox"
id="node-input-acceptLocations"
style="width: auto; margin-left: 125px; vertical-align: top"
/>
<label for="node-input-acceptLocations" style="width: auto">
Accept locations <code style="text-transform: none;">m.location</code>
</label>
</div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-receive"> <script type="text/html" data-help-name="matrix-receive">
@ -116,6 +183,11 @@
</dd> </dd>
</dl> </dl>
<dl class="message-properties">
<dt>msg.isDM <span class="property-type">bool</span></dt>
<dd> returns true if message is from a direct message room.</dd>
</dl>
<dl class="message-properties"> <dl class="message-properties">
<dt>msg.encrypted <span class="property-type">bool</span></dt> <dt>msg.encrypted <span class="property-type">bool</span></dt>
<dd> returns true if message was encrypted (e2ee).</dd> <dd> returns true if message was encrypted (e2ee).</dd>
@ -204,6 +276,11 @@
</li> </li>
<li><code>msg.type</code> == '<strong>m.file</strong>' <li><code>msg.type</code> == '<strong>m.file</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the file's parsed filename</dd>
</dl>
<dl class="message-properties"> <dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt> <dt>msg.url <span class="property-type">string</span></dt>
<dd>the file's URL</dd> <dd>the file's URL</dd>
@ -215,7 +292,44 @@
</dl> </dl>
</li> </li>
<li><code>msg.type</code> == '<strong>m.audio</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the image's parsed filename</dd>
</dl>
<dl class="message-properties">
<dt>msg.mimetype <span class="property-type">string</span></dt>
<dd>audio file mimetype (ex: audio/ogg)</dd>
</dl>
<dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt>
<dd>the file's URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.mxc_url <span class="property-type">string</span></dt>
<dd>the file's Matrix URL</dd>
</dl>
<dl class="message-properties">
<dt>msg.duration <span class="property-type">integer</span></dt>
<dd>duration of audio file in milliseconds</dd>
</dl>
<dl class="message-properties">
<dt>msg.waveform <span class="property-type">array[int]</span></dt>
<dd>waveform of the audio clip</dd>
</dl>
</li>
<li><code>msg.type</code> == '<strong>m.image</strong>' <li><code>msg.type</code> == '<strong>m.image</strong>'
<dl class="message-properties">
<dt>msg.filename <span class="property-type">string</span></dt>
<dd>the image's parsed filename</dd>
</dl>
<dl class="message-properties"> <dl class="message-properties">
<dt>msg.url <span class="property-type">string</span></dt> <dt>msg.url <span class="property-type">string</span></dt>
<dd>the image's URL</dd> <dd>the image's URL</dd>
@ -236,5 +350,12 @@
<dd>the image's thumbnail Matrix URL</dd> <dd>the image's thumbnail Matrix URL</dd>
</dl> </dl>
</li> </li>
<li><code>msg.type</code> == '<strong>m.location</strong>'
<dl class="message-properties">
<dt>msg.geo_uri <span class="property-type">string</span></dt>
<dd>URI format of the geolocation</dd>
</dl>
</li>
</ul> </ul>
</script> </script>

View File

@ -6,23 +6,29 @@ 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;
this.acceptAudio = n.acceptAudio;
this.acceptImages = n.acceptImages; this.acceptImages = n.acceptImages;
this.acceptVideos = n.acceptVideos;
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" });
}); });
@ -30,108 +36,109 @@ module.exports = function(RED) {
node.status({ fill: "green", shape: "ring", text: "connected" }); node.status({ fill: "green", shape: "ring", text: "connected" });
}); });
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, data) { node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
if (toStartOfTimeline) {
return; // ignore paginated results
}
if (!event.getSender() || event.getSender() === node.server.userId) {
return; // ignore our own messages
}
if (!event.getUnsigned() || event.getUnsigned().age > 1000) {
return; // ignore old messages
}
// if node has a room ID set we only listen on that room // if node has a room ID set we only listen on that room
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) { if (node.roomIds.length && !node.roomIds.includes(room.roomId)) {
return; return;
} }
try { if (!node.acceptOwnEvents && (!event.getSender() || event.getSender().toLowerCase() === node.server.matrixClient.getUserId().toLowerCase())) {
await node.server.matrixClient.decryptEventIfNeeded(event); node.log(`Ignoring${msg.encrypted ? ' encrypted' : ''} timeline event [${msg.type}]: (${room.name}) ${event.getId()} for reason: own event`);
} catch (error) {
node.error(error);
return; return;
} }
let msg = { const setUrls = (urlKey, encryptedKey) => {
encrypted : event.isEncrypted(), const url = msg.encrypted ? msg.content[encryptedKey]?.url : msg.content[urlKey];
redacted : event.isRedacted(), if (url) {
content : event.getContent(), msg.url = node.server.matrixClient.mxcUrlToHttp(url);
type : (event.getContent()['msgtype'] || event.getType()) || null, msg.mxc_url = url;
payload : (event.getContent()['body'] || event.getContent()) || null, }
userId : event.getSender(),
topic : event.getRoomId(),
eventId : event.getId(),
event : event,
}; };
node.log("Received" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getSender() + " :: " + msg.content.body); 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;
case 'm.notice':
if (!node.acceptNotices) return;
break; 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;
if(msg.encrypted) { msg.filename = msg.content.filename || msg.content.body;
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url); setUrls('url', 'file');
msg.mxc_url = msg.content.file.url; break;
} else {
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url); case 'm.audio':
msg.mxc_url = msg.content.url; if (!node.acceptAudio) return;
setUrls('url', 'file');
if ('org.matrix.msc1767.file' in msg.content) {
msg.filename = msg.content['org.matrix.msc1767.file'].name;
msg.mimetype = msg.content['org.matrix.msc1767.file'].mimetype;
}
if ('org.matrix.msc1767.audio' in msg.content) {
msg.duration = msg.content['org.matrix.msc1767.audio'].duration;
msg.waveform = msg.content['org.matrix.msc1767.audio'].waveform;
} }
break; break;
case 'm.image': case 'm.image':
if(!node.acceptImages) return; if (!node.acceptImages) return;
msg.filename = msg.content.filename || msg.content.body;
setUrls('url', 'file');
setThumbnailUrls('thumbnail_file');
break;
if(msg.encrypted) { case 'm.video':
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url); if (!node.acceptVideos) return;
msg.mxc_url = msg.content.file.url; msg.filename = msg.content.filename || msg.content.body;
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_file.url); setUrls('url', 'file');
msg.thumbnail_mxc_url = msg.content.info.thumbnail_file.url; setThumbnailUrls('thumbnail_file');
} else { break;
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
msg.mxc_url = msg.content.url; case 'm.location':
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url); if (!node.acceptLocations) return;
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url; msg.geo_uri = msg.content.geo_uri;
} msg.payload = msg.content.body;
break; break;
case 'm.reaction': case 'm.reaction':
if(!node.acceptReactions) return; if (!node.acceptReactions) return;
msg.info = msg.content["m.relates_to"].info; msg.info = msg.content["m.relates_to"].info;
msg.referenceEventId = msg.content["m.relates_to"].event_id; msg.referenceEventId = msg.content["m.relates_to"].event_id;
msg.payload = msg.content["m.relates_to"].key; msg.payload = msg.content["m.relates_to"].key;
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);
} }

View File

@ -8,8 +8,9 @@
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 }
}, },
label: function() { label: function() {
return this.name || "Room Ban"; return this.name || "Room Ban";
@ -20,7 +21,7 @@
<script type="text/html" data-template-name="matrix-room-ban"> <script type="text/html" data-template-name="matrix-room-ban">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">
@ -28,9 +29,25 @@
<input type="text" id="node-input-server"> <input type="text" id="node-input-server">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-row">
<label for="node-input-reason"><i class="fa fa-comment"></i> Reason</label>
<input type="text" id="node-input-reason" placeholder="msg.topic">
</div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-room-ban"> <script type="text/html" data-help-name="matrix-room-ban">
@ -52,7 +69,7 @@
<dt class="optional">msg.reason <dt class="optional">msg.reason
<span class="property-type">string</span> <span class="property-type">string</span>
</dt> </dt>
<dd> Reason for banning the user.</dd> <dd> Reason for banning the user. If configured on the node it will overwrite this input</dd>
</dl> </dl>
<h3>Outputs</h3> <h3>Outputs</h3>

View File

@ -7,11 +7,13 @@ 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.roomId = n.roomId; this.roomId = n.roomId;
this.reason = n.reason;
if (!node.server) { if (!node.server) {
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" });
@ -25,38 +27,43 @@ 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;
} }
node.server.matrixClient.ban(msg.topic, msg.userId, msg.reason || undefined) node.server.matrixClient.ban(msg.topic, msg.userId, n.reason || msg.reason || undefined)
.then(function(e) { .then(function(e) {
node.log("Successfully banned " + msg.userId + " from " + msg.topic); node.log("Successfully banned " + msg.userId + " from " + msg.topic);
msg.eventId = e.event_id; msg.eventId = e.event_id;
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);
} }

View File

@ -0,0 +1,80 @@
<script type="text/javascript">
RED.nodes.registerType('matrix-room-invite', {
category: 'matrix',
color: '#00b7ca',
icon: "matrix.png",
outputLabels: ["success", "error"],
inputs: 0,
outputs: 1,
defaults: {
name: { value: null },
server: { type: "matrix-server-config" },
roomId: { value: null },
},
label: function() {
return this.name || "Room Invite";
},
paletteLabel: 'Room Invite'
});
</script>
<script type="text/html" data-template-name="matrix-room-invite">
<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>
</script>
<script type="text/html" data-help-name="matrix-room-invite">
<h3>Details</h3>
<p>
This node receives room invites.
</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Success
<dl class="message-properties">
<dt>msg.type <span class="property-type">string</span></dt>
<dd>Always <code>m.room.member</code></dd>
</dl>
<dl class="message-properties">
<dt>msg.userId <span class="property-type">string</span></dt>
<dd>ID of the user the invite is from</dd>
</dl>
<dl class="message-properties">
<dt>msg.topic <span class="property-type">string</span></dt>
<dd>The room identifier for the invite: for example, <code>!h8zld9j31:example.com</code>.</dd>
</dl>
<dl class="message-properties">
<dt>msg.topicName <span class="property-type">string</span></dt>
<dd>The invited room name.</dd>
</dl>
<dl class="message-properties">
<dt>msg.event <span class="property-type">object</span></dt>
<dd>The event object for this invite to get extra details.</dd>
</dl>
<dl class="message-properties">
<dt>msg.eventId <span class="property-type">object</span></dt>
<dd>The ID of the event for this invite.</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>

36
src/matrix-room-invite.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = function(RED) {
function MatrixRoomInvite(n) {
RED.nodes.createNode(this, n);
let node = this;
this.name = n.name;
this.server = RED.nodes.getNode(n.server);
this.roomId = n.roomId;
if(!this.server) {
node.error('Server must be configured on the 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.server.on("Room.invite", async function(msg) {
node.send(msg);
});
node.on("close", function() {
node.server.deregister(node);
});
}
RED.nodes.registerType("matrix-room-invite", MatrixRoomInvite);
}

View File

@ -8,8 +8,9 @@
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 }
}, },
label: function() { label: function() {
return this.name || "Room Kick"; return this.name || "Room Kick";
@ -20,7 +21,7 @@
<script type="text/html" data-template-name="matrix-room-kick"> <script type="text/html" data-template-name="matrix-room-kick">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">
@ -28,9 +29,25 @@
<input type="text" id="node-input-server"> <input type="text" id="node-input-server">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-row">
<label for="node-input-reason"><i class="fa fa-comment"></i> Reason</label>
<input type="text" id="node-input-reason" placeholder="msg.topic">
</div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-room-kick"> <script type="text/html" data-help-name="matrix-room-kick">
@ -52,7 +69,7 @@
<dt class="optional">msg.reason <dt class="optional">msg.reason
<span class="property-type">string</span> <span class="property-type">string</span>
</dt> </dt>
<dd> Reason for kicking the user.</dd> <dd> Reason for kicking the user. If configured on the node it will overwrite this input</dd>
</dl> </dl>
<h3>Outputs</h3> <h3>Outputs</h3>

View File

@ -7,11 +7,13 @@ 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.roomId = n.roomId; this.roomId = n.roomId;
this.reason = n.reason;
if (!node.server) { if (!node.server) {
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" });
@ -25,38 +27,43 @@ 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;
} }
node.server.matrixClient.kick(msg.topic, msg.userId, msg.reason || undefined) node.server.matrixClient.kick(msg.topic, msg.userId, n.reason || msg.reason || undefined)
.then(function(e) { .then(function(e) {
node.log("Successfully kicked " + msg.userId + " from " + msg.topic); node.log("Successfully kicked " + msg.userId + " from " + msg.topic);
msg.eventId = e.event_id; msg.eventId = e.event_id;
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);
} }

View 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>

View 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);
}

View File

@ -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() {
@ -20,7 +20,7 @@
<script type="text/html" data-template-name="matrix-room-users"> <script type="text/html" data-template-name="matrix-room-users">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">
@ -30,10 +30,19 @@
<div class="form-row"> <div class="form-row">
<label for="node-input-server"><i class="fa fa-user"></i> Room Id</label> <label for="node-input-server"><i class="fa fa-user"></i> Room Id</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-tips"> <script type="text/javascript">
This only works on Synapse servers. The user also must be an administrator. $(function(){
</div> $("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-room-users"> <script type="text/html" data-help-name="matrix-room-users">

View File

@ -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);
} }

View File

@ -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,8 +20,28 @@
</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="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">
@ -29,8 +49,9 @@
<input type="text" id="node-input-server"> <input type="text" id="node-input-server">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-contentType"><i class="fa fa-user"></i> Content-Type</label> <label for="node-input-contentType"><i class="fa fa-user"></i> Content-Type</label>
@ -39,6 +60,17 @@
<div class="form-tips"> <div class="form-tips">
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 empty 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 empty
</div> </div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-send-file"> <script type="text/html" data-help-name="matrix-send-file">

View File

@ -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);
} }

View File

@ -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,8 +20,28 @@
</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="icon-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">
</div> </div>
<div class="form-row"> <div class="form-row">
@ -29,8 +49,9 @@
<input type="text" id="node-input-server"> <input type="text" id="node-input-server">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-contentType"><i class="fa fa-user"></i> Content-Type</label> <label for="node-input-contentType"><i class="fa fa-user"></i> Content-Type</label>
@ -39,11 +60,22 @@
<div class="form-tips"> <div class="form-tips">
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: image/png) or left empty 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: image/png) or left empty
</div> </div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-send-image"> <script type="text/html" data-help-name="matrix-send-image">
<h3>Details</h3> <h3>Details</h3>
<p>This node will send an image to a Matrix chat room. Supports direct linking to a File In node.</p> <p>This node will send an image to a Matrix chat room. Supports direct linking to a File In node. It's recommended you set <code>msg.contentType</code> so the client knows how to render the image (otherwise the image could show blank).</p>
<h3>Inputs</h3> <h3>Inputs</h3>
<dl class="message-properties"> <dl class="message-properties">

View File

@ -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;
} }
@ -72,24 +74,36 @@ module.exports = function(RED) {
}) })
.then(function(file){ .then(function(file){
node.server.matrixClient node.server.matrixClient
.sendImageMessage(msg.topic, file.content_uri, {}, (msg.body || msg.filename) || "") .sendImageMessage(
msg.topic,
file.content_uri,
{},
(msg.body || msg.filename) || null,
null
)
.then(function(e) { .then(function(e) {
node.log("Image message sent: " + e); node.log("Image message sent: " + e);
msg.eventId = e.event_id; msg.eventId = e.event_id;
msg.content_uri_mxc = file.content_uri;
msg.content_uri = node.server.matrixClient.mxcUrlToHttp(file.content_uri);
node.send([msg, null]); node.send([msg, null]);
}) })
.catch(function(e){ .catch(function(e){
node.warn("Error sending image message " + e); node.warn("Error sending image message: " + e);
msg.error = e; msg.error = e;
node.send([null, msg]); node.send([null, msg]);
}); });
}) })
.catch(function(e){ .catch(function(e){
node.warn("Error uploading image message " + e); node.warn("Error uploading image 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-image", MatrixSendImage); RED.nodes.registerType("matrix-send-image", MatrixSendImage);
} }

View File

@ -6,23 +6,38 @@
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 },
messageType: { value: 'm.text' }, messageType: { value: 'm.text' },
messageFormat: { value: '' }, messageFormat: { value: '' },
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>
<script type="text/html" data-template-name="matrix-send-message"> <script type="text/html" data-template-name="matrix-send-message">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>
@ -32,8 +47,28 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-roomId"><i class="fa fa-user"></i> Room ID</label> <label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
<input type="text" id="node-input-roomId" placeholder="msg.topic"> <input type="text" id="node-input-roomId" placeholder="msg.topic">
<pre class="form-tips" id="node-input-roomId-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;margin-top: 12px;display:none;"></pre>
</div>
<div class="form-row">
<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>
</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">
<input
type="checkbox"
id="node-input-replaceMessage"
style="width: auto; margin-left: 105px; vertical-align: top"
/>
<label for="node-input-replaceMessage" style="width: auto;max-width:50%;">
Update existing message if <code>msg.eventId</code> is set
</label>
</div> </div>
<div class="form-row"> <div class="form-row">
@ -50,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
@ -60,6 +103,17 @@
<option value="msg.format">msg.format input</option> <option value="msg.format">msg.format input</option>
</select> </select>
</div> </div>
<script type="text/javascript">
$(function(){
$("#node-input-roomId").on("keyup", function() {
if($(this).val() && !$(this).val().startsWith("!")) {
$("#node-input-roomId-error").html(`Room IDs start with exclamation point "!"<br />Example: !OGEhHVWSdvArJzumhm:matrix.org`).show();
} else {
$("#node-input-roomId-error").hide();
}
}).trigger('keyup');
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-send-message"> <script type="text/html" data-help-name="matrix-send-message">
@ -74,14 +128,19 @@
<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. </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
<span class="property-type">bool</span>
</dt>
<dd> If true and <code>msg.eventId</code> is present it will update an existing message. Posts a new message if false or <code>msg.eventId</code> is missing. </dd>
<dt class="optional">msg.formatted_payload <dt class="optional">msg.formatted_payload
<span class="property-type">string</span> <span class="property-type">string</span>
</dt> </dt>
<dd> the formatted HTML message (uses msg.payload if not defined). This only affects HTML messages.</dd> <dd> the formatted HTML message (uses <code>msg.payload</code> if not defined). This only affects HTML messages.</dd>
<dt class="optional">msg.type <dt class="optional">msg.type
<span class="property-type">string | null</span> <span class="property-type">string | null</span>

View File

@ -1,7 +1,8 @@
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;
@ -9,6 +10,10 @@ module.exports = function(RED) {
this.roomId = n.roomId; this.roomId = n.roomId;
this.messageType = n.messageType; this.messageType = n.messageType;
this.messageFormat = n.messageFormat; this.messageFormat = n.messageFormat;
this.replaceMessage = n.replaceMessage;
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 = [
@ -50,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" });
@ -61,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");
@ -87,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;
} }
@ -98,36 +112,100 @@ module.exports = function(RED) {
return; return;
} }
if(!msg.payload) { let payload = n.message || msg.payload;
node.error('msg.payload is required'); if(!payload) {
node.error('msg.payload must be defined or the message configured on the node.', msg);
return; return;
} }
let content = { let content = null;
msgtype: msgType, if (typeof payload === 'object') {
body: msg.payload.toString() 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 === 'html') { if (msgFormat === 'msg.format') {
content.format = "org.matrix.custom.html"; if (!Object.hasOwn(msg, 'format')) {
content.formatted_body = node.error("Message format is set to be passed in via msg.format but was not defined", msg);
(typeof msg.formatted_payload !== 'undefined' && msg.formatted_payload) return;
? msg.formatted_payload.toString() }
: msg.payload.toString(); msgFormat = msg.format;
}
content = {
msgtype: msgType,
body: payload.toString()
};
if (msgFormat === 'html') {
content.format = "org.matrix.custom.html";
content.formatted_body =
(typeof msg.formatted_payload !== 'undefined' && msg.formatted_payload)
? msg.formatted_payload.toString()
: payload.toString();
}
if ((node.replaceMessage || msg.replace) && msg.eventId) {
content['m.new_content'] = {
msgtype: content.msgtype,
body: content.body
};
if ('format' in content) {
content['m.new_content']['format'] = content['format'];
}
if ('formatted_body' in content) {
content['m.new_content']['formatted_body'] = content['formatted_body'];
}
content['m.relates_to'] = {
rel_type: RelationType.Replace,
event_id: msg.eventId
};
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: " + msg.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);
} };

View File

@ -1,18 +1,43 @@
<style>
.matrix-loader {
border: 8px solid #f3f3f3;
border-radius: 50%;
border-top: 8px solid #3498db;
border-bottom: 8px solid #3498db;
width: 20px;
height: 20px;
-webkit-animation: matrix-spin 2s linear infinite;
animation: matrix-spin 2s linear infinite;
}
@-webkit-keyframes matrix-spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes matrix-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<script type="text/javascript"> <script type="text/javascript">
RED.nodes.registerType('matrix-server-config',{ RED.nodes.registerType('matrix-server-config',{
category: 'config', category: 'config',
color: '#00b7ca', color: '#00b7ca',
credentials: { credentials: {
userId: { type: "text", required: true }, userId: { type: "text", required: true },
deviceLabel: { type: "text", required: false },
accessToken: { type: "password", required: true }, accessToken: { type: "password", required: true },
deviceId: { type: "text", required: true }, deviceId: { type: "text", required: false },
url: { type: "text", required: true } url: { type: "text", required: true },
}, },
defaults: { defaults: {
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() {
@ -26,28 +51,51 @@
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label> <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name"> <input type="text" id="node-config-input-name" placeholder="Name">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-userId"><i class="fa fa-server"></i> User ID</label> <label for="node-config-input-deviceLabel"><i class="fa fa-tag"></i> Device Label</label>
<input type="text" placeholder="Node-RED" id="node-config-input-deviceLabel">
</div>
<div class="form-row">
<label for="node-config-input-url"><i class="fa fa-server"></i> Server URL</label>
<input type="text" placeholder="https://matrix.org" id="node-config-input-url">
</div>
<div class="form-row">
<label for="node-config-input-userId"><i class="fa fa-user"></i> User ID</label>
<input type="text" placeholder="@example:matrix.org" id="node-config-input-userId"> <input type="text" placeholder="@example:matrix.org" id="node-config-input-userId">
</div> </div>
<div class="form-row">
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
<input type="password" placeholder="" id="node-config-input-password">
</div>
<div class="form-tips" style="margin-bottom: 12px;">
Password is never saved and is only used to fetch an access token using the button below.
</div>
<pre class="form-tips" id="matrix-chat-login-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;display:none;"></pre>
<pre class="form-tips" id="matrix-chat-login-success" style="color: #155724;background-color: #d4edda;border-color: #c3e6cb;margin-bottom: 12px;display:none;"></pre>
<button class="ui-button ui-corner-all ui-widget primary" id="matrix-login-btn">Update Access Token</button>
<div class="matrix-loader" id="matrix-access-token-loader" style="display:none;"></div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label> <label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label>
<input type="text" id="node-config-input-accessToken"> <input type="text" id="node-config-input-accessToken">
</div> </div>
<div class="form-tips" style="margin-bottom: 12px;"> <div class="form-tips" style="margin-bottom: 12px;">
View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token. You can also generate them using the Shared Secret Registration node. You can either provide/generate an access token yourself or use the login button above to do it automatically. View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here.
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-deviceId"><i class="fa fa-key"></i> Device ID</label> <label for="node-config-input-deviceId"><i class="fa fa-desktop"></i> Device ID</label>
<input type="text" id="node-config-input-deviceId"> <input type="text" id="node-config-input-deviceId">
</div> </div>
<div class="form-tips" style="margin-bottom: 12px;"> <div class="form-tips" style="margin-bottom: 12px;">
This can either be an existing Device ID attached to the above Access Token or you can enter a unique value to set a new one. If you have a Device ID for this auth token you can place it here, or you can leave it blank to have one automatically generated (this field will always be blank if auto generated as it is saved internally). DO NOT manually set one, this must be generated by the server.<br />
WARNING: If you change this after the client has already initialized you will break encryption. Your Device ID is tied to your encryption keys.
</div> </div>
<div class="form-row">
<label for="node-config-input-url"><i class="fa fa-globe"></i> Server URL</label>
<input type="text" placeholder="https://matrix.org" id="node-config-input-url">
</div>
<div class="form-row"> <div class="form-row">
<input <input
type="checkbox" type="checkbox"
@ -65,7 +113,7 @@
style="width: auto; margin-left: 125px; vertical-align: top" style="width: auto; margin-left: 125px; vertical-align: top"
/> />
<label for="node-config-input-enableE2ee" style="width: auto;max-width:50%;"> <label for="node-config-input-enableE2ee" style="width: auto;max-width:50%;">
Enable end-to-end encryption (requires a Device ID to be set.) Enable end-to-end encryption
</label> </label>
</div> </div>
@ -79,10 +127,93 @@
Global access to Matrix Client Global access to Matrix Client
</label> </label>
<div class="form-tips" style="margin-bottom: 12px;"> <div class="form-tips" style="margin-bottom: 12px;">
If enabled this allows you to access the matrix client directly with a Function node. This way you can do <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command" target="_blank">whatever you want</a> with the client. Example:<br> If global access is enabled you can access the client directly within a Function node. This way you can do <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command" target="_blank">whatever you want</a> with the client. Example:<br>
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code> <code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
</div> </div>
</div> </div>
<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">
$("#matrix-login-btn").on("click", function() {
function prettyPrintJson(json) {
try{
return typeof json === 'object' ? JSON.stringify(json, null, 2) : json;
}
catch (error){
return json;
}
}
let userId = $("#node-config-input-userId").val(),
userPassword = $("#node-config-input-password").val(),
serverUrl = $("#node-config-input-url").val();
if(!userId) {
alert("User ID is required to fetch access token.");
return;
}
if(!userPassword) {
alert("Password is required to fetch access token.");
return;
}
if(!serverUrl) {
alert("Server URL is required to fetch access token.");
return;
}
$("#matrix-login-btn, #matrix-chat-login-error, #matrix-chat-login-success").hide();
$("#matrix-access-token-loader").show();
$.ajax({
type: 'POST',
url: 'matrix-chat/login',
dataType: 'json',
data: {
'userId': userId,
'password': userPassword,
'baseUrl': serverUrl,
'displayName': $("#node-config-input-deviceLabel").val(),
}
}).then(
function(data) {
if(data.result && data.result === 'ok') {
$("#matrix-chat-login-error").hide();
$("#matrix-chat-login-success")
.html("Login Successful! Auth Token and Device ID have been set below.")
.show();
$("#node-config-input-accessToken").val(data.token);
$("#node-config-input-deviceId").val(data.device_id);
} else if(data.result && data.result === 'error') {
$("#matrix-chat-login-success").hide();
$("#matrix-chat-login-error")
.html(data.message ? ('Failed to login: <br />' + prettyPrintJson(data.message)) : 'Failed to login')
.show();
}
$("#matrix-login-btn").show();
$("#matrix-access-token-loader").hide();
}, function() {
$("#matrix-chat-login-success").hide();
$("#matrix-chat-login-error")
.html("Failed to login due to server error communicating with Node-RED")
.show();
$("#matrix-login-btn").show();
$("#matrix-access-token-loader").hide();
}
);
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-server-config"> <script type="text/html" data-help-name="matrix-server-config">

View File

@ -1,49 +1,109 @@
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 { LocalStorage } = require('node-localstorage'); const { LocalStorage } = require('node-localstorage');
const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store'); require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it
if (!globalThis.fetch) {
// polyfill fetch if we don't have it
if (!globalThis.fetch) {
import('node-fetch').then(({ default: fetch, Headers, Request, Response }) => {
Object.assign(globalThis, { fetch, Headers, Request, Response });
});
}
}
module.exports = function(RED) { module.exports = function(RED) {
// disable logging if set to "off"
let loggingSettings = RED.settings.get('logging');
if (
typeof loggingSettings.console !== 'undefined' &&
typeof loggingSettings.console.level !== 'undefined' &&
['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0
) {
import('matrix-js-sdk/lib/logger.js')
.then(({ logger }) => {
logger.disableAll();
})
.catch((err) => {
console.error("Error loading logger module:", err);
});
}
function MatrixFolderNameFromUserId(name) { function MatrixFolderNameFromUserId(name) {
return name.replace(/[^a-z0-9]/gi, '_').toLowerCase(); return name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
} }
function MatrixServerNode(n) { function MatrixServerNode(n) {
let storageDir = './matrix-client-storage'; let node = this,
storageDir = RED.settings.userDir + '/matrix-client-storage';
// we should add support for getting access token automatically from username/password
// ref: https://matrix.org/docs/guides/usage-of-the-matrix-js-sdk#login-with-an-access-token
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
node.setMaxListeners(1000);
let node = this;
node.log("Initializing Matrix Server Config node"); node.log("Initializing Matrix Server Config node");
if(!this.credentials) { if(!this.credentials) {
this.credentials = {}; this.credentials = {};
} }
node.setMaxListeners(1000); 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;
this.deviceLabel = this.credentials.deviceLabel || null;
this.deviceId = this.credentials.deviceId || null; this.deviceId = this.credentials.deviceId || null;
this.url = this.credentials.url; this.url = this.credentials.url;
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
this.enableE2ee = n.enableE2ee || false; this.e2ee = n.enableE2ee || false;
this.e2ee = (this.enableE2ee && this.deviceId);
this.globalAccess = n.global; this.globalAccess = n.global;
this.allowUnknownDevices = n.allowUnknownDevices || false;
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) {
node.log("Matrix connection failed: missing user ID in configuration.");
return;
}
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId),
localStorage = new LocalStorage(localStorageDir),
initialSetup = false;
let retryStartTimeout = null;
if(!this.credentials.accessToken) { if(!this.credentials.accessToken) {
node.log("Matrix connection failed: missing access token."); node.error("Matrix connection failed: missing access token in configuration.", {});
} else if(!this.url) { } else if(!this.url) {
node.log("Matrix connection failed: missing server URL."); node.error("Matrix connection failed: missing server URL in configuration.", {});
} else if(!this.userId) {
node.log("Matrix connection failed: missing user ID.");
} else { } else {
node.setConnected = function(connected, cb) { node.setConnected = async function(connected, cb) {
if (node.connected !== connected) { if (node.connected !== connected) {
node.connected = connected; node.connected = connected;
if(typeof cb === 'function') { if(typeof cb === 'function') {
@ -53,6 +113,45 @@ module.exports = function(RED) {
if (connected) { if (connected) {
node.log("Matrix server connection ready."); node.log("Matrix server connection ready.");
node.emit("connected"); node.emit("connected");
if(!initialSetup) {
// store Device ID internally
let stored_device_id = getStoredDeviceId(localStorage),
device_id = this.matrixClient.getDeviceId();
if(!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.", {});
} else {
if(!stored_device_id || stored_device_id !== device_id) {
node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`);
storeDeviceId(localStorage, device_id);
}
// update device label
if(node.deviceLabel) {
node.matrixClient
.getDevice(device_id)
.then(
function(response) {
if(response.display_name !== node.deviceLabel) {
node.matrixClient.setDeviceDetails(device_id, {
display_name: node.deviceLabel
}).then(
function() {},
function(error) {
node.error("Failed to set device label: " + error, {});
}
);
}
},
function(error) {
node.error("Failed to fetch device: " + error, {});
}
);
}
}
initialSetup = true;
}
} else { } else {
node.emit("disconnected"); node.emit("disconnected");
} }
@ -64,32 +163,46 @@ module.exports = function(RED) {
}; };
node.setConnected(false); node.setConnected(false);
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId);
fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist
upgradeDirectoryIfNecessary(node, storageDir); upgradeDirectoryIfNecessary(node, storageDir);
const localStorage = new LocalStorage(localStorageDir);
node.matrixClient = sdk.createClient({ node.matrixClient = sdk.createClient({
baseUrl: this.url, baseUrl: this.url,
accessToken: this.credentials.accessToken, accessToken: this.credentials.accessToken,
sessionStore: new sdk.WebStorageSessionStore(localStorage),
cryptoStore: new LocalStorageCryptoStore(localStorage), cryptoStore: new LocalStorageCryptoStore(localStorage),
store: new MemoryStore({
localStorage: localStorage,
}),
userId: this.userId, userId: this.userId,
deviceId: this.deviceId || undefined, deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined
}); });
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);
} }
node.on('close', function(done) { function stopClient() {
if(node.matrixClient) { if(node.matrixClient && node.matrixClient.clientRunning) {
node.matrixClient.close();
node.matrixClient.stopClient(); node.matrixClient.stopClient();
node.setConnected(false); node.setConnected(false);
} }
if(retryStartTimeout) {
clearTimeout(retryStartTimeout);
}
}
node.on('close', function(done) {
stopClient();
if(node.globalAccess) {
try {
node.context().global.set('matrixClient["'+node.userId+'"]', undefined);
} catch(e) {
node.error(e.message, {});
}
}
done(); done();
}); });
@ -97,10 +210,79 @@ module.exports = function(RED) {
return node.connected; return node.connected;
}; };
node.matrixClient.on("Room.timeline", async function(event, room, toStartOfTimeline, data) { node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) {
node.emit("Room.timeline", event, room, toStartOfTimeline, data); if (toStartOfTimeline) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result");
return;
}
if (!data || !data.liveEvent) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message");
return;
}
if(node.initializedAt > event.getDate()) {
node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message before init");
return;
}
try {
await node.matrixClient.decryptEventIfNeeded(event);
} catch (error) {
node.error(error, {});
return;
}
const isDmRoom = (room) => {
// Find out if this is a direct message room.
let isDM = !!room.getDMInviter();
const allMembers = room.currentState.getMembers();
if (!isDM && allMembers.length <= 2) {
// if not a DM, but there are 2 users only
// double check DM (needed because getDMInviter works only if you were invited, not if you invite)
// hence why we check for each member
if (allMembers.some((m) => m.getDMInviter())) {
return true;
}
}
return allMembers.length <= 2 && isDM;
};
let msg = {
encrypted : event.isEncrypted(),
redacted : event.isRedacted(),
content : event.getContent(),
type : (event.getContent()['msgtype'] || event.getType()) || null,
payload : (event.getContent()['body'] || event.getContent()) || null,
isDM : isDmRoom(room),
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,
};
// 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);
}); });
/**
* Fires when we want to suggest to the user that they restore their megolm keys
* from backup or by cross-signing the device.
*
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
*/
// node.matrixClient.on("crypto.suggestKeyRestore", function(){
//
// });
// node.matrixClient.on("RoomMember.typing", async function(event, member) { // node.matrixClient.on("RoomMember.typing", async function(event, member) {
// let isTyping = member.typing; // let isTyping = member.typing;
// let roomId = member.roomId; // let roomId = member.roomId;
@ -118,21 +300,35 @@ module.exports = function(RED) {
// }); // });
// handle auto-joining rooms // handle auto-joining rooms
node.matrixClient.on("RoomMember.membership", async function(event, member) {
node.matrixClient.on(RoomMemberEvent.Membership, async function(event, member) {
if(node.initializedAt > event.getDate()) {
return; // skip events that occurred before our client initialized
}
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);
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);
}).catch(function(e) { }).catch(function(e) {
node.warn("Cannot join room (could be from being kicked/banned) " + member.roomId + ": " + e); node.warn("Cannot join room (could be from being kicked/banned) " + member.roomId + ": " + e);
}); });
} else {
node.log("Got invite to join room " + member.roomId);
} }
let room = node.matrixClient.getRoom(event.getRoomId());
node.emit("Room.invite", {
type : 'm.room.member',
userId : event.getSender(),
topic : event.getRoomId(),
topicName : (room ? room.name : null) || null,
event : event,
eventId : event.getId(),
});
} }
}); });
node.matrixClient.on("sync", async function(state, prevState, data) { node.matrixClient.on(ClientEvent.Sync, async function(state, prevState, data) {
node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]"); node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]");
if(prevState === null && state === "PREPARED" ) { if(prevState === null && state === "PREPARED" ) {
// Occurs when the initial sync is completed first time. // Occurs when the initial sync is completed first time.
@ -143,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
@ -160,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
@ -183,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.
@ -195,12 +391,12 @@ 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("Session.logged_out", async function(errorObj){ node.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){
// Example if user auth token incorrect: // Example if user auth token incorrect:
// { // {
// errcode: 'M_UNKNOWN_TOKEN', // errcode: 'M_UNKNOWN_TOKEN',
@ -212,7 +408,8 @@ module.exports = function(RED) {
// httpStatus: 401 // httpStatus: 401
// } // }
node.error("[Session.logged_out] " + errorObj); node.error("Authentication failure: " + errorObj, {});
stopClient();
}); });
async function run() { async function run() {
@ -220,32 +417,131 @@ 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({ initialSyncLimit: 8 }); await node.matrixClient.startClient({
} catch(error){ initialSyncLimit: node.initialSyncLimit
node.error(error); });
} catch(error) {
node.error(error, {});
} }
} }
run().catch((error) => node.error(error)); // do an authed request and only continue if we don't get an error
// this prevent the matrix client from crashing Node-RED on invalid auth token
(function checkAuthTokenThenStart() {
if(node.matrixClient.clientRunning) {
return;
}
/**
* We do a /whoami request before starting for a few reasons:
* - validate our auth token
* - make sure auth token belongs to provided node.userId
* - fetch device_id if possible (only available on Synapse >= v1.40.0 under MSC2033)
*/
node.matrixClient.whoami()
.then(
function(data) {
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.", {});
}
if('device_id' in data && data['device_id'] && !node.deviceId) {
// if we have no device_id configured lets use the one
// returned by /whoami for this access_token
node.matrixClient.deviceId = data['device_id'];
}
// make sure our userId matches the access token's
if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) {
node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`, {});
return;
}
run().catch((error) => node.error(error));
},
function(err) {
// if the error isn't authentication related retry in a little bit
if(err.code !== "M_UNKNOWN_TOKEN") {
retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000);
node.error("Auth check failed: " + err, {});
}
}
);
})();
} }
} }
RED.nodes.registerType("matrix-server-config", MatrixServerNode, { RED.nodes.registerType("matrix-server-config", MatrixServerNode, {
credentials: { credentials: {
userId: { type:"text", required: true }, deviceLabel: { type: "text", required: false },
accessToken: { type:"text", required: true }, userId: { type: "text", required: true },
deviceId: { type: "text", required: true }, accessToken: { type: "text", required: true },
url: { type: "text", required: true }, deviceId: { type: "text", required: false },
url: { type: "text", required: true }
} }
}); });
function upgradeDirectoryIfNecessary(node, storageDir) { RED.httpAdmin.post(
let oldStorageDir = './matrix-local-storage'; "/matrix-chat/login",
RED.auth.needsPermission('flows.write'),
function(req, res) {
let userId = req.body.userId || undefined,
password = req.body.password || undefined,
baseUrl = req.body.baseUrl || undefined,
deviceId = req.body.deviceId || undefined,
displayName = req.body.displayName || undefined;
// if the old storage location exists lets move it to it's new location (async () => {
const mod = await import("matrix-js-sdk");
const matrixClient = mod.createClient({
baseUrl: baseUrl,
deviceId: deviceId,
timelineSupport: true,
localTimeoutMs: '30000'
});
new TimelineWindow(); // from our top-level variable, but to keep minimal changes,
// 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,
},
password: password,
initial_device_display_name: displayName
})
.then(
function(response) {
res.json({
'result': 'ok',
'token': response.access_token,
'device_id': response.device_id,
'user_id': response.user_id,
});
},
function(err) {
res.json({
'result': 'error',
'message': err
});
}
);
})().catch(err => {
res.json({ result: 'error', message: err.toString() });
});
}
);
function upgradeDirectoryIfNecessary(node, storageDir) {
let oldStorageDir = './matrix-local-storage',
oldStorageDir2 = './matrix-client-storage';
// if the old storage location exists lets move it to the new location
if(fs.pathExistsSync(oldStorageDir)){ if(fs.pathExistsSync(oldStorageDir)){
RED.nodes.eachNode(function(n){ RED.nodes.eachNode(function(n){
try { try {
@ -258,7 +554,7 @@ module.exports = function(RED) {
fs.copySync(oldStorageDir, dir); fs.copySync(oldStorageDir, dir);
} }
} catch (err) { } catch (err) {
console.error(err) node.error(err, {});
} }
}); });
@ -266,5 +562,33 @@ module.exports = function(RED) {
node.log("archiving old config folder '" + oldStorageDir + "' to '" + oldStorageDir + "-backup"); node.log("archiving old config folder '" + oldStorageDir + "' to '" + oldStorageDir + "-backup");
fs.renameSync(oldStorageDir, oldStorageDir + "-backup"); fs.renameSync(oldStorageDir, oldStorageDir + "-backup");
} }
if(RED.settings.userDir !== resolve('./') && resolve(oldStorageDir2) !== resolve(storageDir)) {
// user directory does not match running directory
// check if we stored stuff in wrong directory and move it
if(fs.pathExistsSync(oldStorageDir2)){
fs.ensureDirSync(storageDir);
node.log("found old '" + oldStorageDir2 + "' path, copying to new location '" + storageDir);
fs.copySync(oldStorageDir2, storageDir);
// rename folder to keep as a backup (and so we don't run again)
fs.renameSync(oldStorageDir2, oldStorageDir2 + "-backup");
}
}
} }
}
function getStoredDeviceId(localStorage) {
let deviceId = localStorage.getItem('my_device_id');
if(deviceId === "null" || !deviceId) {
return null;
}
return deviceId;
}
function storeDeviceId(localStorage, deviceId) {
if(!deviceId) {
return false;
}
localStorage.setItem('my_device_id', deviceId);
return true;
}
};

View File

@ -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";
@ -19,7 +19,7 @@
<script type="text/html" data-template-name="matrix-synapse-create-edit-user"> <script type="text/html" data-template-name="matrix-synapse-create-edit-user">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>

View File

@ -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,18 +42,18 @@ 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;
} }
node.server.matrixClient.http node.server.matrixClient.http
.authedRequest( .authedRequest(
undefined,
'PUT', 'PUT',
node.encodeUri( node.encodeUri(
"/_synapse/admin/v2/users/$userId", "/_synapse/admin/v2/users/$userId",
@ -62,13 +63,17 @@ module.exports = function(RED) {
msg.payload, msg.payload,
{ prefix: '' } { prefix: '' }
).then(function(e){ ).then(function(e){
msg.payload = e; msg.payload = e;
node.send([msg, null]); node.send([msg, null]);
}).catch(function(e){ }).catch(function(e){
node.warn("Error creating/editing user " + e); node.warn("Error creating/editing user " + 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-synapse-create-edit-user", MatrixSynapseCreateEditUser); RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser);

View File

@ -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";
@ -19,7 +19,7 @@
<script type="text/html" data-template-name="matrix-synapse-deactivate-user"> <script type="text/html" data-template-name="matrix-synapse-deactivate-user">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="icon-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">
</div> </div>

Some files were not shown because too many files have changed in this diff Show More