Compare commits
102 Commits
3e70369cae
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| af1067a99b | |||
| 390a5b264e | |||
| 9dc4362819 | |||
| 1801b49fae | |||
| 72cc9cfb3b | |||
| b8652a3a9a | |||
| 0362fe81e8 | |||
| 9ccbe4526f | |||
| 0c48db92a0 | |||
| 3f69614ed0 | |||
| e3a23df6d6 | |||
| 3e34dc5961 | |||
| 7af8891d5b | |||
| 939fe42b40 | |||
| 67920840e1 | |||
| ebcb1eab81 | |||
| 68e63e5def | |||
| 99909a77c3 | |||
| aadd82d820 | |||
| 4e6fa50a67 | |||
| 58bf2dcb54 | |||
| c15893bab5 | |||
| f0af0e92fe | |||
| 04de0b4eb3 | |||
| 8cb52112c1 | |||
| 54a9972bbc | |||
| ad34f018ab | |||
| 20345787d2 | |||
| 99c19923c6 | |||
| 093d59893e | |||
| 913f5dfcb9 | |||
| e0947dd3bc | |||
| 8287f3c08a | |||
| 2a78524a90 | |||
| d01838ac84 | |||
| 2059f8455d | |||
| 0cb8ecf8aa | |||
| 77f2c4be46 | |||
| cf82daf5da | |||
| 487241e097 | |||
| 3b161f1ad9 | |||
| 45ff930518 | |||
| 9e3b66f4aa | |||
| 02826e2769 | |||
| 6bbd1d5119 | |||
| 5de1274def | |||
| 1b54bc03eb | |||
| 65edc94854 | |||
| 351679ad77 | |||
| 2b2da4faf7 | |||
| 5090e4fbb6 | |||
| 51e649b4cf | |||
| a08709265e | |||
| 57ba70db6c | |||
| a3e1381d53 | |||
| 6dca3aa70e | |||
| b36286d994 | |||
| f14190d9ea | |||
| 000c28e3b8 | |||
| fd174e32ff | |||
| e8506d8887 | |||
| d7c4bc26bb | |||
| 785e0cd7be | |||
| 2e9633e113 | |||
| 1859696122 | |||
| fd605005d1 | |||
| 85de450a1a | |||
| e7e0f2967b | |||
| 611e23b845 | |||
| 9d050a0d44 | |||
| c833a40a84 | |||
| 0e755bc350 | |||
| c920dd12cb | |||
| 9661922f78 | |||
| 20c7182511 | |||
| f48ba74a72 | |||
| 124a0cba34 | |||
| 8ca11f36d8 | |||
| 8a7fba39e8 | |||
| c61eadd05d | |||
| 78f8ab7abb | |||
| ce8be4a30f | |||
| 3e808cabec | |||
| 2fdc7482ce | |||
| c7f9d56df2 | |||
| 3c042ae47d | |||
| 0a34870fa3 | |||
| 768a1c8ce0 | |||
| 22dd9b4ca3 | |||
| 462f9670c2 | |||
| e4b01c40c2 | |||
| 908d60835d | |||
| 4c17a21008 | |||
| bd4f6ea486 | |||
| 5ef0b6a11f | |||
| 97f27e61c6 | |||
| 7bdadc0fe9 | |||
| 5f129560aa | |||
| 00bc14e1c7 | |||
| 9f41b67174 | |||
| 4e93b7253e | |||
| ecb4427217 |
@@ -0,0 +1,81 @@
|
||||
name: Publish to npm
|
||||
|
||||
# Publishes the package to npm whenever a GitHub Release is published.
|
||||
# It publishes the exact commit the release tag points to, so a pre-release
|
||||
# can be cut from any branch (e.g. a beta off `dev`) without that branch
|
||||
# having to be merged into master first.
|
||||
#
|
||||
# The release tag is the source of truth for the version:
|
||||
# - Stable tag (e.g. v1.2.3) -> published to the "latest"
|
||||
# dist-tag; the version bump is
|
||||
# committed back to master.
|
||||
# - Pre-release tag (e.g. v1.2.3-beta.1) -> published to a matching dist-tag
|
||||
# ("beta", "rc", ...); does NOT
|
||||
# become "latest" and is NOT
|
||||
# committed back to master.
|
||||
#
|
||||
# Authentication uses npm Trusted Publishing (OIDC) - no token or secret is
|
||||
# needed. Configure a trusted publisher for this package on npmjs.com:
|
||||
# Repository: Skylar-Tech/node-red-contrib-matrix-chat
|
||||
# Workflow: publish.yml
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # commit the version bump back to master
|
||||
id-token: write # npm Trusted Publishing (OIDC) + provenance
|
||||
steps:
|
||||
- name: Check out the released commit
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Update npm
|
||||
# Trusted Publishing requires npm 11.5.1 or newer; Node 22 ships npm 10.
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Determine version and dist-tag
|
||||
id: ver
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
# pre-release, e.g. 1.0.0-beta.1 -> dist-tag "beta"
|
||||
DIST_TAG="${VERSION#*-}"
|
||||
DIST_TAG="${DIST_TAG%%.*}"
|
||||
PRERELEASE=true
|
||||
else
|
||||
DIST_TAG=latest
|
||||
PRERELEASE=false
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing $VERSION to npm dist-tag '$DIST_TAG' (prerelease=$PRERELEASE)"
|
||||
|
||||
- name: Set version
|
||||
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --provenance --access public --tag "${{ steps.ver.outputs.dist_tag }}"
|
||||
|
||||
- name: Commit version bump back to master
|
||||
if: steps.ver.outputs.prerelease == 'false'
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "package.json already at ${{ steps.ver.outputs.version }}; nothing to commit."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -am "Set version to ${{ steps.ver.outputs.version }}"
|
||||
git push origin HEAD:master \
|
||||
|| echo "::warning::Could not push the version bump to master (branch protection?). The package was still published."
|
||||
@@ -1,73 +1,81 @@
|
||||
# node-red-contrib-matrix-chat
|
||||
Matrix chat server client for [Node-RED](https://nodered.org/)
|
||||
[Matrix](https://matrix.org/) chat client for [Node-RED](https://nodered.org/), with full end-to-end encryption support.
|
||||
|
||||
***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.***
|
||||
Please report any issues in our [issue tracker](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/issues). Breaking changes between releases are listed in the [release notes](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/releases); check them before upgrading.
|
||||
|
||||
If you need help with this feel free to join our public matrix room at [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech)
|
||||
Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech)
|
||||
|
||||
[](https://ko-fi.com/B0B51BM7C)
|
||||
|
||||
### Features
|
||||
|
||||
The following is supported from this package:
|
||||
Supported functionality in this package includes:
|
||||
|
||||
- End-to-end encryption
|
||||
- [Currently a WIP](#end-to-end-encryption-notes)
|
||||
- Receive events from a room (messages, reactions, images, and files) whether encrypted or not
|
||||
- Send Images/Files (sending files to e2ee room doesn't currently encrypt them yet)
|
||||
- Decrypt files in e2ee rooms
|
||||
- Send HTML/Plain Text Message/Notice
|
||||
- React to messages
|
||||
- Register user's on closed registration Synapse servers using `registration_shared_secret` (Admin Only)
|
||||
- List out users on a Synapse server (Admin Only)
|
||||
- Get WhoIs info for a Synapse user (Admin Only)
|
||||
- Add/Edit Synapse users using the v2 API (requires a pre-existing admin account)
|
||||
- Deactivate users on Synapse servers (Admin Only)
|
||||
- Get a user list from a room
|
||||
- Kick user from room
|
||||
- Ban user from room
|
||||
- Join a room
|
||||
- Create a room
|
||||
- Invite to a room
|
||||
- Synapse admin API to force add user to room (requires bot to be in same room already)
|
||||
- **End-to-end encryption (E2EE)**: send and receive encrypted messages (see the [encryption notes](#end-to-end-encryption-notes))
|
||||
- **Cross-signing & secure backup**: interactive setup from the server config node so the bot's own device shows as verified
|
||||
- **Device verification**: interactive SAS (emoji) verification, either from the server config node or with the `matrix-verification` flow nodes
|
||||
- **Session management**: review, verify, rename, or remove the account's sessions from the server config node (Element-style)
|
||||
- **Receive events** from rooms (encrypted or unencrypted): messages, reactions, emotes, notices, stickers, images, video, audio, locations, files
|
||||
- **Fetch/modify room state**: Update room settings
|
||||
- **Paginate room history**
|
||||
- **Send files** to rooms, encrypted or unencrypted
|
||||
- **Send & modify messages** (plain text, Markdown, and HTML formats)
|
||||
- **Send location messages**: produce `m.location` events that match Element's *"Share my location"* wire format
|
||||
- **Send typing notifications**
|
||||
- **Delete events** (messages, reactions, etc.)
|
||||
- **Decrypt files** in E2EE rooms
|
||||
- **React to messages**
|
||||
- **Admin tools**:
|
||||
- Register users on closed Synapse servers (`registration_shared_secret`)
|
||||
- Manage users, including listing, adding, editing, deactivating (Synapse API)
|
||||
- Force-add users to rooms
|
||||
- **Room management**: Invite, kick, ban, join, create, and leave rooms
|
||||
|
||||
|
||||
Therefore, you can easily build a bot, chat relay, or administrate your Matrix server from within [Node-RED](https://nodered.org/).
|
||||
These features allow you to easily build bots, set up chat relays, or even administrate your Matrix server directly from [Node-RED](https://nodered.org/).
|
||||
|
||||
### Installing
|
||||
|
||||
You can either install from within Node-RED by searching for `node-red-contrib-matrix-chat` or run this from within your Node-RED directory:
|
||||
**Requires Node.js 22 or newer** (this is a requirement of the bundled `matrix-js-sdk`).
|
||||
|
||||
Install through Node-RED's UI by searching for `node-red-contrib-matrix-chat`, or use the following command inside your Node-RED directory:
|
||||
|
||||
```bash
|
||||
npm install node-red-contrib-matrix-chat
|
||||
```
|
||||
|
||||
### Usage
|
||||
We have examples! [Check them out](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme)
|
||||
|
||||
#### Extra functionality
|
||||
You are not limited by just the nodes we have created. If you turn on global access when setting up your Matrix Client you can access the client directly from any function node to write your own logic.
|
||||
Explore our [examples](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) to see the module in action.
|
||||
|
||||
View an example [here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command)
|
||||
#### Extending functionality
|
||||
|
||||
You're not limited to just the nodes we've created. Enable global access in your Matrix Client to directly interact with the client from function nodes and create custom logic.
|
||||
|
||||
[View an example here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command).
|
||||
|
||||
### End-to-End Encryption Notes
|
||||
It is recommended you use the bot exclusively with Node-RED after it's creation if using e2ee. 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.
|
||||
|
||||
We now have a device verification node that will help in sharing keys (check the [examples](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) for more info). This node is currently in beta and is still experimental.
|
||||
- E2EE uses the Rust crypto stack from `matrix-js-sdk`. The first time a bot starts after upgrading from an older version, any existing (legacy libolm) crypto state is migrated automatically.
|
||||
- **Storage:** E2EE state is saved in a folder called `matrix-client-storage` within your Node-RED directory. Each account's Rust crypto store is persisted there as `rust-crypto-store.v8` (snapshotted on shutdown and every 5 minutes). Setting up secure backup (below) lets you recover the account's keys even if this folder is lost.
|
||||
- **Cross-signing & secure backup (strongly recommended):** open the server config node and use the **Set up secure backup & cross-signing** button. It lets you unlock an existing secure backup with its recovery key, or create a fresh one; once done, the bot's own device is cross-signed and shows as verified to others. **Save the recovery key somewhere safe.** It is shown only once, and is the only way to restore the account's encryption keys if the crypto store is ever lost.
|
||||
- **Device verification:** there are two ways to verify devices:
|
||||
- From the server config node, the **Pending verification requests** button opens a list of incoming requests and lets you complete the SAS (emoji) check interactively, no flow required.
|
||||
- Or build your own flow: the `matrix-verification` node emits verification requests and phase changes, and `matrix-verification-action` accepts, starts, confirms, or cancels them (e.g. emailing the SAS emoji for a human to confirm). See the [device verification example](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#device-verification).
|
||||
|
||||
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).
|
||||
### Registering a User
|
||||
|
||||
Want to contribute? Any help on getting the last pieces of e2ee figured out would be greatly appreciated :)
|
||||
|
||||
### Generate user
|
||||
You will need a user to use this module. Luckily this module comes with a node that allows you to register users to a homeserver using the secret registration endpoint. This is perfect because it returns an `access_token` as well as a `device_id` which is exactly what we need.
|
||||
|
||||
[Click here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) to see how to generate a user using secret registration
|
||||
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
|
||||
|
||||
- [node-red-contrib-gamedig](https://www.npmjs.com/package/node-red-contrib-gamedig) - Query game servers from Node-RED!
|
||||
- [node-red-contrib-gamedig](https://www.npmjs.com/package/node-red-contrib-gamedig) - Query game servers from Node-RED.
|
||||
|
||||
### Contributing
|
||||
All contributions are welcome! If you do add a feature please do a pull request so that everyone benefits :)
|
||||
|
||||
Sharing is caring!
|
||||
We welcome all contributions! Please submit a pull request if you add a feature so the whole community can benefit.
|
||||
|
||||
**Sharing is caring!**
|
||||
|
||||
@@ -1,255 +1,487 @@
|
||||
# Examples
|
||||
These are examples of what is possible with the [node-red-contrib-matrix-chat](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat) module for [Node-RED](https://nodered.org/)
|
||||
|
||||
If you want to try any of them out just copy their JSON contents from their .json file and use the hamburger menu in Node-RED to import the flow.
|
||||
These examples showcase what is possible with the [node-red-contrib-matrix-chat](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat) module for [Node-RED](https://nodered.org/).
|
||||
|
||||
Build something cool with these nodes? Feel free to submit a pull request to share it!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Matrix account
|
||||
- Node-RED set up and running
|
||||
- Installed `node-red-contrib-matrix-chat` module
|
||||
|
||||
## How to Use the Examples
|
||||
|
||||
To try out any of the examples:
|
||||
|
||||
1. Copy the JSON contents from the `.json` file linked in each example.
|
||||
2. In Node-RED, use the menu to import the flow:
|
||||
- Click the hamburger menu (top right corner).
|
||||
- Select **Import**.
|
||||
- Paste the JSON and click **Import**.
|
||||
|
||||
## Index
|
||||
|
||||
- [Create User with Shared Secret Registration](#create-user-with-shared-secret-registration)
|
||||
- [Create/Edit Synapse User](#createedit-synapse-user)
|
||||
- [Use function node to run any command](#use-function-node-to-run-any-command)
|
||||
- [Start and accept device verification from specific user](#start-and-accept-device-verification-from-specific-user)
|
||||
- [Request device verification & immediately accept](#request-device-verification--immediately-accept)
|
||||
- [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)
|
||||
- [Remove messages containing "delete"](#remove-messages-containing-delete)
|
||||
- [Respond to "users" with full list of server users](#respond-to-users-with-full-list-of-server-users)
|
||||
- [Respond to "newroom" by creating new room and inviting user](#respond-to-newroom-by-creating-new-room-and-inviting-user)
|
||||
- [Respond to "joinroom <room_id_or_alias>" by joining mentioned room](#respond-to-joinroom-room_id_or_alias-by-joining-mentioned-room)
|
||||
- [Respond to "rooms <user_id>" with user's rooms (list server's rooms if <user_id> is left blank)](#respond-to-rooms-user_id-with-users-rooms-list-servers-rooms-if-user_id-is-left-blank)
|
||||
- [Respond to "whois <user_id>" with information about the user's session](#respond-to-whois-user_id-with-information-about-the-users-session)
|
||||
- [Respond to "room_users" with current room's users](#respond-to-room_users-with-current-rooms-users)
|
||||
- [Download & store all received files/images](#download--store-all-received-filesimages)
|
||||
- [Kick/Ban user from room](#kickban-user-from-room)
|
||||
- [Deactivate user](#deactivate-user)
|
||||
**Click the ▶ example to ▼ expand**
|
||||
|
||||
### User Management
|
||||
|
||||
### Create user with Shared Secret Registration
|
||||
<details>
|
||||
<summary>Get or set current user display name</summary>
|
||||
|
||||
[View JSON](get-set-displayname.json)
|
||||
|
||||
This flow lets you get or set the displayname for the current user.
|
||||
|
||||

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

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

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Create User with Shared Secret Registration</summary>
|
||||
|
||||
[View JSON](shared-secret-registration.json)
|
||||
|
||||
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.
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
### Use function node to run any command
|
||||
|
||||
[View JSON](custom-redact-function-node.json)
|
||||
|
||||
If we do not have a node for something you want to do you can do this manually with a function node. We now have a node for removing events but this is still a good example.
|
||||
|
||||
**Note:** You should make sure to catch any errors in your function node otherwise you could cause Node-RED to crash.
|
||||
|
||||
To view what sort of functions you have access to check out the `client.ts` file from `matrix-js-sdk` [here](https://github.com/matrix-org/matrix-js-sdk/blob/master/src/client.ts).
|
||||
|
||||

|
||||
|
||||
|
||||
### Request device verification & immediately accept
|
||||
|
||||
[View JSON](request-device-verification.json)
|
||||
|
||||
Edit the inject node to match the details of a user & device you would like to request verification from.
|
||||
After the end user starts verification the bot automatically accepts the result (note: you should be validating the result and not just blindly accepting them, this is just an example)
|
||||
|
||||

|
||||
|
||||
|
||||
### Start and accept device verification from specific user
|
||||
|
||||
[View JSON](start-accept-verification-from-user.json)
|
||||
|
||||
Edit the switch node labeled "is from me" to match whatever user ID you would like to accept verification requests from.
|
||||
After verification starts the bot automatically accepts the result (note: you should be validating the result and not just blindly accepting them, this is just an example)
|
||||
|
||||

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

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

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

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

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

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

|
||||
|
||||
|
||||
|
||||
### Respond to "users" with full list of server users
|
||||
|
||||
[View JSON](respond-users-list.json)
|
||||
|
||||
When someone sends the text "users" they get a HTML message back containing all the current users on the server. If your server has a lot of users this paginates and sends a message with 25 users per message.
|
||||
|
||||
This requires admin privileges.
|
||||
|
||||

|
||||
|
||||
|
||||
### Respond to "newroom" by creating new room and inviting user
|
||||
|
||||
[View JSON](respond-users-list.json)
|
||||
|
||||
When someone sends "newroom" a new room will be created and the user that said the message will be invited. The client will also send a welcome message into the new room.
|
||||
|
||||

|
||||
|
||||
|
||||
### Respond to "joinroom <room_id_or_alias>" by joining mentioned room
|
||||
|
||||
[View JSON](respond-joinroom.json)
|
||||
|
||||
When someone sends "newroom" a new room will be created and the user that said the message will be invited. The client will also send a welcome message into the new room.
|
||||
|
||||

|
||||
|
||||
### Respond to "rooms <user_id>" with user's rooms (list server's rooms if <user_id> is left blank)
|
||||
|
||||
[View JSON](respond-rooms.json)
|
||||
|
||||
Responds to "rooms <user_id>" with that user's rooms. If the message is just "rooms" it responds with a list of all rooms the server is participating in.
|
||||
|
||||
Note: If there are a lot of rooms this may fail to send the message as it is too large. This also only works for user's that are on the current server.
|
||||
|
||||
This requires admin privileges.
|
||||
|
||||

|
||||
|
||||
|
||||
### Respond to "whois <user_id>" with information about the user's session
|
||||
|
||||
[View JSON](respond-whois.json)
|
||||
|
||||
This lists out the user's session info. Each session contains the IP address, when it was last seen, and the user agent. Useful to find out more about a specific user on your server.
|
||||
|
||||
Note: If there are a lot of sessions this may fail to send the message as it is too large. This also only works for user's that are on the current server.
|
||||
|
||||
This requires admin privileges.
|
||||
|
||||

|
||||
|
||||
|
||||
### Respond to "room_users" with current room's users
|
||||
|
||||
[View JSON](respond-room-users.json)
|
||||
|
||||
List out the users participating in a room.
|
||||
|
||||
Note: If there are a lot of users in the room this will fail to send due to a large message error.
|
||||
|
||||

|
||||
|
||||
|
||||
### Download & store all received files/images
|
||||
|
||||
[View JSON](store-received-files.json)
|
||||
|
||||
Download received files/images. If the file is encrypted it will decrypt it for you. The decrypt node downloads the file for you otherwise you need to use a HTTP Request node to download the file.
|
||||
|
||||
Note: You may need to edit the storage directory for this to work. Default action is to create a `downloads` folder in the Node-RED directory and places files in that but there is a good chance your Node-RED instance doesn't have access to write to this directory.
|
||||
|
||||

|
||||
|
||||
|
||||
### Kick/Ban user from room
|
||||
|
||||
[View JSON](room-kick-ban.json)
|
||||
|
||||
If you say "kick @test:example.com" the bot will kick @test:example.com from the current room.
|
||||
|
||||
If you say "ban @test:example.com" the bot will ban @test:example.com from the current room.
|
||||
|
||||
Note: This requires the bot to have permissions to kick/ban in the current room.
|
||||
|
||||

|
||||
|
||||
|
||||
### Deactivate user
|
||||
<details>
|
||||
<summary>Deactivate User</summary>
|
||||
|
||||
[View JSON](deactivate-user.json)
|
||||
|
||||
If you say "deactivate_user @test:example.com" the bot will deactivate the @test:example.com account on the server.
|
||||
If you send "deactivate_user @test:example.com", the bot will deactivate the `@test:example.com` account on the server.
|
||||
|
||||
Note: This requires the bot to be a server admin.
|
||||
**Note:**
|
||||
|
||||
WARNING: Accounts that are deleted cannot be restored. If you want to temp-disable edit the user instead.
|
||||
- This requires the bot to be a server admin.
|
||||
- **WARNING:** Accounts that are deleted cannot be restored. If you want to temporarily disable a user, consider modifying the user instead.
|
||||
|
||||

|
||||

|
||||
|
||||
### Force user to join room
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Force User to Join Room</summary>
|
||||
|
||||
[View JSON](force-join-room.json)
|
||||
|
||||
If you say "force_join @test:example.com !320j90mf0394f:example.com" the bot will force the user `@test:example.com` into room `!320j90mf0394f:example.com`
|
||||
If you send "force_join @test:example.com !320j90mf0394f:example.com", the bot will force the user `@test:example.com` into room `!320j90mf0394f:example.com`.
|
||||
|
||||
Note: This requires the bot to be a server admin. This also only works for rooms on the same server.
|
||||
**Note:**
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Kick/Ban User from Room</summary>
|
||||
|
||||
[View JSON](room-kick-ban.json)
|
||||
|
||||
- If you say "kick @test:example.com", the bot will kick `@test:example.com` from the current room.
|
||||
- If you say "ban @test:example.com", the bot will ban `@test:example.com` from the current room.
|
||||
|
||||
**Note:** This requires the bot to have permissions to kick/ban in the current room.
|
||||
|
||||

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

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

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Respond to "rooms <user_id>" with User's Rooms</summary>
|
||||
|
||||
[View JSON](respond-rooms.json)
|
||||
|
||||
Responds to "rooms <user_id>" with that user's rooms. If the message is just "rooms", it responds with a list of all rooms the server is participating in.
|
||||
|
||||
**Notes:**
|
||||
|
||||
- This requires admin privileges.
|
||||
- If there are many rooms, the message may be too large to send in one piece.
|
||||
- This only works for users on the current server.
|
||||
|
||||

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

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

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

|
||||
|
||||
</details>
|
||||
|
||||
### Device Verification
|
||||
|
||||
<details>
|
||||
<summary>Handle device verification (SAS / emoji)</summary>
|
||||
|
||||
[View JSON](device-verification-flow.json)
|
||||
|
||||
An end-to-end example of interactive device verification. The `matrix-verification` node emits every verification request and phase change; the flow routes by phase, automatically **accepts** incoming requests and **starts SAS**, then surfaces the SAS emoji so a human can compare it. Inject nodes let you **confirm** or **reject** the match, and there are paths to have the bot **request** verification of a specific user's device, or a user in a room.
|
||||
|
||||
Requires end-to-end encryption to be enabled on the server config node. For the bot's own device to be trusted by others, also set up cross-signing via the **Set up secure backup & cross-signing** button on the server config node.
|
||||
|
||||
**Instructions:**
|
||||
|
||||
1. Import the flow and set the Matrix server config on each matrix node.
|
||||
2. Replace the `@CHANGE_ME:example.org` / `CHANGE_ME` placeholders in the "Verify a user" inject nodes if you want to use the bot-initiated paths.
|
||||
3. To verify the bot from another client, start a verification with it, watch the debug sidebar for the `sas` event, compare the emoji, then click the **Confirm SAS match** inject.
|
||||
|
||||

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

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

|
||||
|
||||
</details>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,548 @@
|
||||
[
|
||||
{
|
||||
"id": "7158964bd67edc52",
|
||||
"type": "group",
|
||||
"z": "vtest",
|
||||
"name": "Example verification flow",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"40c105c38054d6db",
|
||||
"83f785d52a61009a",
|
||||
"d51bab8cbf5f247c",
|
||||
"2e543533d49b467c"
|
||||
],
|
||||
"x": 88,
|
||||
"y": 73,
|
||||
"w": 1044,
|
||||
"h": 754
|
||||
},
|
||||
{
|
||||
"id": "40c105c38054d6db",
|
||||
"type": "group",
|
||||
"z": "vtest",
|
||||
"g": "7158964bd67edc52",
|
||||
"name": "Verification request handling",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"mv_all",
|
||||
"dbg_events",
|
||||
"sw_phase",
|
||||
"act_accept",
|
||||
"act_start",
|
||||
"chg_savevid",
|
||||
"dbg_sas",
|
||||
"dbg_done",
|
||||
"dbg_cancelled",
|
||||
"dbg_err"
|
||||
],
|
||||
"x": 114,
|
||||
"y": 99,
|
||||
"w": 992,
|
||||
"h": 342
|
||||
},
|
||||
{
|
||||
"id": "mv_all",
|
||||
"type": "matrix-verification",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "All verifications",
|
||||
"server": null,
|
||||
"phaseRequested": true,
|
||||
"phaseReady": true,
|
||||
"phaseStarted": true,
|
||||
"phaseSas": true,
|
||||
"phaseDone": true,
|
||||
"phaseCancelled": true,
|
||||
"initiatedBy": "any",
|
||||
"verificationType": "any",
|
||||
"selfVerification": "any",
|
||||
"userFilter": "",
|
||||
"roomFilter": "",
|
||||
"x": 220,
|
||||
"y": 180,
|
||||
"wires": [
|
||||
[
|
||||
"dbg_events",
|
||||
"sw_phase"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dbg_events",
|
||||
"type": "debug",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "all verification events",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"x": 500,
|
||||
"y": 140,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "sw_phase",
|
||||
"type": "switch",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "route by phase",
|
||||
"property": "phase",
|
||||
"propertyType": "msg",
|
||||
"rules": [
|
||||
{
|
||||
"t": "eq",
|
||||
"v": "requested",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"t": "eq",
|
||||
"v": "ready",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"t": "eq",
|
||||
"v": "sas",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"t": "eq",
|
||||
"v": "done",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"t": "eq",
|
||||
"v": "cancelled",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"checkall": "true",
|
||||
"outputs": 5,
|
||||
"x": 460,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"act_accept"
|
||||
],
|
||||
[
|
||||
"act_start"
|
||||
],
|
||||
[
|
||||
"chg_savevid"
|
||||
],
|
||||
[
|
||||
"dbg_done"
|
||||
],
|
||||
[
|
||||
"dbg_cancelled"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "act_accept",
|
||||
"type": "matrix-verification-action",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "Accept",
|
||||
"server": null,
|
||||
"mode": "accept",
|
||||
"x": 700,
|
||||
"y": 180,
|
||||
"wires": [
|
||||
[],
|
||||
[
|
||||
"dbg_err"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "act_start",
|
||||
"type": "matrix-verification-action",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "Start SAS",
|
||||
"server": null,
|
||||
"mode": "start",
|
||||
"x": 700,
|
||||
"y": 230,
|
||||
"wires": [
|
||||
[],
|
||||
[
|
||||
"dbg_err"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chg_savevid",
|
||||
"type": "change",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "save verificationId",
|
||||
"rules": [
|
||||
{
|
||||
"t": "set",
|
||||
"p": "verificationId",
|
||||
"pt": "flow",
|
||||
"to": "verificationId",
|
||||
"tot": "msg"
|
||||
}
|
||||
],
|
||||
"x": 710,
|
||||
"y": 290,
|
||||
"wires": [
|
||||
[
|
||||
"dbg_sas"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dbg_sas",
|
||||
"type": "debug",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "SAS emoji (msg.sas)",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"x": 960,
|
||||
"y": 290,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "dbg_done",
|
||||
"type": "debug",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "verification done",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"x": 710,
|
||||
"y": 350,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "dbg_cancelled",
|
||||
"type": "debug",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "verification cancelled",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"x": 730,
|
||||
"y": 400,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "dbg_err",
|
||||
"type": "debug",
|
||||
"z": "vtest",
|
||||
"g": "40c105c38054d6db",
|
||||
"name": "action errors",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"x": 940,
|
||||
"y": 200,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "83f785d52a61009a",
|
||||
"type": "group",
|
||||
"z": "vtest",
|
||||
"g": "7158964bd67edc52",
|
||||
"name": "Confirm or reject last verification request",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"inj_confirm",
|
||||
"chg_vid_c",
|
||||
"act_confirm",
|
||||
"inj_reject",
|
||||
"chg_vid_r",
|
||||
"act_mismatch",
|
||||
"dbg_result"
|
||||
],
|
||||
"x": 114,
|
||||
"y": 459,
|
||||
"w": 982,
|
||||
"h": 142
|
||||
},
|
||||
{
|
||||
"id": "inj_confirm",
|
||||
"type": "inject",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "Confirm SAS match",
|
||||
"props": [],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"x": 250,
|
||||
"y": 500,
|
||||
"wires": [
|
||||
[
|
||||
"chg_vid_c"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chg_vid_c",
|
||||
"type": "change",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "verificationId from flow",
|
||||
"rules": [
|
||||
{
|
||||
"t": "set",
|
||||
"p": "verificationId",
|
||||
"pt": "msg",
|
||||
"to": "verificationId",
|
||||
"tot": "flow"
|
||||
}
|
||||
],
|
||||
"x": 500,
|
||||
"y": 500,
|
||||
"wires": [
|
||||
[
|
||||
"act_confirm"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "act_confirm",
|
||||
"type": "matrix-verification-action",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "Confirm SAS",
|
||||
"server": null,
|
||||
"mode": "confirm",
|
||||
"x": 750,
|
||||
"y": 500,
|
||||
"wires": [
|
||||
[
|
||||
"dbg_result"
|
||||
],
|
||||
[
|
||||
"dbg_err"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inj_reject",
|
||||
"type": "inject",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "Reject SAS (mismatch)",
|
||||
"props": [],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"x": 260,
|
||||
"y": 560,
|
||||
"wires": [
|
||||
[
|
||||
"chg_vid_r"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chg_vid_r",
|
||||
"type": "change",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "verificationId from flow",
|
||||
"rules": [
|
||||
{
|
||||
"t": "set",
|
||||
"p": "verificationId",
|
||||
"pt": "msg",
|
||||
"to": "verificationId",
|
||||
"tot": "flow"
|
||||
}
|
||||
],
|
||||
"x": 500,
|
||||
"y": 560,
|
||||
"wires": [
|
||||
[
|
||||
"act_mismatch"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "act_mismatch",
|
||||
"type": "matrix-verification-action",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "Reject (mismatch)",
|
||||
"server": null,
|
||||
"mode": "mismatch",
|
||||
"x": 760,
|
||||
"y": 560,
|
||||
"wires": [
|
||||
[
|
||||
"dbg_result"
|
||||
],
|
||||
[
|
||||
"dbg_err"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dbg_result",
|
||||
"type": "debug",
|
||||
"z": "vtest",
|
||||
"g": "83f785d52a61009a",
|
||||
"name": "action result",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"x": 980,
|
||||
"y": 530,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "d51bab8cbf5f247c",
|
||||
"type": "group",
|
||||
"z": "vtest",
|
||||
"g": "7158964bd67edc52",
|
||||
"name": "Request verification with specific user & device",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"inj_request",
|
||||
"act_request"
|
||||
],
|
||||
"x": 114,
|
||||
"y": 619,
|
||||
"w": 512,
|
||||
"h": 82
|
||||
},
|
||||
{
|
||||
"id": "inj_request",
|
||||
"type": "inject",
|
||||
"z": "vtest",
|
||||
"g": "d51bab8cbf5f247c",
|
||||
"name": "Verify a user & device",
|
||||
"props": [
|
||||
{
|
||||
"p": "userId",
|
||||
"v": "@CHANGE_ME:example.org",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "deviceId",
|
||||
"v": "CHANGE_ME",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"x": 260,
|
||||
"y": 660,
|
||||
"wires": [
|
||||
[
|
||||
"act_request"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "act_request",
|
||||
"type": "matrix-verification-action",
|
||||
"z": "vtest",
|
||||
"g": "d51bab8cbf5f247c",
|
||||
"name": "Request verification",
|
||||
"server": null,
|
||||
"mode": "request",
|
||||
"x": 510,
|
||||
"y": 660,
|
||||
"wires": [
|
||||
[
|
||||
"dbg_result"
|
||||
],
|
||||
[
|
||||
"dbg_err"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2e543533d49b467c",
|
||||
"type": "group",
|
||||
"z": "vtest",
|
||||
"g": "7158964bd67edc52",
|
||||
"name": "Request verification with specific user & room",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"f7c043d39780b9a4",
|
||||
"b2807fd5125b56b4"
|
||||
],
|
||||
"x": 114,
|
||||
"y": 719,
|
||||
"w": 512,
|
||||
"h": 82
|
||||
},
|
||||
{
|
||||
"id": "f7c043d39780b9a4",
|
||||
"type": "inject",
|
||||
"z": "vtest",
|
||||
"g": "2e543533d49b467c",
|
||||
"name": "Verify a user & room",
|
||||
"props": [
|
||||
{
|
||||
"p": "userId",
|
||||
"v": "@CHANGE_ME:example.org",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "CHANGE_ME",
|
||||
"x": 250,
|
||||
"y": 760,
|
||||
"wires": [
|
||||
[
|
||||
"b2807fd5125b56b4"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b2807fd5125b56b4",
|
||||
"type": "matrix-verification-action",
|
||||
"z": "vtest",
|
||||
"g": "2e543533d49b467c",
|
||||
"name": "Request verification",
|
||||
"server": null,
|
||||
"mode": "request",
|
||||
"x": 510,
|
||||
"y": 760,
|
||||
"wires": [
|
||||
[
|
||||
"dbg_result"
|
||||
],
|
||||
[
|
||||
"dbg_err"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 139 KiB |
@@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -1,92 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "9345e8c42e327dba",
|
||||
"type": "matrix-device-verification",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"server": null,
|
||||
"mode": "request",
|
||||
"inputs": 1,
|
||||
"outputs": 2,
|
||||
"x": 480,
|
||||
"y": 1660,
|
||||
"wires": [
|
||||
[
|
||||
"b676082d56430aec"
|
||||
],
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b676082d56430aec",
|
||||
"type": "matrix-device-verification",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"server": null,
|
||||
"mode": "start",
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"x": 740,
|
||||
"y": 1660,
|
||||
"wires": [
|
||||
[
|
||||
"23a0225f2f2615a3"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "23a0225f2f2615a3",
|
||||
"type": "matrix-device-verification",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"server": null,
|
||||
"mode": "accept",
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"x": 970,
|
||||
"y": 1660,
|
||||
"wires": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "3eced60b58c999eb",
|
||||
"type": "inject",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"props": [
|
||||
{
|
||||
"p": "userId",
|
||||
"v": "@bot:example.com",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "devices",
|
||||
"v": "[\"ZRRJKASJDUK\"]",
|
||||
"vt": "json"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"x": 290,
|
||||
"y": 1660,
|
||||
"wires": [
|
||||
[
|
||||
"9345e8c42e327dba"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f58ceba2a8819c09",
|
||||
"type": "comment",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "Request verification from a specific userId and device",
|
||||
"info": "",
|
||||
"x": 440,
|
||||
"y": 1620,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -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"
|
||||
],
|
||||
[]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -1,86 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "5073ca88b21abfb4",
|
||||
"type": "matrix-device-verification",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"server": null,
|
||||
"mode": "receive",
|
||||
"inputs": 0,
|
||||
"outputs": 1,
|
||||
"x": 350,
|
||||
"y": 1540,
|
||||
"wires": [
|
||||
[
|
||||
"b76c1d185c2793a0"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "05947740ced04e2c",
|
||||
"type": "matrix-device-verification",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"server": null,
|
||||
"mode": "start",
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"x": 740,
|
||||
"y": 1540,
|
||||
"wires": [
|
||||
[
|
||||
"b3158c0779b72b41"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b76c1d185c2793a0",
|
||||
"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": 550,
|
||||
"y": 1540,
|
||||
"wires": [
|
||||
[
|
||||
"05947740ced04e2c"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b3158c0779b72b41",
|
||||
"type": "matrix-device-verification",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "",
|
||||
"server": null,
|
||||
"mode": "accept",
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"x": 970,
|
||||
"y": 1540,
|
||||
"wires": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "eb8ba0741df1b365",
|
||||
"type": "comment",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "Accept all device validation from a user",
|
||||
"info": "",
|
||||
"x": 390,
|
||||
"y": 1500,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -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": []
|
||||
}
|
||||
]
|
||||
@@ -1,14 +1,24 @@
|
||||
{
|
||||
"name": "node-red-contrib-matrix-chat",
|
||||
"version": "0.5.6",
|
||||
"version": "1.0.0",
|
||||
"description": "Matrix chat server client for Node-RED",
|
||||
"dependencies": {
|
||||
"fs-extra": "^10.0.1",
|
||||
"abort-controller": "^3.0.0",
|
||||
"commonmark": "^0.31.2",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^11.1.0",
|
||||
"got": "^12.0.2",
|
||||
"image-size": "^1.0.2",
|
||||
"isomorphic-webcrypto": "^2.3.8",
|
||||
"matrix-js-sdk": "^16.0.0",
|
||||
"linkifyjs": "^4.3.3",
|
||||
"lodash.escape": "^4.0.1",
|
||||
"matrix-js-sdk": "^41.5.0",
|
||||
"mime": "^3.0.0",
|
||||
"node-fetch": "^3.3.0",
|
||||
"node-localstorage": "^2.2.1",
|
||||
"olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/271/download",
|
||||
"sharp": "^0.33.4",
|
||||
"tmp": "^0.2.1",
|
||||
"utf8": "^3.0.0"
|
||||
},
|
||||
"node-red": {
|
||||
@@ -17,39 +27,52 @@
|
||||
"matrix-server-config": "src/matrix-server-config.js",
|
||||
"matrix-receive": "src/matrix-receive.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-image": "src/matrix-send-image.js",
|
||||
"matrix-upload-file": "src/matrix-upload-file.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-invite-room": "src/matrix-invite-room.js",
|
||||
"matrix-room-invite": "src/matrix-room-invite.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-room-kick": "src/matrix-room-kick.js",
|
||||
"matrix-room-ban": "src/matrix-room-ban.js",
|
||||
"matrix-device-verification": "src/matrix-device-verification.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-register": "src/matrix-synapse-register.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-join-room": "src/matrix-synapse-join-room.js",
|
||||
"matrix-whois-user": "src/matrix-whois-user.js",
|
||||
"matrix-room-users": "src/matrix-room-users.js"
|
||||
"matrix-paginate-room": "src/matrix-paginate-room.js",
|
||||
"matrix-get-event": "src/matrix-get-event.js",
|
||||
"matrix-event-relations": "src/matrix-event-relations.js",
|
||||
"matrix-verification": "src/matrix-verification.js",
|
||||
"matrix-verification-action": "src/matrix-verification-action.js",
|
||||
"matrix-send-location": "src/matrix-send-location.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"node-red",
|
||||
"matrix",
|
||||
"support",
|
||||
"bot",
|
||||
"chat"
|
||||
"chat",
|
||||
"chatbot",
|
||||
"federated"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/skylar-tech/node-red-contrib-matrix-chat"
|
||||
"url": "git+https://github.com/Skylar-Tech/node-red-contrib-matrix-chat.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Skylar Sadlier",
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -8,7 +8,7 @@
|
||||
outputs: 2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" }
|
||||
server: { type: "matrix-server-config" }
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Create Room";
|
||||
|
||||
@@ -8,9 +8,10 @@ module.exports = function(RED) {
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
|
||||
if(!this.server) {
|
||||
node.error('Server must be configured on the node.');
|
||||
node.error('Server must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
this.encodeUri = function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
@@ -41,8 +42,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!msg.payload) {
|
||||
@@ -64,6 +66,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-create-room", MatrixCreateRoom);
|
||||
}
|
||||
@@ -43,6 +43,16 @@
|
||||
<span class="property-type">string | null</span>
|
||||
</dt>
|
||||
<dd> the decoded mxc url.</dd>
|
||||
|
||||
<dt class="optional">msg.headers
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>optional HTTP headers. If provided, they are used when downloading media (for example authenticated media bearer tokens).</dd>
|
||||
|
||||
<dt class="optional">msg.access_token
|
||||
<span class="property-type">string</span>
|
||||
</dt>
|
||||
<dd>optional Matrix access token. Used as a bearer token if <code>msg.headers.Authorization</code> is not present.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
@@ -74,4 +84,4 @@
|
||||
<ul>
|
||||
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">MIME Types</a> - description of <code>msg.contentType</code> format</li>
|
||||
</ul>
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -12,27 +12,29 @@ module.exports = function(RED) {
|
||||
const { got } = await import('got');
|
||||
|
||||
if(!msg.type) {
|
||||
node.error('msg.type is required.');
|
||||
node.error('msg.type is required.', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!msg.content) {
|
||||
node.error('msg.content is required.');
|
||||
node.error('msg.content is required.', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!msg.content.file) {
|
||||
node.error('msg.content.file is required.');
|
||||
node.error('msg.content.file is required.', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!msg.url) {
|
||||
node.error('msg.url is required.');
|
||||
node.error('msg.url is required.', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
let buffer = await got(msg.url).buffer();
|
||||
const requestOptions = getRequestOptions(msg);
|
||||
|
||||
let buffer = await downloadBufferWithFallback(got, msg.url, requestOptions);
|
||||
msg.payload = Buffer.from(await decryptAttachment(buffer, msg.content.file));
|
||||
|
||||
// handle thumbnail decryption if necessary
|
||||
@@ -41,13 +43,14 @@ module.exports = function(RED) {
|
||||
&& msg.thumbnail_url
|
||||
&& msg.content.info.thumbnail_file
|
||||
) {
|
||||
let thumb_buffer = await got(msg.thumbnail_url).buffer();
|
||||
let thumb_buffer = await downloadBufferWithFallback(got, msg.thumbnail_url, requestOptions);
|
||||
msg.thumbnail_payload = Buffer.from(await decryptAttachment(thumb_buffer, msg.content.info.thumbnail_file));
|
||||
}
|
||||
} catch(error){
|
||||
node.error(error);
|
||||
msg.error = error;
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.filename = msg.content.filename || msg.content.body;
|
||||
@@ -57,12 +60,58 @@ module.exports = function(RED) {
|
||||
}
|
||||
RED.nodes.registerType("matrix-decrypt-file", MatrixDecryptFile);
|
||||
|
||||
function getRequestOptions(msg) {
|
||||
const headers = { ...(msg.headers || {}) };
|
||||
if (!headers.Authorization && msg.access_token) {
|
||||
headers.Authorization = `Bearer ${msg.access_token}`;
|
||||
}
|
||||
|
||||
return Object.keys(headers).length ? { headers } : {};
|
||||
}
|
||||
|
||||
function getMediaEndpointFallbackUrl(url) {
|
||||
if (typeof url !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.includes("/_matrix/media/v3/download/")) {
|
||||
return url.replace("/_matrix/media/v3/download/", "/_matrix/client/v1/media/download/");
|
||||
}
|
||||
|
||||
if (url.includes("/_matrix/client/v1/media/download/")) {
|
||||
return url.replace("/_matrix/client/v1/media/download/", "/_matrix/media/v3/download/");
|
||||
}
|
||||
|
||||
if (url.includes("/_matrix/media/v3/thumbnail/")) {
|
||||
return url.replace("/_matrix/media/v3/thumbnail/", "/_matrix/client/v1/media/thumbnail/");
|
||||
}
|
||||
|
||||
if (url.includes("/_matrix/client/v1/media/thumbnail/")) {
|
||||
return url.replace("/_matrix/client/v1/media/thumbnail/", "/_matrix/media/v3/thumbnail/");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function downloadBufferWithFallback(got, url, requestOptions) {
|
||||
try {
|
||||
return await got(url, requestOptions).buffer();
|
||||
} catch (error) {
|
||||
const fallbackUrl = getMediaEndpointFallbackUrl(url);
|
||||
if (error?.response?.statusCode === 404 && fallbackUrl && fallbackUrl !== url) {
|
||||
return await got(fallbackUrl, requestOptions).buffer();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function atob(a) {
|
||||
return new Buffer.from(a, 'base64').toString('binary');
|
||||
return Buffer.from(a, 'base64').toString('binary');
|
||||
}
|
||||
|
||||
function btoa(b) {
|
||||
return new Buffer.from(b).toString('base64');
|
||||
return Buffer.from(b).toString('base64');
|
||||
}
|
||||
|
||||
// the following was taken & modified from https://github.com/matrix-org/browser-encrypt-attachment/blob/master/index.js
|
||||
@@ -200,4 +249,4 @@ module.exports = function(RED) {
|
||||
}
|
||||
return uint8Array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Persistence helpers for the matrix-js-sdk Rust crypto store in Node.js.
|
||||
*
|
||||
* matrix-js-sdk v37+ removed the legacy (libolm) crypto stack. The Rust crypto
|
||||
* replacement persists its state (device identity, Olm/megolm sessions, etc.)
|
||||
* to IndexedDB, which does not exist in Node.js. We provide an in-memory
|
||||
* IndexedDB via `fake-indexeddb` and snapshot the databases to/from disk so the
|
||||
* crypto state survives Node-RED restarts.
|
||||
*
|
||||
* The `indexeddbshim` package (which can persist to disk directly) is not used
|
||||
* because it is incompatible with the Rust crypto store migrations
|
||||
* (see matrix-org/matrix-sdk-crypto-wasm#195). `fake-indexeddb` is spec
|
||||
* compliant, so snapshotting it through the public IndexedDB API is reliable.
|
||||
*/
|
||||
const fs = require('fs-extra');
|
||||
const v8 = require('v8');
|
||||
|
||||
let shimInstalled = false;
|
||||
|
||||
/**
|
||||
* Install the in-memory IndexedDB shim onto globalThis. Idempotent. Must be
|
||||
* called before MatrixClient.initRustCrypto().
|
||||
*/
|
||||
function ensureIndexedDBShim() {
|
||||
if (shimInstalled || globalThis.indexedDB) {
|
||||
shimInstalled = true;
|
||||
return;
|
||||
}
|
||||
// `fake-indexeddb/auto` assigns indexedDB / IDBKeyRange / etc. onto globalThis.
|
||||
require('fake-indexeddb/auto');
|
||||
shimInstalled = true;
|
||||
}
|
||||
|
||||
function reqAsync(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
function txDone(tx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error || new Error('IndexedDB transaction aborted'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore previously snapshotted IndexedDB databases from `filePath` into the
|
||||
* in-memory store. No-op if the snapshot does not exist. Databases that are
|
||||
* already present in memory (e.g. after a Node-RED redeploy that kept the
|
||||
* process alive) are left untouched so the live state is not clobbered.
|
||||
*
|
||||
* Must be called before MatrixClient.initRustCrypto().
|
||||
*
|
||||
* @returns {Promise<boolean>} true if at least one database was restored.
|
||||
*/
|
||||
async function restoreCryptoStore(filePath) {
|
||||
ensureIndexedDBShim();
|
||||
|
||||
if (!filePath || !fs.pathExistsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let databases;
|
||||
try {
|
||||
databases = v8.deserialize(fs.readFileSync(filePath));
|
||||
} catch (e) {
|
||||
// Corrupt/unreadable snapshot - start fresh rather than crash.
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(databases) || !databases.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existing = new Set((await indexedDB.databases()).map((d) => d.name));
|
||||
let restored = 0;
|
||||
|
||||
for (const dbSpec of databases) {
|
||||
if (existing.has(dbSpec.name)) {
|
||||
continue; // already live in memory - don't overwrite
|
||||
}
|
||||
|
||||
const openReq = indexedDB.open(dbSpec.name, dbSpec.version);
|
||||
openReq.onupgradeneeded = () => {
|
||||
const db = openReq.result;
|
||||
for (const store of dbSpec.stores) {
|
||||
if (db.objectStoreNames.contains(store.name)) {
|
||||
continue;
|
||||
}
|
||||
const os = db.createObjectStore(store.name, {
|
||||
keyPath: store.keyPath || undefined,
|
||||
autoIncrement: store.autoIncrement,
|
||||
});
|
||||
for (const ix of store.indexes) {
|
||||
os.createIndex(ix.name, ix.keyPath, { unique: ix.unique, multiEntry: ix.multiEntry });
|
||||
}
|
||||
}
|
||||
};
|
||||
const db = await reqAsync(openReq);
|
||||
|
||||
for (const store of dbSpec.stores) {
|
||||
if (!store.values.length) {
|
||||
continue;
|
||||
}
|
||||
const tx = db.transaction(store.name, 'readwrite');
|
||||
const os = tx.objectStore(store.name);
|
||||
for (let i = 0; i < store.values.length; i++) {
|
||||
if (store.keyPath) {
|
||||
os.put(store.values[i]);
|
||||
} else {
|
||||
os.put(store.values[i], store.keys[i]);
|
||||
}
|
||||
}
|
||||
await txDone(tx);
|
||||
}
|
||||
db.close();
|
||||
restored++;
|
||||
}
|
||||
|
||||
return restored > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot IndexedDB databases to `filePath`. If `dbNamePrefix` is given only
|
||||
* databases whose name starts with it are written, so multiple Matrix accounts
|
||||
* sharing one process do not snapshot each other's data.
|
||||
*
|
||||
* The write is atomic (temp file + rename). Values are serialized with the V8
|
||||
* serializer so typed arrays / Maps inside the crypto store survive intact.
|
||||
*
|
||||
* @returns {Promise<boolean>} true if a snapshot file was written.
|
||||
*/
|
||||
async function snapshotCryptoStore(filePath, dbNamePrefix) {
|
||||
if (!filePath || !globalThis.indexedDB || typeof indexedDB.databases !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let dbList = await indexedDB.databases();
|
||||
if (dbNamePrefix) {
|
||||
dbList = dbList.filter((d) => typeof d.name === 'string' && d.name.startsWith(dbNamePrefix));
|
||||
}
|
||||
|
||||
const out = [];
|
||||
for (const { name, version } of dbList) {
|
||||
const db = await reqAsync(indexedDB.open(name, version));
|
||||
const stores = [];
|
||||
for (const storeName of Array.from(db.objectStoreNames)) {
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const os = tx.objectStore(storeName);
|
||||
const indexes = Array.from(os.indexNames).map((n) => {
|
||||
const ix = os.index(n);
|
||||
return { name: n, keyPath: ix.keyPath, unique: ix.unique, multiEntry: ix.multiEntry };
|
||||
});
|
||||
stores.push({
|
||||
name: storeName,
|
||||
keyPath: os.keyPath,
|
||||
autoIncrement: os.autoIncrement,
|
||||
indexes,
|
||||
values: await reqAsync(os.getAll()),
|
||||
keys: await reqAsync(os.getAllKeys()),
|
||||
});
|
||||
}
|
||||
db.close();
|
||||
out.push({ name, version, stores });
|
||||
}
|
||||
|
||||
const tmp = `${filePath}.tmp`;
|
||||
fs.writeFileSync(tmp, v8.serialize(out));
|
||||
fs.renameSync(tmp, filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore };
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
reason: { value: "" },
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -27,7 +28,7 @@ module.exports = function(RED) {
|
||||
node.on('input', function(msg) {
|
||||
|
||||
if(!msg.eventId) {
|
||||
node.error("eventId is missing");
|
||||
node.error("eventId is missing", msg);
|
||||
node.send([null, msg])
|
||||
return;
|
||||
}
|
||||
@@ -38,7 +39,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
@@ -70,6 +71,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-delete-event",MatrixDeleteEvent);
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
let computeInputAndOutputCounts = function(node){
|
||||
switch($("#node-input-mode").val()) {
|
||||
default:
|
||||
node.outputs = node.inputs = 0;
|
||||
break;
|
||||
case 'receive':
|
||||
node.outputs = 1;
|
||||
node.inputs = 0;
|
||||
break;
|
||||
case 'request':
|
||||
case 'start':
|
||||
case 'accept':
|
||||
case 'cancel':
|
||||
node.outputs = 2;
|
||||
node.inputs = 1;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
RED.nodes.registerType('matrix-device-verification', {
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
inputs: 0,
|
||||
outputs: 0,
|
||||
outputLabels: ["success", "error"],
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
mode: { value: null, type: "text", required: true },
|
||||
inputs: { value: 0 },
|
||||
outputs: { value: 0 }
|
||||
},
|
||||
oneditprepare: function () {
|
||||
computeInputAndOutputCounts(this);
|
||||
},
|
||||
oneditsave: function () {
|
||||
computeInputAndOutputCounts(this);
|
||||
},
|
||||
label: function() {
|
||||
if(this.name) {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
switch(this.mode) {
|
||||
default:
|
||||
return 'Device Verification';
|
||||
case 'receive':
|
||||
return 'Receive Device Verification';
|
||||
case 'request':
|
||||
return 'Request Device Verification';
|
||||
case 'start':
|
||||
return 'Start Device Verification';
|
||||
case 'accept':
|
||||
return 'Accept Device Verification';
|
||||
case 'cancel':
|
||||
return 'Cancel Device Verification';
|
||||
}
|
||||
return this.name || "Device Verify Request";
|
||||
},
|
||||
paletteLabel: function(){
|
||||
return "Device Verification";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-device-verification">
|
||||
<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-mode"><i class="fa fa-user"></i> Mode</label>
|
||||
<select id="node-input-mode" style="width:70%;">
|
||||
<option value="">Unconfigured</option>
|
||||
<option value="receive">Receive Verification Request</option>
|
||||
<option value="request">Request Verification</option>
|
||||
<option value="start">Verification Start</option>
|
||||
<option value="accept">Verification Accept</option>
|
||||
<option value="cancel">Verification Cancel</option>
|
||||
</select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-device-verification">
|
||||
<h3>Details</h3>
|
||||
<p>
|
||||
Handle device verification. Check out the <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme" target="_blank">examples</a> page for a good understanding of how this works.
|
||||
<br />
|
||||
General flow:
|
||||
<ol>
|
||||
<li>Request/Receive device verification</li>
|
||||
<li>Start Verification</li>
|
||||
<li>Compare Emojis</li>
|
||||
<li>Accept/Cancel Verification</li>
|
||||
</ol>
|
||||
<br />
|
||||
THIS NODE IS IN BETA. There is a good chance that we will change how this node works later down the road. Make sure to read the release notes before upgrading.
|
||||
</p>
|
||||
<a href="https://matrix-org.github.io/synapse/develop/admin_api/room_membership.html#edit-room-membership-api" target="_blank">Synapse API Endpoint Information</a>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<ul class="node-inputs">
|
||||
<li><code>mode</code> set to '<strong>Receive Verification Request</strong>'
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
Doesn't take an input
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li><code>mode</code> set to '<strong>Request Verification</strong>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.userId <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
ID of the user to request device verification from
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.devices <span class="property-type">array[string]|null</span></dt>
|
||||
<dd> list of <code>msg.userId</code>'s devices IDs to request verification from. If empty it will request from all known devices.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>mode</code> set to '<strong>Verification Start</strong>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
Internal ID to reference the verification request throughout the flows
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.cancel <span class="property-type">bool</span></dt>
|
||||
<dd>
|
||||
If set and is true the verification request will be cancelled
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>mode</code> set to '<strong>Verification Accept</strong>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
Internal ID to reference the verification request throughout the flows
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>mode</code> set to '<strong>Verification Cancel</strong>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
Internal ID to reference the verification request throughout the flows
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ul class="node-outputs">
|
||||
<li><code>mode</code> set to '<strong>Receive Verification Request</strong>' or '<strong>Request Verification</strong>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verifyRequestId <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
Internal ID to reference the verification request throughout the flows
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verifyMethods <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
Common verification methods supported by both sides
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.userId <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
ID of the user to request device verification from
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.deviceIds <span class="property-type">array[string]</span></dt>
|
||||
<dd>
|
||||
List of devices we are verifying
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.selfVerification <span class="property-type">bool</span></dt>
|
||||
<dd>
|
||||
true if we are verifying one of our own devices
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.phase <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
what phase of verification we are in
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>mode</code> set to '<strong>Verification Start</strong>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.payload <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
sas verification payload
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.emojis <span class="property-type">array[string]</span></dt>
|
||||
<dd>
|
||||
array of emojis for verification request
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.emojis_text <span class="property-type">array[string]</span></dt>
|
||||
<dd>
|
||||
array of emojis in text form for verification request
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>mode</code> set to '<strong>Verification Accept</strong>' or '<strong>Verification Cancel</strong>'
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
Passes input straight to output on success. If an error occurs it goes to the second output.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</script>
|
||||
@@ -1,234 +0,0 @@
|
||||
const {Phase} = require("matrix-js-sdk/lib/crypto/verification/request/VerificationRequest");
|
||||
const {CryptoEvent} = require("matrix-js-sdk/lib/crypto");
|
||||
|
||||
module.exports = function(RED) {
|
||||
const verificationRequests = new Map();
|
||||
|
||||
function MatrixDeviceVerification(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
var node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.mode = n.mode;
|
||||
|
||||
if (!node.server) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!node.server.e2ee) {
|
||||
node.error("End-to-end encryption needs to be enabled to use this.");
|
||||
}
|
||||
|
||||
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" });
|
||||
});
|
||||
|
||||
function getKeyByValue(object, value) {
|
||||
return Object.keys(object).find(key => object[key] === value);
|
||||
}
|
||||
|
||||
switch(node.mode) {
|
||||
default:
|
||||
node.error("Node not configured with a mode");
|
||||
break;
|
||||
|
||||
case 'request':
|
||||
node.on('input', async function(msg){
|
||||
if(!msg.userId) {
|
||||
node.error("msg.userId is required for start verification mode");
|
||||
}
|
||||
|
||||
node.server.matrixClient.requestVerification(msg.userId, msg.devices || null)
|
||||
.then(function(e) {
|
||||
node.log("Successfully requested verification");
|
||||
let verifyRequestId = msg.userId + ':' + e.channel.deviceId;
|
||||
verificationRequests.set(verifyRequestId, e);
|
||||
node.send({
|
||||
verifyRequestId: verifyRequestId, // internally used to reference between nodes
|
||||
verifyMethods: e.methods,
|
||||
userId: msg.userId,
|
||||
deviceIds: e.channel.devices,
|
||||
selfVerification: e.isSelfVerification,
|
||||
phase: getKeyByValue(Phase, e.phase)
|
||||
});
|
||||
})
|
||||
.catch(function(e){
|
||||
node.warn("Error requesting device verification: " + e);
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'receive':
|
||||
/**
|
||||
* Fires when a key verification is requested.
|
||||
* @event module:client~MatrixClient#"crypto.verification.request"
|
||||
* @param {object} data
|
||||
* @param {MatrixEvent} data.event the original verification request message
|
||||
* @param {Array} data.methods the verification methods that can be used
|
||||
* @param {Number} data.timeout the amount of milliseconds that should be waited
|
||||
* before cancelling the request automatically.
|
||||
* @param {Function} data.beginKeyVerification a function to call if a key
|
||||
* verification should be performed. The function takes one argument: the
|
||||
* name of the key verification method (taken from data.methods) to use.
|
||||
* @param {Function} data.cancel a function to call if the key verification is
|
||||
* rejected.
|
||||
*/
|
||||
node.server.matrixClient.on(CryptoEvent.VerificationRequest, async function(data){
|
||||
if(data.phase === Phase.Cancelled || data.phase === Phase.Done) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.requested || true) {
|
||||
let verifyRequestId = data.targetDevice.userId + ':' + data.targetDevice.deviceId;
|
||||
verificationRequests.set(verifyRequestId, data);
|
||||
node.send({
|
||||
verifyRequestId: verifyRequestId, // internally used to reference between nodes
|
||||
verifyMethods: data.methods,
|
||||
userId: data.targetDevice.userId,
|
||||
deviceId: data.targetDevice.deviceId,
|
||||
selfVerification: data.isSelfVerification,
|
||||
phase: getKeyByValue(Phase, data.phase)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
node.on('close', function(done) {
|
||||
// clear verification requests
|
||||
verificationRequests.clear();
|
||||
done();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'start':
|
||||
node.on('input', async function(msg){
|
||||
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
|
||||
// if(msg.userId && msg.deviceId) {
|
||||
// node.server.beginKeyVerification("m.sas.v1", msg.userId, msg.deviceId);
|
||||
// }
|
||||
|
||||
node.error("invalid verification request (invalid msg.verifyRequestId): " + (msg.verifyRequestId || null));
|
||||
}
|
||||
|
||||
var data = verificationRequests.get(msg.verifyRequestId);
|
||||
if(msg.cancel) {
|
||||
await data._verifier.cancel();
|
||||
verificationRequests.delete(msg.verifyRequestId);
|
||||
} else {
|
||||
try {
|
||||
data.on('change', async function() {
|
||||
var that = this;
|
||||
if(this.phase === Phase.Started) {
|
||||
let verifierCancel = function(){
|
||||
let verifyRequestId = that.targetDevice.userId + ':' + that.targetDevice.deviceId;
|
||||
if(verificationRequests.has(verifyRequestId)) {
|
||||
verificationRequests.delete(verifyRequestId);
|
||||
}
|
||||
};
|
||||
|
||||
data._verifier.on('cancel', function(e){
|
||||
node.warn("Device verification cancelled " + e);
|
||||
verifierCancel();
|
||||
});
|
||||
|
||||
let show_sas = function(e) {
|
||||
// e = {
|
||||
// sas: {
|
||||
// decimal: [ 8641, 3153, 2357 ],
|
||||
// emoji: [
|
||||
// [Array], [Array],
|
||||
// [Array], [Array],
|
||||
// [Array], [Array],
|
||||
// [Array]
|
||||
// ]
|
||||
// },
|
||||
// confirm: [AsyncFunction: confirm],
|
||||
// cancel: [Function: cancel],
|
||||
// mismatch: [Function: mismatch]
|
||||
// }
|
||||
msg.payload = e.sas;
|
||||
msg.emojis = e.sas.emoji.map(function(emoji, i) {
|
||||
return emoji[0];
|
||||
});
|
||||
msg.emojis_text = e.sas.emoji.map(function(emoji, i) {
|
||||
return emoji[1];
|
||||
});
|
||||
node.send(msg);
|
||||
};
|
||||
data._verifier.on('show_sas', show_sas);
|
||||
data._verifier.verify()
|
||||
.then(function(e){
|
||||
data._verifier.off('show_sas', show_sas);
|
||||
data._verifier.done();
|
||||
}, function(e) {
|
||||
verifierCancel();
|
||||
node.warn(e);
|
||||
// @todo return over second output
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
data.emit("change");
|
||||
await data.accept();
|
||||
} catch(e) {
|
||||
console.log("ERROR", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
node.on('input', async function(msg){
|
||||
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
|
||||
node.error("Invalid verification request: " + (msg.verifyRequestId || null));
|
||||
}
|
||||
|
||||
var data = verificationRequests.get(msg.verifyRequestId);
|
||||
if(data) {
|
||||
data.cancel()
|
||||
.then(function(e){
|
||||
node.send([msg, null]);
|
||||
})
|
||||
.catch(function(e) {
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'accept':
|
||||
node.on('input', async function(msg){
|
||||
if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) {
|
||||
node.error("Invalid verification request: " + (msg.verifyRequestId || null));
|
||||
}
|
||||
|
||||
var data = verificationRequests.get(msg.verifyRequestId);
|
||||
if(data._verifier && data._verifier.sasEvent) {
|
||||
data._verifier.sasEvent.confirm()
|
||||
.then(function(e){
|
||||
node.send([msg, null]);
|
||||
})
|
||||
.catch(function(e) {
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
} else {
|
||||
node.error("Verification must be started");
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType("matrix-device-verification", MatrixDeviceVerification);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 = await node.server.matrixClient.getProfileInfo(userId);
|
||||
if(profileInfo && Object.keys(profileInfo).length > 0) {
|
||||
user2.displayName = profileInfo.displayname;
|
||||
user2.avatarUrl = profileInfo.avatar_url;
|
||||
}
|
||||
|
||||
let presence = await node.server.matrixClient.getPresence(userId);
|
||||
if(presence && Object.keys(presence).length > 0) {
|
||||
user2.currentlyActive = presence.currently_active;
|
||||
user2.lastActiveAgo = presence.last_active_ago;
|
||||
user2.presenceStatusMsg = presence.presence_status_msg;
|
||||
user2.presence = presence.presence;
|
||||
}
|
||||
|
||||
if(Object.keys(user2).length > 0) {
|
||||
setToValue(user2, node.propertyType, node.propertyValue);
|
||||
node.send([msg, null]);
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
msg.error = "Failed getting user: " + e.message;
|
||||
node.error(msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setToValue(user, node.propertyType, node.propertyValue);
|
||||
node.send([msg, null]);
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-get-user", MatrixGetUser);
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
outputs: 2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Room Invite";
|
||||
return this.name || "Invite to Room";
|
||||
},
|
||||
paletteLabel: 'Room Invite'
|
||||
paletteLabel: 'Invite to Room'
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ module.exports = function(RED) {
|
||||
this.roomId = n.roomId;
|
||||
|
||||
if(!this.server) {
|
||||
node.error('Server must be configured on the node.');
|
||||
node.error('Server must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
this.encodeUri = function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
@@ -37,24 +38,26 @@ module.exports = function(RED) {
|
||||
|
||||
node.on("input", function (msg) {
|
||||
if (! node.server || ! node.server.matrixClient) {
|
||||
node.error("No matrix server selected");
|
||||
node.error("No matrix server selected", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || 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;
|
||||
}
|
||||
|
||||
// we need the status code, so set onlydata to false for this request
|
||||
// invite(roomId, userId, opts|reason) - the SDK no longer accepts a
|
||||
// callback argument, so the reason is passed as the 3rd parameter.
|
||||
node.server.matrixClient
|
||||
.invite(msg.topic, msg.userId, undefined, msg.reason || undefined)
|
||||
.invite(msg.topic, msg.userId, msg.reason || undefined)
|
||||
.then(function(e){
|
||||
msg.payload = e;
|
||||
node.send([msg, null]);
|
||||
@@ -64,6 +67,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-invite-room", MatrixInviteRoom);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" }
|
||||
server: { type: "matrix-server-config" }
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Join Room";
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -24,17 +25,18 @@ module.exports = function(RED) {
|
||||
|
||||
node.on("input", function (msg) {
|
||||
if (! node.server || ! node.server.matrixClient) {
|
||||
node.error("No matrix server selected");
|
||||
node.error("No matrix server selected", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!msg.topic) {
|
||||
node.error("Room must be specified in msg.topic");
|
||||
node.error("Room must be specified in msg.topic", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,11 +48,15 @@ module.exports = function(RED) {
|
||||
node.send([msg, null]);
|
||||
})
|
||||
.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;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-join-room", MatrixJoinRoom);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Markdown -> HTML converter for matrix messages.
|
||||
//
|
||||
// Ported from matrix-react-sdk's `src/Markdown.ts` (now living at
|
||||
// element-hq/element-web `apps/web/src/Markdown.ts`) so the HTML this module
|
||||
// generates lines up with what Element produces for the same markdown source.
|
||||
//
|
||||
// Keep this in sync with element-web's Markdown.ts when noticeable changes
|
||||
// land there. Source of truth:
|
||||
// https://github.com/element-hq/element-web/blob/develop/apps/web/src/Markdown.ts
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2016 OpenMarket Ltd
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
|
||||
const commonmark = require("commonmark");
|
||||
const escape = require("lodash.escape");
|
||||
const linkify = require("linkifyjs");
|
||||
|
||||
const ALLOWED_HTML_TAGS = ["sub", "sup", "del", "s", "u", "br", "br/"];
|
||||
|
||||
// These types of node are definitely text
|
||||
const TEXT_NODES = ["text", "softbreak", "linebreak", "paragraph", "document"];
|
||||
|
||||
function isAllowedHtmlTag(node) {
|
||||
if (!node.literal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Regex won't work for tags with attrs, but the tags we allow
|
||||
// shouldn't really have any anyway.
|
||||
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
||||
if (matches && matches.length == 2) {
|
||||
const tag = matches[1];
|
||||
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if the parse output containing the node
|
||||
* comprises multiple block level elements (ie. lines),
|
||||
* or false if it is only a single line.
|
||||
*/
|
||||
function isMultiLine(node) {
|
||||
let par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
}
|
||||
return par.firstChild != par.lastChild;
|
||||
}
|
||||
|
||||
function getTextUntilEndOrLinebreak(node) {
|
||||
let currentNode = node;
|
||||
let text = "";
|
||||
while (currentNode && currentNode.type !== "softbreak" && currentNode.type !== "linebreak") {
|
||||
const { literal, type } = currentNode;
|
||||
if (type === "text" && literal) {
|
||||
let n = 0;
|
||||
let char = literal[n];
|
||||
while (char !== " " && char !== null && n <= literal.length) {
|
||||
if (char === " ") {
|
||||
break;
|
||||
}
|
||||
if (char) {
|
||||
text += char;
|
||||
}
|
||||
n += 1;
|
||||
char = literal[n];
|
||||
}
|
||||
if (char === " ") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
const formattingChangesByNodeType = {
|
||||
emph: "_",
|
||||
strong: "__",
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the literal of a node and all child nodes.
|
||||
*/
|
||||
const innerNodeLiteral = (node) => {
|
||||
let literal = "";
|
||||
|
||||
const walker = node.walker();
|
||||
let step;
|
||||
|
||||
while ((step = walker.next())) {
|
||||
const currentNode = step.node;
|
||||
const currentNodeLiteral = currentNode.literal;
|
||||
if (step.entering && currentNode.type === "text" && currentNodeLiteral) {
|
||||
literal += currentNodeLiteral;
|
||||
}
|
||||
}
|
||||
|
||||
return literal;
|
||||
};
|
||||
|
||||
const emptyItemWithNoSiblings = (node) => {
|
||||
return !node.prev && !node.next && !node.firstChild;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class that wraps commonmark, adding the ability to see whether
|
||||
* a given message actually uses any markdown syntax or whether
|
||||
* it's plain text.
|
||||
*/
|
||||
class Markdown {
|
||||
constructor(input) {
|
||||
this.input = input;
|
||||
|
||||
const parser = new commonmark.Parser();
|
||||
this.parsed = parser.parse(this.input);
|
||||
this.parsed = this.repairLinks(this.parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is modifying the parsed AST in such a way that links are always
|
||||
* properly linkified instead of sometimes being wrongly emphasised in case
|
||||
* if you were to write a link like the example below:
|
||||
* https://my_weird-link_domain.domain.com
|
||||
* ^ this link would be parsed to something like this:
|
||||
* <a href="https://my">https://my</a><b>weird-link</b><a href="https://domain.domain.com">domain.domain.com</a>
|
||||
* This method makes it so the link gets properly modified to a version where it is
|
||||
* not emphasised until it actually ends.
|
||||
* See: https://github.com/vector-im/element-web/issues/4674
|
||||
*/
|
||||
repairLinks(parsed) {
|
||||
const walker = parsed.walker();
|
||||
let event = null;
|
||||
let text = "";
|
||||
let isInPara = false;
|
||||
let previousNode = null;
|
||||
let shouldUnlinkFormattingNode = false;
|
||||
while ((event = walker.next())) {
|
||||
const { node } = event;
|
||||
if (node.type === "paragraph") {
|
||||
isInPara = !!event.entering;
|
||||
}
|
||||
if (isInPara) {
|
||||
// Clear saved string when line ends
|
||||
if (
|
||||
node.type === "softbreak" ||
|
||||
node.type === "linebreak" ||
|
||||
// Also start calculating the text from the beginning on any spaces
|
||||
(node.type === "text" && node.literal === " ")
|
||||
) {
|
||||
text = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break up text nodes on spaces, so that we don't shoot past them without resetting
|
||||
if (node.type === "text" && node.literal) {
|
||||
const [thisPart, ...nextParts] = node.literal.split(/( )/);
|
||||
node.literal = thisPart;
|
||||
text += thisPart;
|
||||
|
||||
// Add the remaining parts as siblings
|
||||
nextParts.reverse().forEach((part) => {
|
||||
if (part) {
|
||||
const nextNode = new commonmark.Node("text");
|
||||
nextNode.literal = part;
|
||||
node.insertAfter(nextNode);
|
||||
// Make the iterator aware of the newly inserted node
|
||||
walker.resumeAt(nextNode, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// We should not do this if previous node was not a textnode, as we can't combine it then.
|
||||
if (
|
||||
(node.type === "emph" || node.type === "strong") &&
|
||||
previousNode && previousNode.type === "text"
|
||||
) {
|
||||
if (event.entering) {
|
||||
const foundLinks = linkify.find(text);
|
||||
for (const { value } of foundLinks) {
|
||||
if (node && node.firstChild && node.firstChild.literal) {
|
||||
/**
|
||||
* NOTE: This technically should unlink the emph node and create LINK nodes instead, adding all the next elements as siblings
|
||||
* but this solution seems to work well and is hopefully slightly easier to understand too
|
||||
*/
|
||||
const format = formattingChangesByNodeType[node.type];
|
||||
const nonEmphasizedText = `${format}${innerNodeLiteral(node)}${format}`;
|
||||
const f = getTextUntilEndOrLinebreak(node);
|
||||
const newText = value + nonEmphasizedText + f;
|
||||
const newLinks = linkify.find(newText);
|
||||
// Should always find only one link here, if it finds more it means that the algorithm is broken
|
||||
if (newLinks.length === 1) {
|
||||
const emphasisTextNode = new commonmark.Node("text");
|
||||
emphasisTextNode.literal = nonEmphasizedText;
|
||||
previousNode.insertAfter(emphasisTextNode);
|
||||
node.firstChild.literal = "";
|
||||
event = node.walker().next();
|
||||
if (event) {
|
||||
// Remove `em` opening and closing nodes
|
||||
node.unlink();
|
||||
previousNode.insertAfter(event.node);
|
||||
shouldUnlinkFormattingNode = true;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"matrix-chat markdown: link escaping found too many links for text:",
|
||||
text,
|
||||
"modified:",
|
||||
newText,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (shouldUnlinkFormattingNode) {
|
||||
node.unlink();
|
||||
shouldUnlinkFormattingNode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
previousNode = node;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
isPlainText() {
|
||||
const walker = this.parsed.walker();
|
||||
let ev;
|
||||
|
||||
while ((ev = walker.next())) {
|
||||
const node = ev.node;
|
||||
|
||||
if (TEXT_NODES.indexOf(node.type) > -1) {
|
||||
// definitely text
|
||||
continue;
|
||||
} else if (node.type == "list" || node.type == "item") {
|
||||
// Special handling for inputs like `+`, `*`, `-` and `2021.` which
|
||||
// would otherwise be treated as a list of a single empty item.
|
||||
// See https://github.com/vector-im/element-web/issues/7631
|
||||
if (
|
||||
node.type == "list" &&
|
||||
node.firstChild &&
|
||||
emptyItemWithNoSiblings(node.firstChild)
|
||||
) {
|
||||
// A list with a single empty item is treated as plain text.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.type == "item" && emptyItemWithNoSiblings(node)) {
|
||||
// An empty list item with no sibling items is treated as plain text.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Everything else is actual lists and therefore not plaintext.
|
||||
return false;
|
||||
} else if (node.type == "html_inline" || node.type == "html_block") {
|
||||
// if it's an allowed html tag, we need to render it and therefore
|
||||
// we will need to use HTML. If it's not allowed, it's not HTML since
|
||||
// we'll just be treating it as text.
|
||||
if (isAllowedHtmlTag(node)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toHTML({ externalLinks = false } = {}) {
|
||||
const renderer = new commonmark.HtmlRenderer({
|
||||
safe: false,
|
||||
|
||||
// Set soft breaks to hard HTML breaks: commonmark
|
||||
// puts softbreaks in for multiple lines in a blockquote,
|
||||
// so if these are just newline characters then the
|
||||
// block quote ends up all on one line
|
||||
// (https://github.com/vector-im/element-web/issues/3154)
|
||||
softbreak: "<br />",
|
||||
});
|
||||
|
||||
// Trying to strip out the wrapping <p/> causes a lot more complication
|
||||
// than it's worth, i think. For instance, this code will go and strip
|
||||
// out any <p/> tag (no matter where it is in the tree) which doesn't
|
||||
// contain \n's.
|
||||
// On the flip side, <p/>s are quite opionated and restricted on where
|
||||
// you can nest them.
|
||||
//
|
||||
// Let's try sending with <p/>s anyway for now, though.
|
||||
const realParagraph = renderer.paragraph;
|
||||
renderer.paragraph = function (node, entering) {
|
||||
// If there is only one top level node, just return the
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
// However, if it's a blockquote, adds a p tag anyway
|
||||
// in order to avoid deviation to commonmark and unexpected
|
||||
// results when parsing the formatted HTML.
|
||||
if ((node.parent && node.parent.type === "block_quote") || isMultiLine(node)) {
|
||||
realParagraph.call(this, node, entering);
|
||||
}
|
||||
};
|
||||
|
||||
renderer.link = function (node, entering) {
|
||||
const attrs = this.attrs(node);
|
||||
if (entering && node.destination) {
|
||||
attrs.push(["href", this.esc(node.destination)]);
|
||||
if (node.title) {
|
||||
attrs.push(["title", this.esc(node.title)]);
|
||||
}
|
||||
// Modified link behaviour to treat them all as external and
|
||||
// thus opening in a new tab.
|
||||
if (externalLinks) {
|
||||
attrs.push(["target", "_blank"]);
|
||||
attrs.push(["rel", "noreferrer noopener"]);
|
||||
}
|
||||
this.tag("a", attrs);
|
||||
} else {
|
||||
this.tag("/a");
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_inline = function (node) {
|
||||
if (node.literal) {
|
||||
if (isAllowedHtmlTag(node)) {
|
||||
this.lit(node.literal);
|
||||
} else {
|
||||
this.lit(escape(node.literal));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_block = function (node) {
|
||||
renderer.html_inline(node);
|
||||
};
|
||||
|
||||
return renderer.render(this.parsed);
|
||||
}
|
||||
|
||||
/*
|
||||
* Render the markdown message to plain text. That is, essentially
|
||||
* just remove any backslashes escaping what would otherwise be
|
||||
* markdown syntax
|
||||
* (to fix https://github.com/vector-im/element-web/issues/2870).
|
||||
*
|
||||
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
||||
* which has no formatting. Otherwise it emits HTML(!).
|
||||
*/
|
||||
toPlaintext() {
|
||||
const renderer = new commonmark.HtmlRenderer({ safe: false });
|
||||
|
||||
renderer.paragraph = function (node, entering) {
|
||||
// as with toHTML, only append lines to paragraphs if there are
|
||||
// multiple paragraphs
|
||||
if (isMultiLine(node)) {
|
||||
if (!entering && node.next) {
|
||||
this.lit("\n\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_block = function (node) {
|
||||
if (node.literal) this.lit(node.literal);
|
||||
if (isMultiLine(node) && node.next) this.lit("\n\n");
|
||||
};
|
||||
|
||||
// We inhibit the default escape function as we escape the entire output string to correctly handle backslashes
|
||||
renderer.esc = (input) => input;
|
||||
|
||||
return escape(renderer.render(this.parsed));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Markdown };
|
||||
@@ -0,0 +1,133 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-paginate-room', {
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
outputLabels: ["Paginated Data", "Error"],
|
||||
inputs: 1,
|
||||
outputs: 2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomType: { value: "msg" },
|
||||
roomValue: { value: "topic" },
|
||||
paginateKeyType: { value: "msg" },
|
||||
paginateKeyValue: { value: "paginationKey" },
|
||||
paginateBackwardsType: { value: "bool" },
|
||||
paginateBackwardsValue: { value: "true" },
|
||||
pageSizeType: { value: "num" },
|
||||
pageSizeValue: { value: "25" }
|
||||
},
|
||||
label: function () {
|
||||
return this.name || "Paginate Room";
|
||||
},
|
||||
paletteLabel: 'Paginate Room',
|
||||
oneditprepare: function () {
|
||||
$("#node-input-room").typedInput({
|
||||
type: this.roomType,
|
||||
types: ['msg', 'flow', 'global', 'str'],
|
||||
})
|
||||
.typedInput('value', this.roomValue)
|
||||
.typedInput('type', this.roomType);
|
||||
|
||||
$("#node-input-paginateBackwards").typedInput({
|
||||
types: ['msg', 'flow', 'global', 'bool'],
|
||||
})
|
||||
.typedInput('value', this.paginateBackwardsValue)
|
||||
.typedInput('type', this.paginateBackwardsType);
|
||||
|
||||
$("#node-input-paginateKey").typedInput({
|
||||
types: ['msg', 'flow', 'global'],
|
||||
})
|
||||
.typedInput('value', this.paginateKeyValue)
|
||||
.typedInput('type', this.paginateKeyType);
|
||||
|
||||
$("#node-input-pageSize").typedInput({
|
||||
types: ['msg', 'flow', 'global', 'num'],
|
||||
})
|
||||
.typedInput('value', this.pageSizeValue)
|
||||
.typedInput('type', this.pageSizeType);
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.roomType = $("#node-input-room").typedInput('type');
|
||||
this.roomValue = $("#node-input-room").typedInput('value');
|
||||
this.paginateBackwardsType = $("#node-input-paginateBackwards").typedInput('type');
|
||||
this.paginateBackwardsValue = $("#node-input-paginateBackwards").typedInput('value');
|
||||
this.paginateKeyType = $("#node-input-paginateKey").typedInput('type');
|
||||
this.paginateKeyValue = $("#node-input-paginateKey").typedInput('value');
|
||||
this.pageSizeType = $("#node-input-pageSize").typedInput('type');
|
||||
this.pageSizeValue = $("#node-input-pageSize").typedInput('value');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-paginate-room">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-server"><i class="fa fa-server"></i> Matrix Server</label>
|
||||
<input type="text" id="node-input-server">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||
<input type="text" id="node-input-room">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-paginateKey"><i class="fa fa-key"></i> Pagination Key</label>
|
||||
<input type="text" id="node-input-paginateKey">
|
||||
<div class="form-tips" style="margin-top: 10px;">A unique key to identify the current pagination session. If not provided, a new key will be generated for each session.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-paginateBackwards"><i class="fa fa-arrow-left"></i> Paginate Backwards</label>
|
||||
<input type="text" id="node-input-paginateBackwards">
|
||||
<div class="form-tips" style="margin-top: 10px;">Set to true to paginate backwards (older events). Set to false to paginate forwards (newer events).</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-pageSize"><i class="fa fa-list"></i> Page Size</label>
|
||||
<input type="text" id="node-input-pageSize">
|
||||
<div class="form-tips" style="margin-top: 10px;">Set the number of events to retrieve per pagination call. It's recommended to keep this value at or below 25 to match the current initial synchronization limit. Adjust this based on your server’s load and capacity to avoid throttling or performance issues.</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-paginate-room">
|
||||
<p>
|
||||
The Matrix Paginate Room node allows you to retrieve historical or future events from a Matrix room, moving forwards or backwards through the event timeline.
|
||||
</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<ul>
|
||||
<li><strong>msg</strong> (<em>default</em>): Triggers the pagination action based on the provided room and parameters.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Output 1 (Paginated Data)</strong>: Returns an array of events if more messages are available. Each event contains details such as:
|
||||
<ul>
|
||||
<li><code>msg.encrypted</code> (boolean) - Indicates if the message was encrypted (end-to-end encryption).</li>
|
||||
<li><code>msg.redacted</code> (boolean) - Indicates if the message was redacted (deleted or hidden).</li>
|
||||
<li><code>msg.payload</code> (string) - The message body or content.</li>
|
||||
<li><code>msg.userId</code> (string) - The user ID of the message sender.</li>
|
||||
<li><code>msg.topic</code> (string) - The room ID of the message's origin.</li>
|
||||
<li><code>msg.eventId</code> (string) - The event ID.</li>
|
||||
<li><code>msg.type</code> (string) - The type of message (e.g., <code>m.text</code>, <code>m.image</code>, <code>m.reaction</code>, etc.).</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Output 2 (Error)</strong>: If an error occurs during pagination, the error message is sent to this output.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Dynamic Properties</h3>
|
||||
<p>Some inputs like <strong>Room</strong>, <strong>Pagination Key</strong>, and <strong>Page Size</strong> can be dynamically set using message, flow, or global context variables.</p>
|
||||
|
||||
<h3>Usage</h3>
|
||||
<p>To paginate through a room's timeline, trigger this node with a <code>msg</code> input. The first run will start the timeline, and a unique pagination key will be generated. Future calls can use this key to continue from where you left off. Use the "Paginate Backwards" option to move through older events or set it to false to move forwards through newer events.</p>
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
const sdkPromise = import("matrix-js-sdk");
|
||||
const crypto = require('crypto');
|
||||
|
||||
module.exports = function(RED) {
|
||||
function MatrixPaginateRoom(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
let node = this;
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.roomType = n.roomType;
|
||||
this.roomValue = n.roomValue;
|
||||
this.paginateBackwardsType = n.paginateBackwardsType;
|
||||
this.paginateBackwardsValue = n.paginateBackwardsValue;
|
||||
this.paginateKeyType = n.paginateKeyType;
|
||||
this.paginateKeyValue = n.paginateKeyValue;
|
||||
this.pageSizeType = n.pageSizeType;
|
||||
this.pageSizeValue = n.pageSizeValue;
|
||||
this.timelineWindows = new Map();
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
if (!node.server) {
|
||||
node.error("No configuration node", {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.server.on("disconnected", function(){
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
});
|
||||
|
||||
node.server.on("connected", function() {
|
||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||
});
|
||||
|
||||
node.on("input", async function (msg) {
|
||||
if (!node.server || !node.server.matrixClient) {
|
||||
node.error("No matrix server selected", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
function getToValue(msg, type, property) {
|
||||
let value = property;
|
||||
if (type === "msg") {
|
||||
value = RED.util.getMessageProperty(msg, property);
|
||||
} else if (type === 'flow' || type === 'global') {
|
||||
try {
|
||||
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||
} catch(e2) {
|
||||
throw new Error("Invalid value evaluation");
|
||||
}
|
||||
} else if (type === "bool") {
|
||||
value = (property === 'true');
|
||||
} else if (type === "num") {
|
||||
value = Number(property);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function setToValue(value, type, property) {
|
||||
if (type === 'global' || type === 'flow') {
|
||||
var contextKey = RED.util.parseContextStore(property);
|
||||
if (/\[msg/.test(contextKey.key)) {
|
||||
// The key has a nested msg. reference that must be evaluated first
|
||||
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true);
|
||||
}
|
||||
var target = node.context()[type];
|
||||
target.set(contextKey.key, value, contextKey.store, err => {
|
||||
if (err) {
|
||||
node.error(err, msg);
|
||||
}
|
||||
});
|
||||
} else if (type === 'msg') {
|
||||
if (!RED.util.setMessageProperty(msg, property, value)) {
|
||||
node.warn(RED._("change.errors.no-override", { property: property }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamically load the SDK
|
||||
const sdk = await sdkPromise;
|
||||
const TimelineWindow = sdk.TimelineWindow;
|
||||
const RelationType = sdk.RelationType;
|
||||
// (Filter was imported originally but is not used, so we omit it.)
|
||||
|
||||
let roomId = getToValue(msg, node.roomType, node.roomValue),
|
||||
paginateBackwards = getToValue(msg, node.paginateBackwardsType, node.paginateBackwardsValue),
|
||||
pageSize = getToValue(msg, node.pageSizeType, node.pageSizeValue),
|
||||
pageKey = getToValue(msg, node.paginateKeyType, node.paginateKeyValue);
|
||||
|
||||
let room = node.server.matrixClient.getRoom(roomId);
|
||||
|
||||
if (!room) {
|
||||
throw new Error(`Room ${roomId} does not exist`);
|
||||
}
|
||||
if (pageSize > node.server.initialSyncLimit) {
|
||||
throw new Error(`Page size=${pageSize} cannot exceed initialSyncLimit=${node.server.initialSyncLimit}`);
|
||||
}
|
||||
if (!pageKey) {
|
||||
pageKey = crypto.randomUUID();
|
||||
setToValue(pageKey, node.paginateKeyType, node.paginateKeyValue);
|
||||
}
|
||||
let timelineWindow = node.timelineWindows.get(pageKey),
|
||||
moreMessages = true;
|
||||
if (!timelineWindow) {
|
||||
let timelineSet = room.getUnfilteredTimelineSet();
|
||||
// MatrixClient's option initialSyncLimit gets set to the filter we are using,
|
||||
// so override that value with our pageSize.
|
||||
timelineWindow = new TimelineWindow(node.server.matrixClient, timelineSet);
|
||||
await timelineWindow.load(msg.eventId || null, pageSize);
|
||||
node.timelineWindows.set(pageKey, timelineWindow);
|
||||
} else {
|
||||
moreMessages = await timelineWindow.paginate(paginateBackwards ? 'b' : 'f', pageSize); // 'b' for backwards, 'f' for forwards
|
||||
if (moreMessages) {
|
||||
await timelineWindow.unpaginate(pageSize, !paginateBackwards);
|
||||
}
|
||||
}
|
||||
|
||||
// To avoid errors converting massive MatrixEvent objects to JSON, we omit them.
|
||||
msg.payload = false;
|
||||
msg.start = timelineWindow.getTimelineIndex('b')?.index;
|
||||
msg.end = timelineWindow.getTimelineIndex('f')?.index;
|
||||
if (moreMessages) {
|
||||
msg.payload = timelineWindow.getEvents().map(function(event) {
|
||||
return {
|
||||
encrypted : event.isEncrypted(),
|
||||
redacted : event.isRedacted(),
|
||||
content : event.getContent(),
|
||||
type : (event.getContent()['msgtype'] || event.getType()) || null,
|
||||
payload : (event.getContent()['body'] || event.getContent()) || null,
|
||||
isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread,
|
||||
mentions : event.getContent()["m.mentions"] || null,
|
||||
userId : event.getSender(),
|
||||
// user : node.matrixClient.getUser(event.getSender()),
|
||||
topic : event.getRoomId(),
|
||||
eventId : event.getId(),
|
||||
event : event.getEffectiveEvent(),
|
||||
};
|
||||
});
|
||||
}
|
||||
node.send([msg, null]);
|
||||
} catch(e) {
|
||||
msg.error = `Room pagination error: ${e}`;
|
||||
node.error(msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
}
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-paginate-room", MatrixPaginateRoom);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
reaction: { value: null }
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -26,30 +27,31 @@ module.exports = function(RED) {
|
||||
|
||||
node.on("input", function (msg) {
|
||||
if (!node.server || !node.server.matrixClient) {
|
||||
node.error("No matrix server selected");
|
||||
node.error("No matrix server selected", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || 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;
|
||||
}
|
||||
|
||||
let payload = n.reaction || msg.payload;
|
||||
if(!payload) {
|
||||
node.error('msg.payload must be defined or the reaction configured on the node.');
|
||||
node.error('msg.payload must be defined or the reaction configured on the node.', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
let eventId = msg.referenceEventId || msg.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;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-react", MatrixReact);
|
||||
}
|
||||
@@ -8,14 +8,19 @@
|
||||
outputs:1,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: {"value": null},
|
||||
acceptOwnEvents: {"value": false},
|
||||
acceptText: {"value": true},
|
||||
acceptEmotes: {"value": true},
|
||||
acceptNotices: {"value": true},
|
||||
acceptStickers: {"value": true},
|
||||
acceptReactions: {"value": true},
|
||||
acceptFiles: {"value": true},
|
||||
acceptAudio: {"value": true},
|
||||
acceptImages: {"value": true},
|
||||
acceptVideos: {"value": true},
|
||||
acceptLocations: {"value": true},
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Matrix Receive";
|
||||
@@ -42,64 +47,114 @@
|
||||
<div class="form-row" style="margin-left: 100px;margin-top:10px;font-weight:bold;">
|
||||
Timeline event filters
|
||||
</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
|
||||
type="checkbox"
|
||||
id="node-input-acceptText"
|
||||
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||
/>
|
||||
<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>
|
||||
</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
|
||||
type="checkbox"
|
||||
id="node-input-acceptEmotes"
|
||||
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-acceptStickers"
|
||||
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-acceptReactions"
|
||||
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-acceptFiles"
|
||||
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||
/>
|
||||
<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>
|
||||
</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
|
||||
type="checkbox"
|
||||
id="node-input-acceptImages"
|
||||
style="width: auto; margin-left: 125px; vertical-align: top"
|
||||
/>
|
||||
<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>
|
||||
</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">
|
||||
@@ -172,6 +227,11 @@
|
||||
<dt>msg.content <span class="property-type">object</span></dt>
|
||||
<dd>the message's content object</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="message-properties">
|
||||
<dt>msg.headers <span class="property-type">object | null</span></dt>
|
||||
<dd>for media events, includes auth headers (for example <code>Authorization: Bearer ...</code>) used by authed media endpoints.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>msg.type</code> == '<strong>m.text</strong>'
|
||||
@@ -237,6 +297,38 @@
|
||||
</dl>
|
||||
</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>'
|
||||
<dl class="message-properties">
|
||||
<dt>msg.filename <span class="property-type">string</span></dt>
|
||||
@@ -263,5 +355,39 @@
|
||||
<dd>the image's thumbnail Matrix URL</dd>
|
||||
</dl>
|
||||
</li>
|
||||
|
||||
<li><code>msg.type</code> == '<strong>m.location</strong>'
|
||||
<p>
|
||||
The structured location fields are surfaced at the top level of
|
||||
<code>msg</code> so the message can be wired straight into a
|
||||
<code>matrix-send-location</code> node to resend the location
|
||||
without any field translation in between.
|
||||
</p>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.geo_uri <span class="property-type">string</span></dt>
|
||||
<dd>The RFC 5870 geo URI from the event (e.g. <code>geo:48.85,2.35</code>).</dd>
|
||||
|
||||
<dt>msg.latitude <span class="property-type">number</span></dt>
|
||||
<dd>Latitude in decimal degrees, parsed from the geo URI.</dd>
|
||||
|
||||
<dt>msg.longitude <span class="property-type">number</span></dt>
|
||||
<dd>Longitude in decimal degrees, parsed from the geo URI.</dd>
|
||||
|
||||
<dt class="optional">msg.altitude <span class="property-type">number</span></dt>
|
||||
<dd>Metres above sea level, parsed from the geo URI. Only set when the geo URI includes an altitude component (<code>geo:lat,lng,alt</code>).</dd>
|
||||
|
||||
<dt class="optional">msg.description <span class="property-type">string</span></dt>
|
||||
<dd>The location's label (e.g. <em>Eiffel Tower</em>). Only set when the sender included one.</dd>
|
||||
|
||||
<dt>msg.assetType <span class="property-type">string</span></dt>
|
||||
<dd><code>"m.self"</code> when the sender was sharing their own location, <code>"m.pin"</code> for a generic dropped pin. Defaults to <code>"m.self"</code> when the event does not carry an explicit asset type (per the Matrix spec).</dd>
|
||||
|
||||
<dt class="optional">msg.timestamp <span class="property-type">number</span></dt>
|
||||
<dd>Milliseconds since the UNIX epoch when the location was correct (the event's <code>m.ts</code> field). Only set when the sender included a timestamp.</dd>
|
||||
|
||||
<dt>msg.payload <span class="property-type">string</span></dt>
|
||||
<dd>The event's <code>body</code> — a human-readable text fallback for clients that cannot render the map snippet.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
// Parse an RFC 5870 geo URI into {latitude, longitude, altitude?}.
|
||||
// Returns null if the URI is missing or malformed.
|
||||
function parseGeoUri(uri) {
|
||||
if (typeof uri !== "string" || uri.indexOf("geo:") !== 0) return null;
|
||||
// strip any ";u=..." / ";crs=..." parameters
|
||||
const body = uri.slice(4).split(";")[0];
|
||||
const parts = body.split(",").map(function(s) { return parseFloat(s.trim()); });
|
||||
if (parts.length < 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) return null;
|
||||
const result = { latitude: parts[0], longitude: parts[1] };
|
||||
if (parts.length >= 3 && Number.isFinite(parts[2])) result.altitude = parts[2];
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = function(RED) {
|
||||
function MatrixReceiveMessage(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
@@ -6,21 +19,27 @@ module.exports = function(RED) {
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.acceptOwnEvents = n.acceptOwnEvents;
|
||||
this.acceptText = n.acceptText;
|
||||
this.acceptEmotes = n.acceptEmotes;
|
||||
this.acceptNotices = n.acceptNotices;
|
||||
this.acceptStickers = n.acceptStickers;
|
||||
this.acceptReactions = n.acceptReactions;
|
||||
this.acceptFiles = n.acceptFiles;
|
||||
this.acceptAudio = n.acceptAudio;
|
||||
this.acceptImages = n.acceptImages;
|
||||
this.acceptVideos = n.acceptVideos;
|
||||
this.acceptLocations = n.acceptLocations;
|
||||
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" });
|
||||
|
||||
if (!node.server) {
|
||||
node.error("No configuration node");
|
||||
node.error("No configuration node", {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.server.on("disconnected", function(){
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
@@ -32,76 +51,157 @@ module.exports = function(RED) {
|
||||
|
||||
node.server.on("Room.timeline", async function(event, room, toStartOfTimeline, removed, data, msg) {
|
||||
// if node has a room ID set we only listen on that room
|
||||
if(node.roomIds.length && node.roomIds.indexOf(room.roomId) === -1) {
|
||||
if (node.roomIds.length && !node.roomIds.includes(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(msg.type) {
|
||||
if (!node.acceptOwnEvents && (!event.getSender() || event.getSender().toLowerCase() === node.server.matrixClient.getUserId().toLowerCase())) {
|
||||
node.log(`Ignoring${msg.encrypted ? ' encrypted' : ''} timeline event [${msg.type}]: (${room.name}) ${event.getId()} for reason: own event`);
|
||||
return;
|
||||
}
|
||||
|
||||
const setAuthHeaders = () => {
|
||||
const accessToken = node.server.matrixClient.getAccessToken?.();
|
||||
if (accessToken) {
|
||||
msg.headers = {
|
||||
...(msg.headers || {}),
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const setUrls = (urlKey, encryptedKey) => {
|
||||
const url = msg.encrypted ? msg.content[encryptedKey]?.url : msg.content[urlKey];
|
||||
if (url) {
|
||||
const authenticatedUrl = node.server.matrixClient.mxcUrlToHttp(
|
||||
url,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
msg.url = authenticatedUrl || node.server.matrixClient.mxcUrlToHttp(url);
|
||||
msg.mxc_url = url;
|
||||
setAuthHeaders();
|
||||
}
|
||||
};
|
||||
|
||||
const setThumbnailUrls = (infoKey) => {
|
||||
const thumbnailFile = msg.content.info?.[infoKey];
|
||||
const thumbnailUrl = thumbnailFile?.url;
|
||||
if (thumbnailUrl) {
|
||||
const authenticatedThumbnailUrl = node.server.matrixClient.mxcUrlToHttp(
|
||||
thumbnailUrl,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
msg.thumbnail_url = authenticatedThumbnailUrl || node.server.matrixClient.mxcUrlToHttp(thumbnailUrl);
|
||||
msg.thumbnail_mxc_url = thumbnailUrl;
|
||||
setAuthHeaders();
|
||||
}
|
||||
};
|
||||
|
||||
switch (msg.type) {
|
||||
case 'm.emote':
|
||||
if(!node.acceptEmotes) return;
|
||||
if (!node.acceptEmotes) return;
|
||||
break;
|
||||
|
||||
case 'm.notice':
|
||||
if (!node.acceptNotices) return;
|
||||
break;
|
||||
|
||||
case 'm.text':
|
||||
if(!node.acceptText) return;
|
||||
if (!node.acceptText) return;
|
||||
break;
|
||||
|
||||
case 'm.sticker':
|
||||
if(!node.acceptStickers) return;
|
||||
if(msg.content.info) {
|
||||
if(msg.content.info.thumbnail_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;
|
||||
}
|
||||
}
|
||||
if (!node.acceptStickers) return;
|
||||
setThumbnailUrls('thumbnail_url');
|
||||
setUrls('url', 'url');
|
||||
break;
|
||||
|
||||
case 'm.file':
|
||||
if(!node.acceptFiles) return;
|
||||
if (!node.acceptFiles) return;
|
||||
msg.filename = msg.content.filename || msg.content.body;
|
||||
if(msg.encrypted) {
|
||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
|
||||
msg.mxc_url = msg.content.file.url;
|
||||
} else {
|
||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
||||
msg.mxc_url = msg.content.url;
|
||||
setUrls('url', 'file');
|
||||
break;
|
||||
|
||||
case 'm.audio':
|
||||
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;
|
||||
|
||||
case 'm.image':
|
||||
if(!node.acceptImages) return;
|
||||
if (!node.acceptImages) return;
|
||||
msg.filename = msg.content.filename || msg.content.body;
|
||||
if(msg.encrypted) {
|
||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.file.url);
|
||||
msg.mxc_url = msg.content.file.url;
|
||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_file.url);
|
||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_file.url;
|
||||
} else {
|
||||
msg.url = node.server.matrixClient.mxcUrlToHttp(msg.content.url);
|
||||
msg.mxc_url = msg.content.url;
|
||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(msg.content.info.thumbnail_url);
|
||||
msg.thumbnail_mxc_url = msg.content.info.thumbnail_url;
|
||||
}
|
||||
setUrls('url', 'file');
|
||||
setThumbnailUrls('thumbnail_file');
|
||||
break;
|
||||
|
||||
case 'm.video':
|
||||
if (!node.acceptVideos) return;
|
||||
msg.filename = msg.content.filename || msg.content.body;
|
||||
setUrls('url', 'file');
|
||||
setThumbnailUrls('thumbnail_file');
|
||||
break;
|
||||
|
||||
case 'm.location': {
|
||||
if (!node.acceptLocations) return;
|
||||
msg.geo_uri = msg.content.geo_uri;
|
||||
msg.payload = msg.content.body;
|
||||
// Surface the structured location fields at the top level
|
||||
// so a `matrix-send-location` node wired straight to this
|
||||
// output resends the same location. Both the stable
|
||||
// (m.location / m.asset / m.ts) and the MSC3488-prefixed
|
||||
// namespaces are checked, since Element currently emits
|
||||
// the prefixed form even though the spec is stable.
|
||||
const loc = msg.content["m.location"] || msg.content["org.matrix.msc3488.location"] || {};
|
||||
const asset = msg.content["m.asset"] || msg.content["org.matrix.msc3488.asset"] || {};
|
||||
let ts = msg.content["m.ts"];
|
||||
if (typeof ts !== "number") ts = msg.content["org.matrix.msc3488.ts"];
|
||||
const coords = parseGeoUri(loc.uri || msg.geo_uri);
|
||||
if (coords) {
|
||||
msg.latitude = coords.latitude;
|
||||
msg.longitude = coords.longitude;
|
||||
if (coords.altitude !== undefined) msg.altitude = coords.altitude;
|
||||
}
|
||||
if (loc.description) msg.description = loc.description;
|
||||
msg.assetType = asset.type || "m.self";
|
||||
if (typeof ts === "number") msg.timestamp = ts;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'm.reaction':
|
||||
if(!node.acceptReactions) return;
|
||||
if (!node.acceptReactions) return;
|
||||
msg.info = msg.content["m.relates_to"].info;
|
||||
msg.referenceEventId = msg.content["m.relates_to"].event_id;
|
||||
msg.payload = msg.content["m.relates_to"].key;
|
||||
break;
|
||||
|
||||
default:
|
||||
// node.warn("Unknown event type: " + msg.type);
|
||||
return;
|
||||
}
|
||||
|
||||
node.send(msg);
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-receive", MatrixReceiveMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
reason: { value: null }
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -26,23 +27,24 @@ module.exports = function(RED) {
|
||||
|
||||
node.on("input", function (msg) {
|
||||
if (! node.server || ! node.server.matrixClient) {
|
||||
node.error("No matrix server selected");
|
||||
node.error("No matrix server selected", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || 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;
|
||||
}
|
||||
|
||||
if(!msg.userId) {
|
||||
node.error("msg.userId was not set.");
|
||||
node.error("msg.userId was not set.", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,11 +55,15 @@ module.exports = function(RED) {
|
||||
node.send([msg, null]);
|
||||
})
|
||||
.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;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-room-ban", MatrixBan);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
reason: { value: null }
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -26,23 +27,24 @@ module.exports = function(RED) {
|
||||
|
||||
node.on("input", function (msg) {
|
||||
if (! node.server || ! node.server.matrixClient) {
|
||||
node.error("No matrix server selected");
|
||||
node.error("No matrix server selected", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || 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;
|
||||
}
|
||||
|
||||
if(!msg.userId) {
|
||||
node.error("msg.userId was not set.");
|
||||
node.error("msg.userId was not set.", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,11 +55,15 @@ module.exports = function(RED) {
|
||||
node.send([msg, null]);
|
||||
})
|
||||
.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;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-room-kick", MatrixKick);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,293 @@
|
||||
module.exports = function(RED) {
|
||||
function MatrixRoomStateEvents(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
var node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.roomId = n.roomId;
|
||||
this.rules = n.rules;
|
||||
|
||||
if (!node.server) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
node.server.on("disconnected", function(){
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
});
|
||||
|
||||
node.server.on("connected", function() {
|
||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||
});
|
||||
|
||||
node.on("input", async function (msg) {
|
||||
if (! node.server || ! node.server.matrixClient) {
|
||||
msg.error = "No matrix server selected";
|
||||
node.error(msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!node.server.isConnected()) {
|
||||
msg.error = "Matrix server connection is currently closed";
|
||||
node.error(msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || msg.topic;
|
||||
if(!msg.topic) {
|
||||
msg.error = "Room must be specified in msg.topic or in configuration";
|
||||
node.error(msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
let getterErrors = {},
|
||||
setterErrors = {};
|
||||
|
||||
if(!Array.isArray(node.rules) || !node.rules.length) {
|
||||
node.warn("No rules configured, skipping", msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
function getToValue(msg, rule) {
|
||||
var value = rule.to;
|
||||
if (rule.tot === 'json') {
|
||||
try {
|
||||
value = JSON.parse(rule.to);
|
||||
} catch(e) {
|
||||
throw new Error("Invalid JSON");
|
||||
}
|
||||
} else if (rule.tot === 'bin') {
|
||||
try {
|
||||
value = Buffer.from(JSON.parse(rule.to))
|
||||
} catch(e) {
|
||||
throw new Error("Invalid Binary");
|
||||
}
|
||||
}
|
||||
if (rule.tot === "msg") {
|
||||
value = RED.util.getMessageProperty(msg,rule.to);
|
||||
} else if ((rule.tot === 'flow') || (rule.tot === 'global')) {
|
||||
try {
|
||||
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg);
|
||||
} catch(e2) {
|
||||
throw new Error("Invalid value evaluation");
|
||||
}
|
||||
} else if (rule.tot === 'date') {
|
||||
value = Date.now();
|
||||
} else if (rule.tot === 'jsonata') {
|
||||
try {
|
||||
value = RED.util.evaluateJSONataExpression(rule.to,msg);
|
||||
} catch(e3) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
return;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function setToValue(value, rule) {
|
||||
if(rule.tot === 'global' || rule.tot === 'flow') {
|
||||
var contextKey = RED.util.parseContextStore(rule.to);
|
||||
if (/\[msg/.test(contextKey.key)) {
|
||||
// The key has a nest msg. reference to evaluate first
|
||||
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true)
|
||||
}
|
||||
var target = node.context()[rule.tot];
|
||||
var callback = err => {
|
||||
if (err) {
|
||||
node.error(err, msg);
|
||||
getterErrors[rule.p] = err.message;
|
||||
}
|
||||
}
|
||||
target.set(contextKey.key, value, contextKey.store, callback);
|
||||
} else if(rule.tot === 'msg') {
|
||||
if (!RED.util.setMessageProperty(msg, rule.to, value)) {
|
||||
node.warn(RED._("change.errors.no-override",{property:rule.to}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(let rule of node.rules) {
|
||||
// [
|
||||
// {
|
||||
// "t": "set",
|
||||
// "p": "m.room.topic",
|
||||
// "to": "asdf",
|
||||
// "tot": "str"
|
||||
// }, ...
|
||||
// ]
|
||||
|
||||
let cachedGetters = {};
|
||||
if(rule.t === 'set') {
|
||||
let value;
|
||||
try {
|
||||
value = getToValue(msg, rule);
|
||||
switch(rule.p) {
|
||||
case "m.room.name":
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.name",
|
||||
typeof value === "string"
|
||||
? {name: value}
|
||||
: value);
|
||||
break;
|
||||
case "m.room.topic":
|
||||
if (typeof value === "string") {
|
||||
await node.server.matrixClient.setRoomTopic(msg.topic, value);
|
||||
} else {
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.topic",
|
||||
value
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "m.room.avatar":
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.avatar",
|
||||
typeof value === "string"
|
||||
? {"url": value}
|
||||
: value,
|
||||
"");
|
||||
break;
|
||||
case "m.room.join_rules":
|
||||
if(typeof value !== 'object') {
|
||||
setterErrors[rule.p] = "m.room.join_rules content must be object";
|
||||
} else {
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.join_rules",
|
||||
value,
|
||||
"");
|
||||
}
|
||||
break;
|
||||
case "m.room.canonical_alias":
|
||||
if(typeof value !== 'object') {
|
||||
setterErrors[rule.p] = "m.room.canonical_alias content must be object";
|
||||
} else {
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.canonical_alias",
|
||||
value,
|
||||
"");
|
||||
}
|
||||
break;
|
||||
case "m.space.parent":
|
||||
if (typeof value !== 'object') {
|
||||
setterErrors[rule.p] = "m.space.parent content must be object";
|
||||
} else if (!msg.state_key) {
|
||||
setterErrors[rule.p] = "m.space.parent required msg.state_key input to be set to the child roomId";
|
||||
}else {
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.power_levels",
|
||||
value,
|
||||
msg.state_key);
|
||||
}
|
||||
break;
|
||||
case "m.space.child":
|
||||
if (typeof value !== 'object') {
|
||||
setterErrors[rule.p] = "m.space.child content must be object";
|
||||
} else if (!msg.state_key) {
|
||||
setterErrors[rule.p] = "m.space.child required msg.state_key input to be set to the parent roomId";
|
||||
}else {
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.power_levels",
|
||||
value,
|
||||
msg.state_key);
|
||||
}
|
||||
break;
|
||||
case "m.room.guest_access":
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
"m.room.guest_access",
|
||||
typeof value === "string"
|
||||
? { "guest_access": value }
|
||||
: value,
|
||||
"");
|
||||
break;
|
||||
default:
|
||||
if(typeof value !== 'object') {
|
||||
setterErrors[rule.p] = `${rule.p} content must be object`;
|
||||
} else {
|
||||
await node.server.matrixClient.sendStateEvent(
|
||||
msg.topic,
|
||||
rule.p,
|
||||
value,
|
||||
msg.state_key || "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch(e) {
|
||||
setterErrors[rule.p] = e.message;
|
||||
}
|
||||
} else if(rule.t === 'get') {
|
||||
let value;
|
||||
if(cachedGetters.hasOwnProperty(rule.p)) {
|
||||
value = cachedGetters[rule.p];
|
||||
} else {
|
||||
try {
|
||||
if(rule.ls) {
|
||||
// we opted to lookup from local storage, will fallback to server if necessary
|
||||
let room = node.server.matrixClient.getRoom(msg.topic);
|
||||
if(room) {
|
||||
value = await room.getLiveTimeline().getState("f").getStateEvents(rule.p, "");
|
||||
}
|
||||
}
|
||||
if(!value) {
|
||||
// fetch the latest state event by type from server
|
||||
value = await node.server.matrixClient.getStateEvent(msg.topic, rule.p, "");
|
||||
if(value) {
|
||||
// normalize some simpler events for easier access
|
||||
switch(rule.p) {
|
||||
case "m.room.name":
|
||||
value = value?.name
|
||||
break;
|
||||
case "m.room.topic":
|
||||
value = value?.topic
|
||||
break;
|
||||
case "m.room.avatar":
|
||||
value = value?.url
|
||||
break;
|
||||
case "m.room.guest_access":
|
||||
value = value?.guest_access;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setToValue(value, rule);
|
||||
cachedGetters[rule.p] = value;
|
||||
} catch(e) {
|
||||
getterErrors[rule.p] = e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(setterErrors).length) {
|
||||
msg.setter_errors = setterErrors;
|
||||
}
|
||||
|
||||
if(Object.keys(getterErrors).length) {
|
||||
msg.getter_errors = getterErrors;
|
||||
}
|
||||
|
||||
node.send([msg, null]);
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-room-state-events", MatrixRoomStateEvents);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: "" }
|
||||
},
|
||||
label: function() {
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -31,13 +32,14 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
let roomId = node.roomId || msg.topic;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -65,6 +67,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-room-users", MatrixRoomUsers);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-send-file',{
|
||||
category: 'matrix',
|
||||
category: 'matrix (deprecated)',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
outputLabels: ["success", "error"],
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
contentType: { value: null }
|
||||
},
|
||||
@@ -20,6 +20,26 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -31,8 +32,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || msg.topic;
|
||||
@@ -57,7 +59,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
if(!msg.payload) {
|
||||
node.error('msg.payload is required');
|
||||
node.error('msg.payload is required', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,6 +96,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-send-file", MatrixSendFile);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-send-image',{
|
||||
category: 'matrix',
|
||||
category: 'matrix (deprecated)',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
outputLabels: ["success", "error"],
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
contentType: { value: null }
|
||||
},
|
||||
@@ -20,6 +20,26 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -31,8 +32,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || msg.topic;
|
||||
@@ -57,7 +59,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
if(!msg.payload) {
|
||||
node.error('msg.payload is required');
|
||||
node.error('msg.payload is required', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,6 +100,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-send-image", MatrixSendImage);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-send-location', {
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
server: { type: "matrix-server-config", required: true },
|
||||
roomId: { value: "" },
|
||||
latitudeType: { value: "msg" },
|
||||
latitudeValue: { value: "latitude" },
|
||||
longitudeType: { value: "msg" },
|
||||
longitudeValue: { value: "longitude" },
|
||||
altitudeType: { value: "msg" },
|
||||
altitudeValue: { value: "altitude" },
|
||||
geoUriType: { value: "msg" },
|
||||
geoUriValue: { value: "geo_uri" },
|
||||
descriptionType: { value: "msg" },
|
||||
descriptionValue: { value: "description" },
|
||||
assetTypeType: { value: "msg" },
|
||||
assetTypeValue: { value: "assetType" },
|
||||
timestampType: { value: "msg" },
|
||||
timestampValue: { value: "timestamp" },
|
||||
textType: { value: "msg" },
|
||||
textValue: { value: "payload" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 2,
|
||||
outputLabels: ["success", "error"],
|
||||
icon: "matrix.png",
|
||||
label: function() {
|
||||
return this.name || "Send Location";
|
||||
},
|
||||
paletteLabel: 'Send Location',
|
||||
oneditprepare: function() {
|
||||
var numTypes = ["msg", "flow", "global", "num"];
|
||||
var strTypes = ["msg", "flow", "global", "str"];
|
||||
|
||||
$("#node-input-latitudeValue").typedInput({
|
||||
typeField: "#node-input-latitudeType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-longitudeValue").typedInput({
|
||||
typeField: "#node-input-longitudeType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-altitudeValue").typedInput({
|
||||
typeField: "#node-input-altitudeType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-geoUriValue").typedInput({
|
||||
typeField: "#node-input-geoUriType",
|
||||
types: strTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-descriptionValue").typedInput({
|
||||
typeField: "#node-input-descriptionType",
|
||||
types: strTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-assetTypeValue").typedInput({
|
||||
typeField: "#node-input-assetTypeType",
|
||||
types: [
|
||||
{
|
||||
value: "str",
|
||||
label: "string",
|
||||
options: [
|
||||
{ value: "m.self", label: "My location (m.self)" },
|
||||
{ value: "m.pin", label: "Pin (m.pin)" }
|
||||
]
|
||||
},
|
||||
"msg",
|
||||
"flow",
|
||||
"global"
|
||||
],
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-timestampValue").typedInput({
|
||||
typeField: "#node-input-timestampType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-textValue").typedInput({
|
||||
typeField: "#node-input-textType",
|
||||
types: strTypes,
|
||||
default: "msg"
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-send-location">
|
||||
<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> Server</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="!room:matrix.org">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">Optional. If empty, <code>msg.topic</code> is used.</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-latitudeValue"><i class="fa fa-crosshairs"></i> Latitude</label>
|
||||
<input type="hidden" id="node-input-latitudeType">
|
||||
<input type="text" id="node-input-latitudeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-longitudeValue"><i class="fa fa-crosshairs"></i> Longitude</label>
|
||||
<input type="hidden" id="node-input-longitudeType">
|
||||
<input type="text" id="node-input-longitudeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-altitudeValue"><i class="fa fa-arrow-up"></i> Altitude</label>
|
||||
<input type="hidden" id="node-input-altitudeType">
|
||||
<input type="text" id="node-input-altitudeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. Metres above sea level. When provided the geo URI becomes <code>geo:lat,lng,alt</code>.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-geoUriValue"><i class="fa fa-map"></i> geo_uri</label>
|
||||
<input type="hidden" id="node-input-geoUriType">
|
||||
<input type="text" id="node-input-geoUriValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. A pre-built RFC 5870 geo URI (e.g. <code>geo:48.85,2.35</code>). When set, Latitude / Longitude / Altitude are ignored.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-descriptionValue"><i class="fa fa-info-circle"></i> Description</label>
|
||||
<input type="hidden" id="node-input-descriptionType">
|
||||
<input type="text" id="node-input-descriptionValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. Label for the location (e.g. <em>Eiffel Tower</em>).
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-assetTypeValue"><i class="fa fa-map-marker"></i> Type</label>
|
||||
<input type="hidden" id="node-input-assetTypeType">
|
||||
<input type="text" id="node-input-assetTypeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
<b>My location</b> (<code>m.self</code>) says "this is where I am"; <b>Pin</b> (<code>m.pin</code>) marks a generic pinned point.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-timestampValue"><i class="fa fa-clock-o"></i> Timestamp</label>
|
||||
<input type="hidden" id="node-input-timestampType">
|
||||
<input type="text" id="node-input-timestampValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. Milliseconds since the UNIX epoch. Defaults to <em>now</em> when empty.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-textValue"><i class="fa fa-file-text-o"></i> Body / text</label>
|
||||
<input type="hidden" id="node-input-textType">
|
||||
<input type="text" id="node-input-textValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
Optional. Override the auto-generated text fallback used in <code>body</code> and <code>m.text</code> for clients that can't render the map.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-send-location">
|
||||
<h3>Details</h3>
|
||||
<p>Sends an m.location event to a Matrix room.</p>
|
||||
|
||||
<p>
|
||||
Produces an <code>m.location</code> message just like Element's
|
||||
<em>"Share my location"</em> feature, so the location renders as a map
|
||||
snippet in clients that support it. The event content matches what
|
||||
Element sends — legacy <code>geo_uri</code> + <code>body</code>
|
||||
fields for older clients, plus the modern extensible-events fields
|
||||
(<code>m.location</code>, <code>m.asset</code>, <code>m.text</code>,
|
||||
<code>m.ts</code>) for newer ones. Built with
|
||||
<code>matrix-js-sdk</code>'s <code>makeLocationContent</code> helper so
|
||||
the wire format stays in lockstep with Element's.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Every input below is a <strong>typed input</strong>: pick the source
|
||||
(<code>msg</code>, <code>flow</code>, <code>global</code>,
|
||||
<code>num</code>, or <code>str</code>) and the value. The defaults
|
||||
read from <code>msg.*</code> using the documented names so the simple
|
||||
case <em>just works</em> — reach for the type selector when you
|
||||
want to pull from a flow / global variable or pin a static value on
|
||||
the node itself.
|
||||
</p>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
<dl class="message-properties">
|
||||
<dt>Server</dt>
|
||||
<dd>The Matrix server config node.</dd>
|
||||
|
||||
<dt>Room ID</dt>
|
||||
<dd>Optional default room. If empty, <code>msg.topic</code> is used.</dd>
|
||||
|
||||
<dt>Latitude <span class="property-type">number</span></dt>
|
||||
<dd>Decimal degrees, between -90 and 90. Default: <code>msg.latitude</code>.</dd>
|
||||
|
||||
<dt>Longitude <span class="property-type">number</span></dt>
|
||||
<dd>Decimal degrees, between -180 and 180. Default: <code>msg.longitude</code>.</dd>
|
||||
|
||||
<dt>Altitude <span class="property-type">number, optional</span></dt>
|
||||
<dd>Metres above sea level. When provided, the geo URI becomes <code>geo:lat,lng,alt</code>. Default: <code>msg.altitude</code>.</dd>
|
||||
|
||||
<dt>geo_uri <span class="property-type">string, optional</span></dt>
|
||||
<dd>A pre-built RFC 5870 geo URI (e.g. <code>geo:48.85,2.35</code>). When set, the Latitude / Longitude / Altitude inputs are ignored. Default: <code>msg.geo_uri</code>.</dd>
|
||||
|
||||
<dt>Description <span class="property-type">string, optional</span></dt>
|
||||
<dd>Label for the location (e.g. <em>Eiffel Tower</em>). Default: <code>msg.description</code>.</dd>
|
||||
|
||||
<dt>Type</dt>
|
||||
<dd>
|
||||
<b>My location</b> (<code>m.self</code>) marks the location as
|
||||
"this is where I am right now" (the sender's location).
|
||||
<b>Pin</b> (<code>m.pin</code>) marks it as a generic pinned
|
||||
point. Defaults to <code>msg.assetType</code> (which the Receive
|
||||
node populates for incoming location events, so a Receive node
|
||||
wired straight to this one resends with the same asset type);
|
||||
falls back to <code>m.self</code> when not provided. Flip the
|
||||
type selector to <code>string</code> to pin a static value.
|
||||
</dd>
|
||||
|
||||
<dt>Timestamp <span class="property-type">number, optional</span></dt>
|
||||
<dd>Milliseconds since the UNIX epoch — when the location was correct. Defaults to <em>now</em>. Default source: <code>msg.timestamp</code>.</dd>
|
||||
|
||||
<dt>Body / text <span class="property-type">string, optional</span></dt>
|
||||
<dd>Override the auto-generated text fallback used in <code>body</code> and <code>m.text</code>. If left empty, a sensible default like <code>User Location "Eiffel Tower" geo:48.85,2.35 at 2026-05-23T04:44:41Z</code> is generated. Default source: <code>msg.payload</code> (matches what the Receive node sets for incoming events).</dd>
|
||||
</dl>
|
||||
|
||||
<h4>Inputs</h4>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.topic <span class="property-type">string</span></dt>
|
||||
<dd>Room ID to send the location to. Used when Room ID is not set on the node.</dd>
|
||||
|
||||
<dt class="optional">msg.<configured></dt>
|
||||
<dd>The specific message keys depend on the node configuration. By default the node reads <code>msg.latitude</code>, <code>msg.longitude</code>, <code>msg.altitude</code>, <code>msg.geo_uri</code>, <code>msg.description</code>, <code>msg.assetType</code>, <code>msg.timestamp</code>, and <code>msg.payload</code> — the same field names the Receive node populates for incoming <code>m.location</code> events, so a Receive node wired straight to this one re-sends the location verbatim. Rename or pull from <code>flow</code>/<code>global</code> via the typed-input selectors.</dd>
|
||||
</dl>
|
||||
|
||||
<h4>Outputs</h4>
|
||||
<p>Two outputs — <b>success</b> (top) and <b>error</b> (bottom).</p>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.eventId <span class="property-type">string</span></dt>
|
||||
<dd>The event ID of the sent location message (on the success output).</dd>
|
||||
|
||||
<dt>msg.payload <span class="property-type">object</span></dt>
|
||||
<dd>The full content object that was sent to the room.</dd>
|
||||
|
||||
<dt>msg.error <span class="property-type">object</span></dt>
|
||||
<dd>The error (on the error output, when sending fails).</dd>
|
||||
</dl>
|
||||
</script>
|
||||
@@ -0,0 +1,202 @@
|
||||
// matrix-js-sdk's main entry does not re-export `makeLocationContent`, so we
|
||||
// deep-import the content-helpers module. The SDK has no `exports` field in
|
||||
// its package.json (verified against v41) so subpath imports are stable.
|
||||
const contentHelpersPromise = import("matrix-js-sdk/lib/content-helpers.js");
|
||||
|
||||
module.exports = function(RED) {
|
||||
const VALID_ASSET_TYPES = ["m.self", "m.pin"];
|
||||
|
||||
function MatrixSendLocation(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
let node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.roomId = n.roomId;
|
||||
|
||||
// Dynamic inputs: each has a `*Type` (msg | flow | global | str | num)
|
||||
// and a `*Value` (the property path or literal). Defaults preserve the
|
||||
// documented per-message names so existing flows keep working.
|
||||
this.latitudeType = n.latitudeType || "msg";
|
||||
this.latitudeValue = n.latitudeValue || "latitude";
|
||||
this.longitudeType = n.longitudeType || "msg";
|
||||
this.longitudeValue = n.longitudeValue || "longitude";
|
||||
this.altitudeType = n.altitudeType || "msg";
|
||||
this.altitudeValue = n.altitudeValue || "altitude";
|
||||
this.geoUriType = n.geoUriType || "msg";
|
||||
this.geoUriValue = n.geoUriValue || "geo_uri";
|
||||
this.descriptionType = n.descriptionType || "msg";
|
||||
this.descriptionValue = n.descriptionValue || "description";
|
||||
this.assetTypeType = n.assetTypeType || "msg";
|
||||
this.assetTypeValue = n.assetTypeValue || "assetType";
|
||||
this.timestampType = n.timestampType || "msg";
|
||||
this.timestampValue = n.timestampValue || "timestamp";
|
||||
this.textType = n.textType || "msg";
|
||||
this.textValue = n.textValue || "payload";
|
||||
|
||||
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" });
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve a typed-input pair to its runtime value.
|
||||
* msg | flow | global - read the property path from that source.
|
||||
* num - parse the configured literal as a number;
|
||||
* empty string => undefined.
|
||||
* str - the configured literal; empty string => undefined.
|
||||
* bool - "true" => true, anything else => false.
|
||||
*
|
||||
* Returning `undefined` from this signals "not provided", which is
|
||||
* how optional fields opt out.
|
||||
*/
|
||||
function getToValue(msg, type, property) {
|
||||
if (type === "msg") {
|
||||
return RED.util.getMessageProperty(msg, property);
|
||||
}
|
||||
if (type === "flow" || type === "global") {
|
||||
try {
|
||||
return RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid " + type + " value evaluation for '" + property + "'");
|
||||
}
|
||||
}
|
||||
if (property === "" || property === undefined || property === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (type === "num") {
|
||||
const n = Number(property);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
if (type === "bool") {
|
||||
return property === "true";
|
||||
}
|
||||
// str / default
|
||||
return property;
|
||||
}
|
||||
|
||||
function isEmpty(v) {
|
||||
return v === undefined || v === null || v === "";
|
||||
}
|
||||
|
||||
node.on("input", async function(msg) {
|
||||
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.error("Room must be specified in msg.topic or in configuration", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve every typed input up-front so a single bad config or
|
||||
// flow/global lookup surfaces as one clear error.
|
||||
let rawLat, rawLng, rawAlt, rawGeoUri, description, assetType, rawTimestamp, text;
|
||||
try {
|
||||
rawLat = getToValue(msg, node.latitudeType, node.latitudeValue);
|
||||
rawLng = getToValue(msg, node.longitudeType, node.longitudeValue);
|
||||
rawAlt = getToValue(msg, node.altitudeType, node.altitudeValue);
|
||||
rawGeoUri = getToValue(msg, node.geoUriType, node.geoUriValue);
|
||||
description = getToValue(msg, node.descriptionType, node.descriptionValue);
|
||||
assetType = getToValue(msg, node.assetTypeType, node.assetTypeValue);
|
||||
rawTimestamp = getToValue(msg, node.timestampType, node.timestampValue);
|
||||
text = getToValue(msg, node.textType, node.textValue);
|
||||
} catch (e) {
|
||||
node.error(e.message, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the geo URI: prefer an explicit geo_uri when supplied;
|
||||
// otherwise build geo:<lat>,<lng>[,<alt>] from numeric inputs.
|
||||
let geoUri = isEmpty(rawGeoUri) ? null : String(rawGeoUri);
|
||||
if (!geoUri) {
|
||||
const lat = parseFloat(rawLat);
|
||||
const lng = parseFloat(rawLng);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
||||
node.error("Latitude and longitude (numbers) - or a geo_uri - are required", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
if (lat < -90 || lat > 90) {
|
||||
node.error("Latitude (" + lat + ") is out of range; must be between -90 and 90", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
if (lng < -180 || lng > 180) {
|
||||
node.error("Longitude (" + lng + ") is out of range; must be between -180 and 180", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
geoUri = "geo:" + lat + "," + lng;
|
||||
if (!isEmpty(rawAlt)) {
|
||||
const alt = parseFloat(rawAlt);
|
||||
if (!Number.isFinite(alt)) {
|
||||
node.error("Altitude must be a number when provided", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
geoUri = "geo:" + lat + "," + lng + "," + alt;
|
||||
}
|
||||
}
|
||||
|
||||
// Asset type defaults to m.self if the resolved value is empty.
|
||||
if (isEmpty(assetType)) {
|
||||
assetType = "m.self";
|
||||
}
|
||||
if (VALID_ASSET_TYPES.indexOf(assetType) === -1) {
|
||||
node.error('Invalid asset type "' + assetType + '"; must be "m.self" or "m.pin"', msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Timestamp the location was correct, in ms since the UNIX epoch.
|
||||
let timestamp = Date.now();
|
||||
if (!isEmpty(rawTimestamp)) {
|
||||
const ts = Number(rawTimestamp);
|
||||
if (Number.isFinite(ts)) {
|
||||
timestamp = ts;
|
||||
}
|
||||
}
|
||||
|
||||
// makeLocationContent uses `undefined` to mean "generate a default".
|
||||
const cleanDescription = isEmpty(description) ? undefined : String(description);
|
||||
const cleanText = isEmpty(text) ? undefined : String(text);
|
||||
|
||||
try {
|
||||
const { makeLocationContent } = await contentHelpersPromise;
|
||||
const content = makeLocationContent(cleanText, geoUri, timestamp, cleanDescription, assetType);
|
||||
const response = await node.server.matrixClient.sendMessage(msg.topic, content);
|
||||
msg.eventId = response.event_id;
|
||||
msg.payload = content;
|
||||
node.send([msg, null]);
|
||||
} catch (e) {
|
||||
node.error("Error sending location: " + e, msg);
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
}
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("matrix-send-location", MatrixSendLocation);
|
||||
};
|
||||
@@ -6,19 +6,32 @@
|
||||
outputLabels: ["success", "error"],
|
||||
inputs:1,
|
||||
outputs:2,
|
||||
paletteLabel: 'Send Message',
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
message: { value: null },
|
||||
messageType: { value: 'm.text' },
|
||||
messageFormat: { value: '' },
|
||||
replaceMessage : { value: false }
|
||||
replaceMessage : { value: false },
|
||||
threadReplyType: { value: "msg" },
|
||||
threadReplyValue: { value: "isThread" },
|
||||
},
|
||||
label: function() {
|
||||
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>
|
||||
|
||||
@@ -43,6 +56,9 @@
|
||||
<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
|
||||
@@ -69,12 +85,21 @@
|
||||
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 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">
|
||||
<label for="node-input-messageFormat">
|
||||
Message Format
|
||||
</label>
|
||||
<select id="node-input-messageFormat">
|
||||
<option value="">Default (plaintext)</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="msg.format">msg.format input</option>
|
||||
</select>
|
||||
@@ -104,9 +129,9 @@
|
||||
<dd> Room ID to send image to. Optional if configured on the node. If configured on the node this input will be overridden.</dd>
|
||||
|
||||
<dt>msg.payload
|
||||
<span class="property-type">string</span>
|
||||
<span class="property-type">string|object</span>
|
||||
</dt>
|
||||
<dd> the message text. If configured on the node this is ignored otherwise it required. </dd>
|
||||
<dd> the message text or an object to customize the full content. If configured on the node this is ignored otherwise it required. </dd>
|
||||
|
||||
<dt>msg.replace
|
||||
<span class="property-type">bool</span>
|
||||
@@ -116,7 +141,7 @@
|
||||
<dt class="optional">msg.formatted_payload
|
||||
<span class="property-type">string</span>
|
||||
</dt>
|
||||
<dd> the formatted HTML message (uses <code>msg.payload</code> 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 messages sent in <strong>HTML</strong> format — in Markdown mode the formatted body is generated from the markdown source.</dd>
|
||||
|
||||
<dt class="optional">msg.type
|
||||
<span class="property-type">string | null</span>
|
||||
@@ -126,7 +151,35 @@
|
||||
<dt class="optional">msg.format
|
||||
<span class="property-type">string | null</span>
|
||||
</dt>
|
||||
<dd> This is only used and required when configured so on the node. Set to <code>null</code> for plain text and <code>'html'</code> for HTML.</dd>
|
||||
<dd> This is only used and required when configured so on the node. Set to <code>null</code> for plain text, <code>'markdown'</code> for markdown (converted to HTML the same way Element does), or <code>'html'</code> for HTML.</dd>
|
||||
</dl>
|
||||
|
||||
<h4>Message formats</h4>
|
||||
<dl class="message-properties">
|
||||
<dt>Default (plaintext)</dt>
|
||||
<dd>The payload is sent as-is as the message body.</dd>
|
||||
|
||||
<dt>Markdown</dt>
|
||||
<dd>
|
||||
The payload is parsed as CommonMark markdown and converted to HTML
|
||||
the same way Element does (using the same converter ported from
|
||||
<code>matrix-react-sdk</code>). If the message turns out to contain
|
||||
no markdown syntax it is sent as plain text; otherwise the original
|
||||
markdown source becomes the message <code>body</code> and the
|
||||
rendered HTML is sent as <code>formatted_body</code>, so clients
|
||||
without HTML rendering still see a readable fallback.
|
||||
</dd>
|
||||
|
||||
<dt>HTML</dt>
|
||||
<dd>
|
||||
The payload is sent as HTML. By default the same HTML is used for
|
||||
both the plain-text and formatted versions; set
|
||||
<code>msg.formatted_payload</code> if you want the
|
||||
<code>formatted_body</code> to differ from <code>msg.payload</code>.
|
||||
</dd>
|
||||
|
||||
<dt>msg.format input</dt>
|
||||
<dd>Set <code>msg.format</code> at runtime to one of the options above (<code>null</code>, <code>'markdown'</code>, or <code>'html'</code>).</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const {RelationType} = require("matrix-js-sdk");
|
||||
const sdkPromise = import("matrix-js-sdk");
|
||||
const { Markdown } = require("./matrix-markdown");
|
||||
|
||||
module.exports = function(RED) {
|
||||
function MatrixSendImage(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
var node = this;
|
||||
|
||||
this.name = n.name;
|
||||
@@ -13,6 +13,8 @@ module.exports = function(RED) {
|
||||
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
|
||||
this.allowedTags = [
|
||||
@@ -54,6 +56,7 @@ module.exports = function(RED) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
@@ -65,25 +68,33 @@ module.exports = function(RED) {
|
||||
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,
|
||||
msgFormat = node.messageFormat;
|
||||
|
||||
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;
|
||||
}
|
||||
msgFormat = node.messageFormat,
|
||||
threadReply = getToValue(msg, node.threadReplyType, node.threadReplyValue);
|
||||
|
||||
if (!node.server || !node.server.matrixClient) {
|
||||
node.warn("No matrix server selected");
|
||||
@@ -91,7 +102,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
@@ -104,54 +115,119 @@ module.exports = function(RED) {
|
||||
|
||||
let payload = n.message || msg.payload;
|
||||
if(!payload) {
|
||||
node.error('msg.payload must be defined or the message configured on the node.');
|
||||
node.error('msg.payload must be defined or the message configured on the node.', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
let 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'];
|
||||
let content = null;
|
||||
if (typeof payload === 'object') {
|
||||
content = payload;
|
||||
} else {
|
||||
if (msgType === 'msg.type') {
|
||||
if (!msg.type) {
|
||||
node.error("msg.type is set to be passed in via msg.type but was not defined", msg);
|
||||
return;
|
||||
}
|
||||
msgType = msg.type;
|
||||
}
|
||||
|
||||
content['m.relates_to'] = {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: msg.eventId
|
||||
if (msgFormat === 'msg.format') {
|
||||
if (!Object.hasOwn(msg, 'format')) {
|
||||
node.error("Message format is set to be passed in via msg.format but was not defined", msg);
|
||||
return;
|
||||
}
|
||||
msgFormat = msg.format;
|
||||
}
|
||||
|
||||
content = {
|
||||
msgtype: msgType,
|
||||
body: payload.toString()
|
||||
};
|
||||
content['body'] = ' * ' + content['body'];
|
||||
|
||||
if (msgFormat === 'markdown') {
|
||||
// Convert the markdown body to HTML using the same logic
|
||||
// as Element (matrix-react-sdk's `Markdown` class).
|
||||
//
|
||||
// If the message contains any markdown syntax, send the
|
||||
// rendered HTML as `formatted_body` and keep the original
|
||||
// markdown source as `body` (matrix spec convention for
|
||||
// formatted messages). If the message turns out to be
|
||||
// plain text and contains backslash escapes, strip those
|
||||
// from `body` and send no HTML; otherwise leave `body`
|
||||
// as the original payload.
|
||||
const source = payload.toString();
|
||||
const md = new Markdown(source);
|
||||
if (md.isPlainText()) {
|
||||
if (source.indexOf("\\") > -1) {
|
||||
content.body = md.toPlaintext();
|
||||
}
|
||||
} else {
|
||||
content.format = "org.matrix.custom.html";
|
||||
content.formatted_body = md.toHTML();
|
||||
}
|
||||
} else 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)
|
||||
.then(function(e) {
|
||||
node.log("Message sent: " + payload);
|
||||
msg.eventId = e.event_id;
|
||||
node.log(JSON.stringify(e));
|
||||
node.send([msg, null]);
|
||||
})
|
||||
.catch(function(e){
|
||||
node.warn("Error sending message " + e);
|
||||
node.error("Error sending message: " + e, {});
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-send-message", MatrixSendImage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,18 +30,766 @@
|
||||
deviceLabel: { type: "text", required: false },
|
||||
accessToken: { type: "password", required: true },
|
||||
deviceId: { type: "text", required: false },
|
||||
secretStoragePassphrase: { type: "password", required: false },
|
||||
url: { type: "text", required: true },
|
||||
password: { type: "password", required: false },
|
||||
},
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
autoAcceptRoomInvites: { value: true },
|
||||
enableE2ee: { type: "checkbox", value: true },
|
||||
global: { type: "checkbox", value: true }
|
||||
global: { type: "checkbox", value: true },
|
||||
allowUnknownDevices: { type: "checkbox", value: true }
|
||||
},
|
||||
icon: "matrix.png",
|
||||
label: function() {
|
||||
return this.name || undefined;
|
||||
},
|
||||
oneditprepare: function() {
|
||||
const nodeId = this.id;
|
||||
|
||||
// --- Secure backup / cross-signing setup (modal dialog) ---
|
||||
// The modal is built once and reused across editor sessions; the
|
||||
// current node id is stored on the overlay so its handlers target
|
||||
// whichever server config node is being edited.
|
||||
if (!document.getElementById("matrix-sb-overlay")) {
|
||||
$('<style>'
|
||||
+ '.matrix-sb-overlay{position:fixed;inset:0;z-index:3000;display:none;background:rgba(0,0,0,.45);}'
|
||||
+ '.matrix-sb-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);'
|
||||
+ 'width:540px;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;'
|
||||
+ 'border-radius:6px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.45);'
|
||||
+ 'background:var(--red-ui-primary-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);}'
|
||||
+ '.matrix-sb-head{display:flex;justify-content:space-between;align-items:center;'
|
||||
+ 'padding:12px 16px;font-size:15px;font-weight:bold;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-sb-x{cursor:pointer;font-size:20px;line-height:1;opacity:.55;}'
|
||||
+ '.matrix-sb-x:hover{opacity:1;}'
|
||||
+ '.matrix-sb-body{padding:16px;overflow:auto;}'
|
||||
+ '.matrix-sb-state{display:flex;gap:10px;align-items:flex-start;font-size:14px;line-height:1.5;}'
|
||||
+ '.matrix-sb-section{margin-top:18px;}'
|
||||
+ '.matrix-sb-section label{display:block;font-weight:bold;margin-bottom:5px;}'
|
||||
+ '.matrix-sb-section input{width:100%;box-sizing:border-box;padding:7px 9px;border-radius:4px;'
|
||||
+ 'border:1px solid var(--red-ui-form-input-border-color,#ccc);'
|
||||
+ 'background:var(--red-ui-form-input-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);}'
|
||||
+ '.matrix-sb-hint{color:var(--red-ui-secondary-text-color,#888);font-size:12px;margin-top:5px;}'
|
||||
+ '.matrix-sb-actions{margin-top:12px;text-align:right;}'
|
||||
+ '.matrix-sb-warn{background:#fdf3e7;border:1px solid #f0c36d;color:#7a5b16;'
|
||||
+ 'border-radius:4px;padding:9px 11px;font-size:13px;margin-bottom:12px;}'
|
||||
+ '.matrix-sb-result{margin-top:18px;padding:11px 13px;border-radius:4px;font-size:13px;white-space:pre-wrap;}'
|
||||
+ '.matrix-sb-result.ok{background:#e7f4ea;border:1px solid #8fcea5;color:#1e6b33;}'
|
||||
+ '.matrix-sb-result.err{background:#fde7e9;border:1px solid #e8a0a8;color:#8a1f2b;}'
|
||||
+ '.matrix-sb-key{margin-top:9px;padding:9px;border-radius:4px;font-family:monospace;'
|
||||
+ 'font-size:13px;word-break:break-all;background:rgba(127,127,127,.16);border:1px dashed #999;}'
|
||||
+ '.matrix-sb-foot{display:flex;justify-content:flex-end;gap:8px;padding:10px 16px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-top:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '</style>').appendTo("head");
|
||||
|
||||
$('<div id="matrix-sb-overlay" class="matrix-sb-overlay"><div class="matrix-sb-modal">'
|
||||
+ '<div class="matrix-sb-head"><span><i class="fa fa-shield"></i> Secure Backup & Cross-signing</span>'
|
||||
+ '<span class="matrix-sb-x" id="matrix-sb-x" title="Close">×</span></div>'
|
||||
+ '<div class="matrix-sb-body">'
|
||||
+ '<div class="matrix-sb-state" id="matrix-sb-state"></div>'
|
||||
+ '<div class="matrix-sb-section" id="matrix-sb-unlock" style="display:none;">'
|
||||
+ '<label>Recovery key or passphrase</label>'
|
||||
+ '<input type="text" id="matrix-sb-recoverykey" placeholder="EsTx xxxx xxxx xxxx ...">'
|
||||
+ '<div class="matrix-sb-hint">The recovery key created when secure backup / key storage was first set up on this account.</div>'
|
||||
+ '<div class="matrix-sb-actions"><button type="button" class="red-ui-button" id="matrix-sb-unlock-btn">Unlock & set up cross-signing</button></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-sb-section" id="matrix-sb-reset" style="display:none;">'
|
||||
+ '<div class="matrix-sb-warn">Resetting creates new cross-signing keys and a new recovery key, replacing the existing ones. Other sessions that trusted the old identity will need to be re-verified.</div>'
|
||||
+ '<label>Account password</label>'
|
||||
+ '<input type="password" id="matrix-sb-password" placeholder="Account password">'
|
||||
+ '<div class="matrix-sb-actions"><button type="button" class="red-ui-button" id="matrix-sb-reset-btn">Reset cross-signing & secure backup</button></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-sb-result" id="matrix-sb-result" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-sb-foot">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-sb-reset-toggle" style="display:none;">Reset instead…</button>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-sb-close">Close</button>'
|
||||
+ '</div></div></div>').appendTo(document.body);
|
||||
|
||||
var sbEsc = function(s) { return $("<div>").text(s == null ? "" : String(s)).html(); };
|
||||
var sbClose = function() { $("#matrix-sb-overlay").fadeOut(120); };
|
||||
var sbId = function() { return $("#matrix-sb-overlay").data("matrixNodeId"); };
|
||||
var sbBtns = function(disabled) { $("#matrix-sb-unlock-btn,#matrix-sb-reset-btn").prop("disabled", disabled); };
|
||||
var sbCall = function(body) {
|
||||
return $.ajax({
|
||||
url: "matrix-chat/secure-backup", type: "POST",
|
||||
contentType: "application/json", data: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
var sbState = function(icon, color, html) {
|
||||
$("#matrix-sb-state").html('<i class="fa ' + icon + '" style="color:' + color + ';font-size:18px;"></i><span>' + html + '</span>');
|
||||
};
|
||||
var sbResult = function(ok, text, key) {
|
||||
var h = sbEsc(text);
|
||||
if (key) { h += '<div class="matrix-sb-key">' + sbEsc(key) + '</div>'; }
|
||||
$("#matrix-sb-result").removeClass("ok err").addClass(ok ? "ok" : "err").html(h).show();
|
||||
};
|
||||
var sbStatus = function() {
|
||||
$("#matrix-sb-unlock,#matrix-sb-reset,#matrix-sb-result,#matrix-sb-reset-toggle").hide();
|
||||
sbState("fa-spinner fa-spin", "#888", "Checking the account…");
|
||||
sbCall({ id: sbId(), action: "status" }).done(function(data) {
|
||||
if (data.result !== "ok") {
|
||||
sbState("fa-exclamation-triangle", "#c9302c", "Could not check the account.");
|
||||
sbResult(false, data.message || "Unknown error");
|
||||
return;
|
||||
}
|
||||
if (data.crossSigningReady) {
|
||||
sbState("fa-check-circle", "#3a9a4e", "<b>Cross-signing is set up.</b> The bot's device is cross-signed.");
|
||||
$("#matrix-sb-reset-toggle").show();
|
||||
} else if (data.secretStorageExists) {
|
||||
sbState("fa-lock", "#d18a1b", "This account has an existing secure backup. Enter its recovery key to set up cross-signing for the bot.");
|
||||
$("#matrix-sb-recoverykey").val("");
|
||||
$("#matrix-sb-unlock,#matrix-sb-reset-toggle").show();
|
||||
} else {
|
||||
sbState("fa-shield", "#888", "No secure backup exists yet. Set one up to enable cross-signing.");
|
||||
$("#matrix-sb-password").val("");
|
||||
$("#matrix-sb-reset").show();
|
||||
}
|
||||
sbBtns(false);
|
||||
}).fail(function() {
|
||||
sbState("fa-exclamation-triangle", "#c9302c", "Request failed — is Node-RED still running?");
|
||||
});
|
||||
};
|
||||
|
||||
$("#matrix-sb-x,#matrix-sb-close").on("click", sbClose);
|
||||
$("#matrix-sb-overlay").on("mousedown", function(e) { if (e.target === this) { sbClose(); } });
|
||||
$(document).on("keydown.matrixsb", function(e) {
|
||||
if (e.key === "Escape" && $("#matrix-sb-overlay").is(":visible")) { sbClose(); }
|
||||
});
|
||||
$("#matrix-sb-reset-toggle").on("click", function() {
|
||||
$(this).hide();
|
||||
$("#matrix-sb-password").val("");
|
||||
$("#matrix-sb-reset").show();
|
||||
});
|
||||
$("#matrix-sb-unlock-btn").on("click", function() {
|
||||
sbBtns(true);
|
||||
sbState("fa-spinner fa-spin", "#888", "Unlocking secure backup…");
|
||||
sbCall({ id: sbId(), action: "unlock", recoveryKey: $("#matrix-sb-recoverykey").val() })
|
||||
.done(function(data) {
|
||||
if (data.result !== "ok") {
|
||||
sbState("fa-lock", "#d18a1b", "Enter the recovery key to set up cross-signing.");
|
||||
sbResult(false, data.message); sbBtns(false); return;
|
||||
}
|
||||
$("#matrix-sb-unlock,#matrix-sb-reset,#matrix-sb-reset-toggle").hide();
|
||||
sbState("fa-check-circle", "#3a9a4e", "<b>Done.</b>");
|
||||
sbResult(true, data.message);
|
||||
})
|
||||
.fail(function() { sbResult(false, "Request failed — is Node-RED still running?"); sbBtns(false); });
|
||||
});
|
||||
$("#matrix-sb-reset-btn").on("click", function() {
|
||||
sbBtns(true);
|
||||
sbState("fa-spinner fa-spin", "#888", "Resetting cross-signing & secure backup…");
|
||||
sbCall({ id: sbId(), action: "reset", password: $("#matrix-sb-password").val() })
|
||||
.done(function(data) {
|
||||
if (data.result !== "ok") {
|
||||
sbState("fa-shield", "#d18a1b", "Enter the account password to reset.");
|
||||
sbResult(false, data.message); sbBtns(false); return;
|
||||
}
|
||||
$("#matrix-sb-unlock,#matrix-sb-reset,#matrix-sb-reset-toggle").hide();
|
||||
sbState("fa-check-circle", "#3a9a4e", "<b>Reset complete.</b>");
|
||||
sbResult(true, data.message, data.recoveryKey);
|
||||
})
|
||||
.fail(function() { sbResult(false, "Request failed — is Node-RED still running?"); sbBtns(false); });
|
||||
});
|
||||
|
||||
// expose the status loader so per-session click handlers can call it
|
||||
$("#matrix-sb-overlay").data("sbStatusFn", sbStatus);
|
||||
}
|
||||
|
||||
$("#matrix-secure-backup-btn").on("click", function() {
|
||||
$("#matrix-sb-overlay").data("matrixNodeId", nodeId).fadeIn(120);
|
||||
$("#matrix-sb-overlay").data("sbStatusFn")();
|
||||
});
|
||||
|
||||
// --- Verification list (modal) ---
|
||||
// Built once and reused; the node id is stored on the overlay.
|
||||
if (!document.getElementById("matrix-vl-overlay")) {
|
||||
$('<style>'
|
||||
+ '.matrix-vl-overlay{position:fixed;inset:0;z-index:3000;display:none;background:rgba(0,0,0,.45);}'
|
||||
+ '.matrix-vl-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);'
|
||||
+ 'width:560px;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;'
|
||||
+ 'border-radius:6px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.45);'
|
||||
+ 'background:var(--red-ui-primary-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);}'
|
||||
+ '.matrix-vl-head{display:flex;justify-content:space-between;align-items:center;'
|
||||
+ 'padding:12px 16px;font-size:15px;font-weight:bold;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-vl-x{cursor:pointer;font-size:20px;line-height:1;opacity:.55;}'
|
||||
+ '.matrix-vl-x:hover{opacity:1;}'
|
||||
+ '.matrix-vl-body{padding:14px 16px;overflow:auto;}'
|
||||
+ '.matrix-vl-note{font-size:12px;color:var(--red-ui-secondary-text-color,#888);margin-bottom:10px;}'
|
||||
+ '.matrix-vl-item{display:flex;justify-content:space-between;gap:12px;padding:10px 12px;'
|
||||
+ 'margin-bottom:8px;border-radius:5px;cursor:pointer;'
|
||||
+ 'border:1px solid var(--red-ui-secondary-border-color,#ddd);'
|
||||
+ 'background:var(--red-ui-secondary-background,#f7f7f7);}'
|
||||
+ '.matrix-vl-item:hover{border-color:var(--red-ui-node-border,#999);}'
|
||||
+ '.matrix-vl-item-l{flex:1;min-width:0;}'
|
||||
+ '.matrix-vl-item-title{font-weight:bold;}'
|
||||
+ '.matrix-vl-item-sub{font-size:12px;color:var(--red-ui-secondary-text-color,#888);'
|
||||
+ 'margin-top:2px;word-break:break-word;}'
|
||||
+ '.matrix-vl-item-r{text-align:right;font-size:12px;white-space:nowrap;}'
|
||||
+ '.matrix-vl-exp{color:#d18a1b;}'
|
||||
+ '.matrix-vl-empty,.matrix-vl-more{font-size:13px;'
|
||||
+ 'color:var(--red-ui-secondary-text-color,#888);padding:6px 2px;}'
|
||||
+ '.matrix-vl-d-state{display:flex;gap:10px;align-items:flex-start;font-size:14px;line-height:1.5;}'
|
||||
+ '.matrix-vl-d-state .fa{font-size:18px;}'
|
||||
+ '.matrix-vl-sas{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0;}'
|
||||
+ '.matrix-vl-emoji{width:88px;text-align:center;padding:8px 4px;border-radius:5px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-vl-emoji .e{font-size:30px;line-height:1.3;}'
|
||||
+ '.matrix-vl-emoji .n{font-size:11px;color:var(--red-ui-secondary-text-color,#888);text-transform:capitalize;}'
|
||||
+ '.matrix-vl-result{margin-top:12px;padding:10px 12px;border-radius:4px;font-size:13px;}'
|
||||
+ '.matrix-vl-foot{display:flex;align-items:center;gap:8px;padding:10px 16px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-top:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '</style>').appendTo("head");
|
||||
|
||||
$('<div id="matrix-vl-overlay" class="matrix-vl-overlay"><div class="matrix-vl-modal">'
|
||||
+ '<div class="matrix-vl-head"><span><i class="fa fa-check-circle"></i> Device Verification</span>'
|
||||
+ '<span class="matrix-vl-x" id="matrix-vl-x" title="Close">×</span></div>'
|
||||
+ '<div class="matrix-vl-body">'
|
||||
+ '<div id="matrix-vl-listview">'
|
||||
+ '<div class="matrix-vl-note">Pending verification requests — this list refreshes every 5 seconds. Click a request to verify it.</div>'
|
||||
+ '<div id="matrix-vl-items"></div>'
|
||||
+ '<div id="matrix-vl-empty" class="matrix-vl-empty" style="display:none;">No pending verification requests.</div>'
|
||||
+ '<div id="matrix-vl-more" class="matrix-vl-more" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '<div id="matrix-vl-detailview" style="display:none;">'
|
||||
+ '<div id="matrix-vl-d-head" class="matrix-vl-item-sub" style="margin-bottom:10px;"></div>'
|
||||
+ '<div id="matrix-vl-d-state" class="matrix-vl-d-state"></div>'
|
||||
+ '<div id="matrix-vl-d-sas" class="matrix-vl-sas" style="display:none;"></div>'
|
||||
+ '<div id="matrix-vl-d-actions" style="display:none;text-align:right;">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-d-mismatch">They don't match</button> '
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-d-confirm">They match</button>'
|
||||
+ '</div>'
|
||||
+ '<div id="matrix-vl-d-result" class="matrix-vl-result" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-vl-foot">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-back" style="display:none;">← Back to list</button>'
|
||||
+ '<span style="flex:1;"></span>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-cancel" style="display:none;">Cancel verification</button>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-close">Close</button>'
|
||||
+ '</div></div></div>').appendTo(document.body);
|
||||
|
||||
var vlListTimer = null, vlTickTimer = null, vlDetailTimer = null;
|
||||
var vlEsc = function(s) { return $("<div>").text(s == null ? "" : String(s)).html(); };
|
||||
var vlId = function() { return $("#matrix-vl-overlay").data("matrixNodeId"); };
|
||||
var vlCurId = function() { return $("#matrix-vl-overlay").data("vlCurrentId"); };
|
||||
var vlCall = function(body) {
|
||||
return $.ajax({
|
||||
url: "matrix-chat/verification", type: "POST",
|
||||
contentType: "application/json", data: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
var vlClearTimers = function() {
|
||||
if(vlListTimer) { clearInterval(vlListTimer); vlListTimer = null; }
|
||||
if(vlTickTimer) { clearInterval(vlTickTimer); vlTickTimer = null; }
|
||||
if(vlDetailTimer) { clearInterval(vlDetailTimer); vlDetailTimer = null; }
|
||||
};
|
||||
var vlDur = function(ms) {
|
||||
var s = Math.max(0, Math.round(ms / 1000));
|
||||
if(s < 60) { return s + "s"; }
|
||||
var m = Math.floor(s / 60), r = s % 60;
|
||||
return m + "m " + (r < 10 ? "0" : "") + r + "s";
|
||||
};
|
||||
var vlState = function(icon, color, html) {
|
||||
$("#matrix-vl-d-state").html('<i class="fa ' + icon + '" style="color:' + color + ';"></i><span>' + html + '</span>');
|
||||
};
|
||||
|
||||
var vlTick = function() {
|
||||
var now = Date.now();
|
||||
$("#matrix-vl-items .matrix-vl-item").each(function() {
|
||||
var $it = $(this);
|
||||
var seen = parseInt($it.attr("data-seen"), 10);
|
||||
var expires = $it.attr("data-expires");
|
||||
$it.find(".matrix-vl-age").text(isNaN(seen) ? "" : "age " + vlDur(now - seen));
|
||||
if(expires) {
|
||||
var left = parseInt(expires, 10) - now;
|
||||
$it.find(".matrix-vl-exp").text(left > 0 ? "expires in " + vlDur(left) : "expired");
|
||||
}
|
||||
});
|
||||
};
|
||||
var vlRenderList = function(data) {
|
||||
if(data.result !== "ok") {
|
||||
$("#matrix-vl-items").html('<div class="matrix-vl-empty">' + vlEsc(data.message) + '</div>');
|
||||
$("#matrix-vl-empty,#matrix-vl-more").hide();
|
||||
return;
|
||||
}
|
||||
var now = Date.now();
|
||||
var $items = $("#matrix-vl-items").empty();
|
||||
(data.verifications || []).forEach(function(v) {
|
||||
var typeLabel = v.type === "room" ? "Room verification" : "Device verification";
|
||||
var sub = "from " + vlEsc(v.userId || "unknown");
|
||||
if(v.type === "device" && v.deviceId) { sub += " · device " + vlEsc(v.deviceId); }
|
||||
else if(v.type === "room" && v.roomId) { sub += " · in room " + vlEsc(v.roomId); }
|
||||
if(v.isSelfVerification) { sub += " · your own session"; }
|
||||
$items.append($('<div class="matrix-vl-item">')
|
||||
.attr("data-vid", v.verificationId)
|
||||
.attr("data-seen", now - (v.ageMs || 0))
|
||||
.attr("data-expires", (v.expiresInMs != null) ? (now + v.expiresInMs) : "")
|
||||
.html('<div class="matrix-vl-item-l"><div class="matrix-vl-item-title">' + vlEsc(typeLabel) + '</div>'
|
||||
+ '<div class="matrix-vl-item-sub">' + sub + '</div></div>'
|
||||
+ '<div class="matrix-vl-item-r"><div class="matrix-vl-age"></div>'
|
||||
+ '<div class="matrix-vl-exp"></div></div>'));
|
||||
});
|
||||
$("#matrix-vl-empty").toggle(!(data.verifications || []).length);
|
||||
if(data.hidden > 0) {
|
||||
$("#matrix-vl-more").text(data.hidden + " older request" + (data.hidden === 1 ? "" : "s") + " hidden.").show();
|
||||
} else {
|
||||
$("#matrix-vl-more").hide();
|
||||
}
|
||||
vlTick();
|
||||
};
|
||||
var vlLoadList = function() {
|
||||
vlCall({ id: vlId(), action: "list" })
|
||||
.done(vlRenderList)
|
||||
.fail(function() { vlRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); });
|
||||
};
|
||||
var vlShowList = function() {
|
||||
vlClearTimers();
|
||||
$("#matrix-vl-overlay").data("vlConfirmed", false);
|
||||
$("#matrix-vl-detailview").hide();
|
||||
$("#matrix-vl-listview").show();
|
||||
$("#matrix-vl-back,#matrix-vl-cancel").hide();
|
||||
$("#matrix-vl-items").html('<div class="matrix-vl-empty">Loading…</div>');
|
||||
$("#matrix-vl-empty,#matrix-vl-more").hide();
|
||||
vlLoadList();
|
||||
vlListTimer = setInterval(vlLoadList, 5000);
|
||||
vlTickTimer = setInterval(vlTick, 1000);
|
||||
};
|
||||
|
||||
var vlRenderSas = function(sas) {
|
||||
var $sas = $("#matrix-vl-d-sas").empty();
|
||||
if(sas.emoji && sas.emoji.length) {
|
||||
sas.emoji.forEach(function(pair) {
|
||||
$sas.append('<div class="matrix-vl-emoji"><div class="e">' + vlEsc(pair[0])
|
||||
+ '</div><div class="n">' + vlEsc(pair[1]) + '</div></div>');
|
||||
});
|
||||
} else if(sas.decimal) {
|
||||
$sas.append('<div style="font-size:24px;font-family:monospace;">' + vlEsc(sas.decimal.join(" ")) + '</div>');
|
||||
}
|
||||
};
|
||||
var vlRenderDetail = function(v) {
|
||||
var typeLabel = v.type === "room" ? "Room verification" : "Device verification";
|
||||
var head = vlEsc(typeLabel);
|
||||
if(v.userId) { head += " — " + vlEsc(v.userId); }
|
||||
if(v.deviceId) { head += " (device " + vlEsc(v.deviceId) + ")"; }
|
||||
$("#matrix-vl-d-head").html(head);
|
||||
|
||||
if(v.phase === "done") {
|
||||
vlClearTimers();
|
||||
vlState("fa-check-circle", "#3a9a4e", "<b>Verified.</b> This session is now verified.");
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
|
||||
$("#matrix-vl-cancel").hide();
|
||||
return;
|
||||
}
|
||||
if(v.phase === "cancelled") {
|
||||
vlClearTimers();
|
||||
var why = v.cancellationCode ? (" (" + vlEsc(v.cancellationCode) + ")") : "";
|
||||
vlState("fa-times-circle", "#c9302c", "Verification was cancelled" + why + ".");
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
|
||||
$("#matrix-vl-cancel").hide();
|
||||
return;
|
||||
}
|
||||
if(v.phase === "gone") {
|
||||
vlClearTimers();
|
||||
vlState("fa-exclamation-triangle", "#888", "This verification is no longer available.");
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
|
||||
$("#matrix-vl-cancel").hide();
|
||||
return;
|
||||
}
|
||||
if($("#matrix-vl-overlay").data("vlConfirmed")) {
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
|
||||
vlState("fa-spinner fa-spin", "#888", "Waiting for the other device to confirm…");
|
||||
return;
|
||||
}
|
||||
if(v.sas && (v.sas.emoji || v.sas.decimal)) {
|
||||
vlState("fa-key", "#d18a1b", "Compare these emoji with the other device, then choose below.");
|
||||
vlRenderSas(v.sas);
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").show();
|
||||
return;
|
||||
}
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
|
||||
vlState("fa-spinner fa-spin", "#888", "Waiting for the verification to start…");
|
||||
};
|
||||
var vlPollDetail = function() {
|
||||
vlCall({ id: vlId(), action: "advance", verificationId: vlCurId() })
|
||||
.done(function(data) {
|
||||
if(data.result !== "ok") {
|
||||
vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message));
|
||||
return;
|
||||
}
|
||||
vlRenderDetail(data.verification);
|
||||
})
|
||||
.fail(function() { vlState("fa-exclamation-triangle", "#c9302c", "Request failed — is Node-RED still running?"); });
|
||||
};
|
||||
var vlShowDetail = function(vid) {
|
||||
vlClearTimers();
|
||||
$("#matrix-vl-overlay").data("vlCurrentId", vid).data("vlConfirmed", false);
|
||||
$("#matrix-vl-listview").hide();
|
||||
$("#matrix-vl-detailview").show();
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions,#matrix-vl-d-result").hide();
|
||||
$("#matrix-vl-d-head").text("");
|
||||
vlState("fa-spinner fa-spin", "#888", "Starting verification…");
|
||||
$("#matrix-vl-back,#matrix-vl-cancel").show();
|
||||
vlPollDetail();
|
||||
vlDetailTimer = setInterval(vlPollDetail, 1500);
|
||||
};
|
||||
|
||||
var vlClose = function() { vlClearTimers(); $("#matrix-vl-overlay").fadeOut(120); };
|
||||
|
||||
$("#matrix-vl-x,#matrix-vl-close").on("click", vlClose);
|
||||
$("#matrix-vl-overlay").on("mousedown", function(e) { if(e.target === this) { vlClose(); } });
|
||||
$(document).on("keydown.matrixvl", function(e) {
|
||||
if(e.key === "Escape" && $("#matrix-vl-overlay").is(":visible")) { vlClose(); }
|
||||
});
|
||||
$("#matrix-vl-back").on("click", vlShowList);
|
||||
$("#matrix-vl-items").on("click", ".matrix-vl-item", function() {
|
||||
vlShowDetail($(this).attr("data-vid"));
|
||||
});
|
||||
$("#matrix-vl-cancel").on("click", function() {
|
||||
vlCall({ id: vlId(), action: "cancel", verificationId: vlCurId() }).always(vlPollDetail);
|
||||
});
|
||||
$("#matrix-vl-d-confirm").on("click", function() {
|
||||
$("#matrix-vl-overlay").data("vlConfirmed", true);
|
||||
$("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
|
||||
vlState("fa-spinner fa-spin", "#888", "Confirming…");
|
||||
vlCall({ id: vlId(), action: "confirm", verificationId: vlCurId() })
|
||||
.done(function(data) {
|
||||
if(data.result !== "ok") {
|
||||
$("#matrix-vl-overlay").data("vlConfirmed", false);
|
||||
vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message));
|
||||
$("#matrix-vl-d-actions").show();
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#matrix-vl-d-mismatch").on("click", function() {
|
||||
$("#matrix-vl-d-actions").hide();
|
||||
vlState("fa-spinner fa-spin", "#888", "Cancelling…");
|
||||
vlCall({ id: vlId(), action: "mismatch", verificationId: vlCurId() }).always(vlPollDetail);
|
||||
});
|
||||
|
||||
$("#matrix-vl-overlay").data("vlShowListFn", vlShowList);
|
||||
$("#matrix-vl-overlay").data("vlShowDetailFn", vlShowDetail);
|
||||
}
|
||||
|
||||
$("#matrix-verification-list-btn").on("click", function() {
|
||||
$("#matrix-vl-overlay").data("matrixNodeId", nodeId).fadeIn(120);
|
||||
$("#matrix-vl-overlay").data("vlShowListFn")();
|
||||
});
|
||||
|
||||
// --- Sessions (modal) ---
|
||||
// Built once and reused; the node id is stored on the overlay.
|
||||
if (!document.getElementById("matrix-ss-overlay")) {
|
||||
$('<style>'
|
||||
+ '.matrix-ss-overlay{position:fixed;inset:0;z-index:3000;display:none;background:rgba(0,0,0,.45);}'
|
||||
+ '.matrix-ss-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);'
|
||||
+ 'width:560px;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;'
|
||||
+ 'border-radius:6px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.45);'
|
||||
+ 'background:var(--red-ui-primary-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);}'
|
||||
+ '.matrix-ss-head{display:flex;justify-content:space-between;align-items:center;'
|
||||
+ 'padding:12px 16px;font-size:15px;font-weight:bold;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-ss-x{cursor:pointer;font-size:20px;line-height:1;opacity:.55;}'
|
||||
+ '.matrix-ss-x:hover{opacity:1;}'
|
||||
+ '.matrix-ss-body{padding:14px 16px;overflow:auto;}'
|
||||
+ '.matrix-ss-h{font-weight:bold;font-size:12px;text-transform:uppercase;'
|
||||
+ 'letter-spacing:.04em;color:var(--red-ui-secondary-text-color,#888);margin:2px 0 8px;}'
|
||||
+ '.matrix-ss-h.spaced{margin-top:20px;}'
|
||||
+ '.matrix-ss-item{display:flex;align-items:center;gap:10px;padding:10px 12px;'
|
||||
+ 'margin-bottom:8px;border-radius:5px;cursor:pointer;'
|
||||
+ 'border:1px solid var(--red-ui-secondary-border-color,#ddd);'
|
||||
+ 'background:var(--red-ui-secondary-background,#f7f7f7);}'
|
||||
+ '.matrix-ss-item:hover{border-color:var(--red-ui-node-border,#999);}'
|
||||
+ '.matrix-ss-item-l{flex:1;min-width:0;}'
|
||||
+ '.matrix-ss-item-title{font-weight:bold;word-break:break-word;}'
|
||||
+ '.matrix-ss-item-sub{font-size:12px;color:var(--red-ui-secondary-text-color,#888);'
|
||||
+ 'margin-top:2px;word-break:break-word;}'
|
||||
+ '.matrix-ss-shield{font-size:20px;width:22px;text-align:center;flex-shrink:0;}'
|
||||
+ '.matrix-ss-box{display:flex;gap:10px;align-items:flex-start;margin:10px 0;'
|
||||
+ 'padding:10px 12px;border-radius:4px;font-size:13px;line-height:1.45;}'
|
||||
+ '.matrix-ss-box.ok{background:#e7f4ea;border:1px solid #8fcea5;color:#1e6b33;}'
|
||||
+ '.matrix-ss-box.err{background:#fde7e9;border:1px solid #e8a0a8;color:#8a1f2b;}'
|
||||
+ '.matrix-ss-box .fa{font-size:18px;margin-top:1px;}'
|
||||
+ '.matrix-ss-details div{display:flex;font-size:13px;padding:5px 0;'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#eee);}'
|
||||
+ '.matrix-ss-details .k{width:130px;flex-shrink:0;color:var(--red-ui-secondary-text-color,#888);}'
|
||||
+ '.matrix-ss-details .v{word-break:break-all;}'
|
||||
+ '.matrix-ss-empty,.matrix-ss-more{font-size:13px;'
|
||||
+ 'color:var(--red-ui-secondary-text-color,#888);padding:6px 2px;}'
|
||||
+ '.matrix-ss-removelink{color:#c9302c;cursor:pointer;font-weight:bold;}'
|
||||
+ '.matrix-ss-result{margin-top:12px;padding:10px 12px;border-radius:4px;font-size:13px;}'
|
||||
+ '.matrix-ss-result.ok{background:#e7f4ea;border:1px solid #8fcea5;color:#1e6b33;}'
|
||||
+ '.matrix-ss-result.err{background:#fde7e9;border:1px solid #e8a0a8;color:#8a1f2b;}'
|
||||
+ '.matrix-ss-foot{display:flex;align-items:center;gap:8px;padding:10px 16px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-top:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '</style>').appendTo("head");
|
||||
|
||||
$('<div id="matrix-ss-overlay" class="matrix-ss-overlay"><div class="matrix-ss-modal">'
|
||||
+ '<div class="matrix-ss-head"><span><i class="fa fa-desktop"></i> Sessions</span>'
|
||||
+ '<span class="matrix-ss-x" id="matrix-ss-x" title="Close">×</span></div>'
|
||||
+ '<div class="matrix-ss-body">'
|
||||
+ '<div id="matrix-ss-listview">'
|
||||
+ '<div class="matrix-ss-h">Current session</div>'
|
||||
+ '<div id="matrix-ss-current"></div>'
|
||||
+ '<div id="matrix-ss-currentmsg"></div>'
|
||||
+ '<div class="matrix-ss-h spaced">Other sessions</div>'
|
||||
+ '<div id="matrix-ss-others"></div>'
|
||||
+ '<div id="matrix-ss-others-empty" class="matrix-ss-empty" style="display:none;">No other sessions.</div>'
|
||||
+ '<div id="matrix-ss-more" class="matrix-ss-more" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '<div id="matrix-ss-detailview" style="display:none;">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">'
|
||||
+ '<div id="matrix-ss-d-name" style="font-size:15px;font-weight:bold;word-break:break-word;"></div>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-d-rename">Rename</button></div>'
|
||||
+ '<div id="matrix-ss-d-status"></div>'
|
||||
+ '<div class="matrix-ss-h spaced">Session details</div>'
|
||||
+ '<div id="matrix-ss-d-details" class="matrix-ss-details"></div>'
|
||||
+ '<div id="matrix-ss-d-verifywrap" style="display:none;margin-top:14px;">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-d-verify">Verify this session</button></div>'
|
||||
+ '<div id="matrix-ss-d-removewrap" style="display:none;margin-top:16px;">'
|
||||
+ '<span class="matrix-ss-removelink" id="matrix-ss-d-removelink">Remove this session</span>'
|
||||
+ '<div id="matrix-ss-d-removeconfirm" style="display:none;margin-top:8px;">'
|
||||
+ '<div style="background:#fdf3e7;border:1px solid #f0c36d;color:#7a5b16;border-radius:4px;'
|
||||
+ 'padding:8px 10px;font-size:13px;margin-bottom:8px;">Removing a session signs it out. '
|
||||
+ 'Enter the account password to confirm.</div>'
|
||||
+ '<input type="password" id="matrix-ss-d-password" placeholder="Account password" '
|
||||
+ 'style="width:100%;box-sizing:border-box;padding:7px 9px;border-radius:4px;'
|
||||
+ 'border:1px solid var(--red-ui-form-input-border-color,#ccc);'
|
||||
+ 'background:var(--red-ui-form-input-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);">'
|
||||
+ '<div style="text-align:right;margin-top:8px;"><button type="button" class="red-ui-button" '
|
||||
+ 'id="matrix-ss-d-removeconfirmbtn">Confirm removal</button></div>'
|
||||
+ '</div></div>'
|
||||
+ '<div id="matrix-ss-d-result" class="matrix-ss-result" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-ss-foot">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-back" style="display:none;">← Back</button>'
|
||||
+ '<span style="flex:1;"></span>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-close">Close</button>'
|
||||
+ '</div></div></div>').appendTo(document.body);
|
||||
|
||||
var ssEsc = function(s) { return $("<div>").text(s == null ? "" : String(s)).html(); };
|
||||
var ssId = function() { return $("#matrix-ss-overlay").data("matrixNodeId"); };
|
||||
var ssCall = function(body) {
|
||||
return $.ajax({
|
||||
url: "matrix-chat/sessions", type: "POST",
|
||||
contentType: "application/json", data: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
var ssRelative = function(ts) {
|
||||
if(!ts) { return "activity unknown"; }
|
||||
var days = Math.floor((Date.now() - ts) / 86400000);
|
||||
if(days <= 0) { return "active today"; }
|
||||
if(days === 1) { return "active yesterday"; }
|
||||
if(days < 90) { return "active " + days + " days ago"; }
|
||||
return "inactive for 90+ days";
|
||||
};
|
||||
var ssFullDate = function(ts) {
|
||||
return ts ? new Date(ts).toLocaleString() : "unknown";
|
||||
};
|
||||
var ssShield = function(verified) {
|
||||
return '<i class="fa fa-shield matrix-ss-shield" style="color:'
|
||||
+ (verified ? '#3a9a4e' : '#c9302c') + ';" title="'
|
||||
+ (verified ? 'Verified' : 'Not verified') + '"></i>';
|
||||
};
|
||||
var ssCard = function(d, isCurrent) {
|
||||
var sub = (d.verified ? 'Verified' : 'Not verified')
|
||||
+ ' · ' + ssRelative(d.lastSeenTs)
|
||||
+ (d.lastSeenIp ? (' · ' + d.lastSeenIp) : '');
|
||||
return $('<div class="matrix-ss-item">')
|
||||
.data("device", d).data("isCurrent", isCurrent)
|
||||
.html(ssShield(d.verified)
|
||||
+ '<div class="matrix-ss-item-l"><div class="matrix-ss-item-title">'
|
||||
+ ssEsc(d.displayName || d.deviceId) + '</div>'
|
||||
+ '<div class="matrix-ss-item-sub">' + ssEsc(sub) + '</div></div>'
|
||||
+ '<i class="fa fa-angle-right" style="opacity:.5;"></i>');
|
||||
};
|
||||
var ssResult = function(ok, text) {
|
||||
$("#matrix-ss-d-result").removeClass("ok err").addClass(ok ? "ok" : "err").text(text).show();
|
||||
};
|
||||
var ssRenderList = function(data) {
|
||||
if(data.result !== "ok") {
|
||||
$("#matrix-ss-current").html('<div class="matrix-ss-empty">' + ssEsc(data.message) + '</div>');
|
||||
$("#matrix-ss-currentmsg,#matrix-ss-others").empty();
|
||||
$("#matrix-ss-others-empty,#matrix-ss-more").hide();
|
||||
return;
|
||||
}
|
||||
$("#matrix-ss-current").empty().append(ssCard(data.current, true));
|
||||
$("#matrix-ss-currentmsg").html(data.current.verified
|
||||
? '<div class="matrix-ss-box ok"><i class="fa fa-shield"></i><div><b>Verified session</b><br>'
|
||||
+ 'This session is cross-signed and ready for secure messaging.</div></div>'
|
||||
: '<div class="matrix-ss-box err"><i class="fa fa-shield"></i><div><b>Not verified</b><br>'
|
||||
+ 'This session is not cross-signed. Use the Set up secure backup & cross-signing '
|
||||
+ 'button to verify it.</div></div>');
|
||||
var $others = $("#matrix-ss-others").empty();
|
||||
(data.others || []).forEach(function(d) { $others.append(ssCard(d, false)); });
|
||||
$("#matrix-ss-others-empty").toggle(!(data.others || []).length);
|
||||
if(data.hidden > 0) {
|
||||
$("#matrix-ss-more").text(data.hidden + " more session" + (data.hidden === 1 ? "" : "s") + " hidden.").show();
|
||||
} else {
|
||||
$("#matrix-ss-more").hide();
|
||||
}
|
||||
};
|
||||
var ssShowList = function() {
|
||||
$("#matrix-ss-detailview").hide();
|
||||
$("#matrix-ss-listview").show();
|
||||
$("#matrix-ss-back").hide();
|
||||
$("#matrix-ss-current").html('<div class="matrix-ss-empty">Loading…</div>');
|
||||
$("#matrix-ss-currentmsg,#matrix-ss-others").empty();
|
||||
$("#matrix-ss-others-empty,#matrix-ss-more").hide();
|
||||
ssCall({ id: ssId(), action: "list" })
|
||||
.done(ssRenderList)
|
||||
.fail(function() { ssRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); });
|
||||
};
|
||||
var ssDetailRow = function(k, v) {
|
||||
return '<div><span class="k">' + ssEsc(k) + '</span><span class="v">' + ssEsc(v) + '</span></div>';
|
||||
};
|
||||
var ssShowDetail = function(d, isCurrent) {
|
||||
$("#matrix-ss-overlay").data("ssDevice", d).data("ssIsCurrent", isCurrent);
|
||||
$("#matrix-ss-listview").hide();
|
||||
$("#matrix-ss-detailview").show();
|
||||
$("#matrix-ss-back").show();
|
||||
$("#matrix-ss-d-result,#matrix-ss-d-removeconfirm").hide();
|
||||
$("#matrix-ss-d-name").text(d.displayName || d.deviceId);
|
||||
$("#matrix-ss-d-status").html(d.verified
|
||||
? '<div class="matrix-ss-box ok"><i class="fa fa-shield"></i><div><b>Verified session</b><br>'
|
||||
+ 'This session is ready for secure messaging.</div></div>'
|
||||
: '<div class="matrix-ss-box err"><i class="fa fa-shield"></i><div><b>Not verified</b><br>'
|
||||
+ (isCurrent
|
||||
? 'This session is not cross-signed. Use the Set up secure backup & cross-signing button to verify it.'
|
||||
: 'Verify this session to confirm it is trusted.')
|
||||
+ '</div></div>');
|
||||
$("#matrix-ss-d-details").html(
|
||||
ssDetailRow("Session ID", d.deviceId)
|
||||
+ ssDetailRow("Last activity", ssFullDate(d.lastSeenTs))
|
||||
+ ssDetailRow("IP address", d.lastSeenIp || "unknown"));
|
||||
$("#matrix-ss-d-verifywrap").toggle(!isCurrent && !d.verified);
|
||||
$("#matrix-ss-d-removewrap").toggle(!isCurrent);
|
||||
};
|
||||
var ssClose = function() { $("#matrix-ss-overlay").fadeOut(120); };
|
||||
|
||||
$("#matrix-ss-x,#matrix-ss-close").on("click", ssClose);
|
||||
$("#matrix-ss-overlay").on("mousedown", function(e) { if(e.target === this) { ssClose(); } });
|
||||
$(document).on("keydown.matrixss", function(e) {
|
||||
if(e.key === "Escape" && $("#matrix-ss-overlay").is(":visible")) { ssClose(); }
|
||||
});
|
||||
$("#matrix-ss-back").on("click", ssShowList);
|
||||
$("#matrix-ss-current,#matrix-ss-others").on("click", ".matrix-ss-item", function() {
|
||||
ssShowDetail($(this).data("device"), $(this).data("isCurrent"));
|
||||
});
|
||||
$("#matrix-ss-d-rename").on("click", function() {
|
||||
var d = $("#matrix-ss-overlay").data("ssDevice");
|
||||
var name = prompt("Session display name:", d.displayName || "");
|
||||
if(name === null) { return; }
|
||||
ssCall({ id: ssId(), action: "rename", deviceId: d.deviceId, displayName: name })
|
||||
.done(function(r) {
|
||||
if(r.result !== "ok") { ssResult(false, r.message); return; }
|
||||
d.displayName = name;
|
||||
$("#matrix-ss-d-name").text(name || d.deviceId);
|
||||
ssResult(true, "Session renamed.");
|
||||
})
|
||||
.fail(function() { ssResult(false, "Request failed."); });
|
||||
});
|
||||
$("#matrix-ss-d-verify").on("click", function() {
|
||||
var d = $("#matrix-ss-overlay").data("ssDevice");
|
||||
$("#matrix-ss-d-result").removeClass("ok err").html('<i class="fa fa-spinner fa-spin"></i> Starting verification…').show();
|
||||
ssCall({ id: ssId(), action: "verify", deviceId: d.deviceId })
|
||||
.done(function(r) {
|
||||
if(r.result !== "ok" || !r.verificationId) {
|
||||
ssResult(false, r.message || "Could not start verification.");
|
||||
return;
|
||||
}
|
||||
// hand off to the verification modal's detail view
|
||||
$("#matrix-ss-overlay").fadeOut(120);
|
||||
$("#matrix-vl-overlay").data("matrixNodeId", ssId()).fadeIn(120);
|
||||
$("#matrix-vl-overlay").data("vlShowDetailFn")(r.verificationId);
|
||||
})
|
||||
.fail(function() { ssResult(false, "Request failed."); });
|
||||
});
|
||||
$("#matrix-ss-d-removelink").on("click", function() {
|
||||
$("#matrix-ss-d-password").val("");
|
||||
$("#matrix-ss-d-removeconfirm").show();
|
||||
});
|
||||
$("#matrix-ss-d-removeconfirmbtn").on("click", function() {
|
||||
var d = $("#matrix-ss-overlay").data("ssDevice");
|
||||
$("#matrix-ss-d-result").removeClass("ok err").html('<i class="fa fa-spinner fa-spin"></i> Removing session…').show();
|
||||
ssCall({ id: ssId(), action: "remove", deviceId: d.deviceId, password: $("#matrix-ss-d-password").val() })
|
||||
.done(function(r) {
|
||||
if(r.result !== "ok") { ssResult(false, r.message); return; }
|
||||
ssShowList();
|
||||
})
|
||||
.fail(function() { ssResult(false, "Request failed."); });
|
||||
});
|
||||
|
||||
$("#matrix-ss-overlay").data("ssShowListFn", ssShowList);
|
||||
}
|
||||
|
||||
$("#matrix-sessions-btn").on("click", function() {
|
||||
$("#matrix-ss-overlay").data("matrixNodeId", nodeId).fadeIn(120);
|
||||
$("#matrix-ss-overlay").data("ssShowListFn")();
|
||||
});
|
||||
|
||||
// --- Login: fetch a fresh access token & device id ---
|
||||
$("#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>
|
||||
@@ -72,7 +820,10 @@
|
||||
<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.
|
||||
Optional. Used to fetch an access token with the button below, and — if you
|
||||
enable cross-signing — as a fallback when the homeserver requires the account
|
||||
password to upload signing keys. If set, it is stored (encrypted) with the node's
|
||||
credentials. Leave blank if you only want to use an access token.
|
||||
</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>
|
||||
@@ -87,14 +838,6 @@
|
||||
You can either provide/generate an access token yourself or use the login button above to do it automatically. View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-secretStoragePassphrase"><i class="fa fa-key"></i> Secret Storage Passphrase</label>
|
||||
<input type="text" id="node-config-input-secretStoragePassphrase">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
You can either provide/generate an access token yourself or use the login button above to do it automatically. View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-deviceId"><i class="fa fa-desktop"></i> Device ID</label>
|
||||
<input type="text" id="node-config-input-deviceId">
|
||||
@@ -139,88 +882,87 @@
|
||||
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$("#matrix-login-btn").on("click", function() {
|
||||
function prettyPrintJson(json) {
|
||||
try{
|
||||
return typeof json === 'object' ? JSON.stringify(json, null, 2) : json;
|
||||
}
|
||||
catch (error){
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
let userId = $("#node-config-input-userId").val(),
|
||||
userPassword = $("#node-config-input-password").val(),
|
||||
serverUrl = $("#node-config-input-url").val();
|
||||
<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>
|
||||
|
||||
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;
|
||||
}
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-shield"></i> Secure Backup</label>
|
||||
<button type="button" class="red-ui-button" id="matrix-secure-backup-btn">Set up secure backup & cross-signing</button>
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
Sets up cross-signing so the bot's own device shows as verified. The server
|
||||
configuration must be deployed and connected first.
|
||||
</div>
|
||||
|
||||
$("#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();
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-check-circle"></i> Verification</label>
|
||||
<button type="button" class="red-ui-button" id="matrix-verification-list-btn">Pending verification requests</button>
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
Review and complete incoming device verification requests without building a flow.
|
||||
The server configuration must be deployed and connected first.
|
||||
</div>
|
||||
|
||||
$("#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>
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-desktop"></i> Sessions</label>
|
||||
<button type="button" class="red-ui-button" id="matrix-sessions-btn">Manage sessions</button>
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
View the account's logged-in sessions, verify them, or remove ones you don't recognize.
|
||||
The server configuration must be deployed and connected first.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-server-config">
|
||||
<h3>Details</h3>
|
||||
<p>Matrix client connection configuration</p>
|
||||
|
||||
<h3>Server URL</h3>
|
||||
<p>
|
||||
The URL of your homeserver. You can enter either the homeserver URL directly
|
||||
(e.g. <code>https://matrix.example.org</code>) or the delegating domain
|
||||
(e.g. <code>https://example.org</code>) — in the latter case the real
|
||||
homeserver is resolved automatically via <code>.well-known</code> discovery.
|
||||
</p>
|
||||
|
||||
<h3>Setting up an account</h3>
|
||||
<div>
|
||||
<p>
|
||||
You need an account for your client to use. If you are going to be using End-to-End Encryption you should generate the bot and only use it within Node-RED otherwise if you have other clients connected on the same user it could cause problems with e2ee (key sharing is currently not supported).
|
||||
You need an account for your client to use. For End-to-End Encryption it is simplest to dedicate the account to the bot and run it only within Node-RED. The account may also be signed in on other clients — if so, verify those sessions against the bot (see <em>Cross-signing & secure backup</em> below) so they trust each other and share keys.
|
||||
</p>
|
||||
<p>If you have access to the server directly you can use Shared Secret Registration as described <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#create-user-with-shared-secret-registration" target="_blank" style="text-decoration: underline;">here</a>.</p>
|
||||
<p>If this is a server you do not administrate/have access to follow these instructions:</p>
|
||||
<ol><li>In a private/incognito browser window, open Element.</li><li>Log in to the account you want to get the access token for, such as the bot's account. <strong>Do not setup key storage</strong>.</li><li>Click on the bot's name in the top left corner then "Settings".</li><li>(Optional) Set your bot's display name and avatar.</li><li>Click the "Help & About" tab (left side of the dialog).</li><li>Scroll to the bottom and click the <code><click to reveal></code> part of <code>Access Token: <click to reveal></code>.</li><li>Copy your access token to a safe place, like the bot's configuration file.</li><li><strong>Do not log out.</strong> Instead, just close the window. If you used a private browsing session, you should be able to still use Element for your own account. Logging out deletes the access token from the server, making the bot unable to use it.</li></ol>
|
||||
<ol><li>In a private/incognito browser window, open Element.</li><li>Log in to the account you want to get the access token for, such as the bot's account.</li><li>Click on the bot's name in the top left corner then "Settings".</li><li>(Optional) Set your bot's display name and avatar.</li><li>Click the "Help & About" tab (left side of the dialog).</li><li>Scroll to the bottom and click the <code><click to reveal></code> part of <code>Access Token: <click to reveal></code>.</li><li>Copy your access token to a safe place, like the bot's configuration file.</li><li><strong>Do not log out.</strong> Instead, just close the window. If you used a private browsing session, you should be able to still use Element for your own account. Logging out deletes the access token from the server, making the bot unable to use it.</li></ol>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<h3>Cross-signing & secure backup</h3>
|
||||
<div>
|
||||
<p>
|
||||
Use the <strong>Set up secure backup & cross-signing</strong> button to set up
|
||||
cross-signing so the bot's own device is verified. The server configuration must be
|
||||
deployed and connected first; the button then checks the account and, interactively:
|
||||
</p>
|
||||
<ul>
|
||||
<li>if the account already has a secure backup, lets you <strong>unlock</strong> it with its recovery key (or passphrase) and set up cross-signing for the bot;</li>
|
||||
<li>otherwise (or on request) lets you <strong>reset</strong> cross-signing and secure backup, creating a new recovery key — this needs the account password.</li>
|
||||
</ul>
|
||||
<p>
|
||||
To verify other devices interactively (SAS emoji), use the <code>matrix-verification</code>
|
||||
node to receive verification requests and <code>matrix-verification-action</code> to
|
||||
accept, start, confirm or cancel them. This lets you build your own approval flow
|
||||
(for example emailing the emoji for a human to confirm).
|
||||
</p>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Synapse Add/Edit User";
|
||||
|
||||
@@ -8,9 +8,10 @@ module.exports = function(RED) {
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
|
||||
if(!this.server) {
|
||||
node.error('Server must be configured on the node.');
|
||||
node.error('Server must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
this.encodeUri = function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
@@ -41,18 +42,18 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
node.server.matrixClient.http
|
||||
.authedRequest(
|
||||
undefined,
|
||||
'PUT',
|
||||
node.encodeUri(
|
||||
"/_synapse/admin/v2/users/$userId",
|
||||
@@ -62,13 +63,17 @@ module.exports = function(RED) {
|
||||
msg.payload,
|
||||
{ prefix: '' }
|
||||
).then(function(e){
|
||||
msg.payload = e;
|
||||
node.send([msg, null]);
|
||||
}).catch(function(e){
|
||||
node.warn("Error creating/editing user " + e);
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
msg.payload = e;
|
||||
node.send([msg, null]);
|
||||
}).catch(function(e){
|
||||
node.warn("Error creating/editing user " + e);
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-synapse-create-edit-user", MatrixSynapseCreateEditUser);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Synapse Deactivate User";
|
||||
|
||||
@@ -8,9 +8,10 @@ module.exports = function(RED) {
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
|
||||
if(!this.server) {
|
||||
node.error('Server must be configured on the node.');
|
||||
node.error('Server must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
this.encodeUri = function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
@@ -41,12 +42,13 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -56,7 +58,6 @@ module.exports = function(RED) {
|
||||
);
|
||||
node.server.matrixClient.http
|
||||
.authedRequest(
|
||||
undefined,
|
||||
'POST',
|
||||
path,
|
||||
undefined,
|
||||
@@ -71,6 +72,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-synapse-deactivate-user", MatrixSynapseDeactivateUser);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs: 2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomId: { value: null },
|
||||
},
|
||||
label: function() {
|
||||
|
||||
@@ -9,10 +9,9 @@ module.exports = function(RED) {
|
||||
this.roomId = n.roomId;
|
||||
|
||||
if(!this.server) {
|
||||
node.error('Server must be configured on the node.');
|
||||
node.error('Server must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
|
||||
this.encodeUri = function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
@@ -42,25 +41,25 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || 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;
|
||||
}
|
||||
|
||||
if(!msg.userId) {
|
||||
node.error("msg.userId is required to set user into a room");
|
||||
node.error("msg.userId is required to set user into a room", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// we need the status code, so set onlydata to false for this request
|
||||
node.server.matrixClient.http
|
||||
.authedRequest(
|
||||
undefined,
|
||||
'POST',
|
||||
node.encodeUri(
|
||||
"/_synapse/admin/v1/join/$room_id_or_alias",
|
||||
@@ -78,6 +77,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-synapse-join-room", MatrixJoinRoom);
|
||||
}
|
||||
@@ -12,12 +12,12 @@ module.exports = function(RED) {
|
||||
this.sharedSecret = this.credentials.sharedSecret;
|
||||
|
||||
if(!this.server) {
|
||||
node.error('Server URL must be configured on the node.');
|
||||
node.error('Server URL must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.sharedSecret) {
|
||||
node.error('Shared registration secret must be configured on the node.');
|
||||
node.error('Shared registration secret must be configured on the node.', {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ module.exports = function(RED) {
|
||||
const { got } = await import('got');
|
||||
|
||||
if(!msg.payload.username) {
|
||||
node.error("msg.payload.username is required");
|
||||
node.error("msg.payload.username is required", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!msg.payload.password) {
|
||||
node.error("msg.payload.password is required");
|
||||
node.error("msg.payload.password is required", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ module.exports = function(RED) {
|
||||
|
||||
var nonce = response.body.nonce;
|
||||
if(!nonce) {
|
||||
node.error('Could not get nonce from /_synapse/admin/v1/register');
|
||||
node.error('Could not get nonce from /_synapse/admin/v1/register', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ module.exports = function(RED) {
|
||||
node.send(msg);
|
||||
})();
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-synapse-register", MatrixSynapseRegister, {
|
||||
credentials: {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
outputs:2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { value: "", type: "matrix-server-config" }
|
||||
server: { type: "matrix-server-config" }
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Synapse User List";
|
||||
|
||||
@@ -12,6 +12,8 @@ module.exports = function(RED) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
node.server.on("disconnected", function(){
|
||||
@@ -29,8 +31,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
let queryParams = {
|
||||
@@ -48,7 +51,6 @@ module.exports = function(RED) {
|
||||
|
||||
node.server.matrixClient.http
|
||||
.authedRequest(
|
||||
undefined,
|
||||
'GET',
|
||||
"/_synapse/admin/v2/users",
|
||||
queryParams,
|
||||
@@ -63,6 +65,10 @@ module.exports = function(RED) {
|
||||
node.send([null, msg]);
|
||||
});
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-synapse-users", MatrixSynapseUsers);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-typing', {
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
outputLabels: ["success", "error"],
|
||||
inputs: 1,
|
||||
outputs: 2,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { type: "matrix-server-config" },
|
||||
roomType: { value: "msg" },
|
||||
roomValue: { value: "topic" },
|
||||
typingType: { value: "bool" },
|
||||
typingValue: { value: "true" },
|
||||
timeoutMsType: { value: "num" },
|
||||
timeoutMsValue: { value: 20000 },
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Typing";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
$("#node-input-room").typedInput({
|
||||
type: this.roomType,
|
||||
types:['msg','flow','global','str'],
|
||||
})
|
||||
.typedInput('value', this.roomValue)
|
||||
.typedInput('type', this.roomType);
|
||||
|
||||
$("#node-input-typing").typedInput({
|
||||
types:['msg','flow','global','bool'],
|
||||
})
|
||||
.typedInput('value', this.typingValue)
|
||||
.typedInput('type', this.typingType);
|
||||
|
||||
$("#node-input-timeoutMs").typedInput({
|
||||
types:['msg','flow','global','num'],
|
||||
})
|
||||
.typedInput('value', this.timeoutMsValue)
|
||||
.typedInput('type', this.timeoutMsType);
|
||||
},
|
||||
oneditsave: function() {
|
||||
this.roomType = $("#node-input-room").typedInput('type');
|
||||
this.roomValue = $("#node-input-room").typedInput('value');
|
||||
this.typingType = $("#node-input-typing").typedInput('type');
|
||||
this.typingValue = $("#node-input-typing").typedInput('value');
|
||||
this.timeoutMsType = $("#node-input-timeoutMs").typedInput('type');
|
||||
this.timeoutMsValue = $("#node-input-timeoutMs").typedInput('value');
|
||||
},
|
||||
paletteLabel: 'Typing'
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-typing">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-server"><i class="fa fa-user"></i> Matrix Server Config</label>
|
||||
<input type="text" id="node-input-server">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-room"><i class="fa fa-comments"></i> Room</label>
|
||||
<input type="text" id="node-input-room">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-typing"><i class="fa fa-commenting-o"></i> Is Typing</label>
|
||||
<input type="text" id="node-input-typing">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-timeoutMs"><i class="fa fa-clock-o"></i> Timeout Milliseconds</label>
|
||||
<input type="text" id="node-input-timeoutMs">
|
||||
</div>
|
||||
|
||||
<div class="form-row form-tips">
|
||||
Timeout Milliseconds is how many milliseconds the server should show the user typing for. Ignored if setting typing to false.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-typing">
|
||||
<h3>Details</h3>
|
||||
<p>
|
||||
Sends typing event to a room
|
||||
</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>dynamic
|
||||
<span class="property-type">any</span>
|
||||
</dt>
|
||||
<dd> The inputs are configurable on the node.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Success
|
||||
<dl class="message-properties">
|
||||
<dd>Returns from first output on success</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Error
|
||||
<dl class="message-properties">
|
||||
<dt>msg.error <span class="property-type">string</span></dt>
|
||||
<dd>the error that occurred.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
module.exports = function(RED) {
|
||||
function MatrixTyping(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
let node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.roomId = n.roomId;
|
||||
this.roomType = n.roomType;
|
||||
this.roomValue = n.roomValue;
|
||||
this.typingType = n.typingType;
|
||||
this.typingValue = n.typingValue;
|
||||
this.timeoutMsType = n.timeoutMsType;
|
||||
this.timeoutMsValue = n.timeoutMsValue;
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
if (!node.server) {
|
||||
node.error("No configuration node", {});
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.server.on("disconnected", function(){
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
});
|
||||
|
||||
node.server.on("connected", function() {
|
||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||
});
|
||||
|
||||
node.on('input', async function(msg) {
|
||||
if (! node.server || ! node.server.matrixClient) {
|
||||
node.error("No matrix server selected", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!node.server.isConnected()) {
|
||||
node.error("Matrix server connection is currently closed", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
function getToValue(msg, type, property) {
|
||||
let value = property;
|
||||
if (type === "msg") {
|
||||
value = RED.util.getMessageProperty(msg, property);
|
||||
} else if ((type === 'flow') || (type === 'global')) {
|
||||
try {
|
||||
value = RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||
} catch(e2) {
|
||||
throw new Error("Invalid value evaluation");
|
||||
}
|
||||
} else if(type === "bool") {
|
||||
value = (property === 'true');
|
||||
} else if(type === "num") {
|
||||
value = Number(property);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
let roomId = getToValue(msg, node.roomType, node.roomValue),
|
||||
typing = getToValue(msg, node.typingType, node.typingValue),
|
||||
timeoutMs = getToValue(msg, node.timeoutMsType, node.timeoutMsValue);
|
||||
|
||||
if(!roomId) {
|
||||
node.error('No room provided in msg.topic', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
await node.server.matrixClient.sendTyping(roomId, typing, timeoutMs);
|
||||
node.send([msg, null]);
|
||||
} catch(e) {
|
||||
node.error("Failed to send typing event " + msg.topic + ": " + e, msg);
|
||||
msg.payload = e;
|
||||
node.send([null, msg]);
|
||||
}
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-typing", MatrixTyping);
|
||||
}
|
||||