mirror of
https://github.com/Skylar-Tech/node-red-contrib-matrix-chat.git
synced 2026-05-24 16:13:40 -06:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5012c603aa | |||
| af1067a99b | |||
| 390a5b264e | |||
| 9dc4362819 | |||
| 1801b49fae | |||
| 72cc9cfb3b | |||
| b8652a3a9a | |||
| 0c48db92a0 | |||
| 3f69614ed0 | |||
| e3a23df6d6 | |||
| 3e34dc5961 | |||
| 7af8891d5b | |||
| 939fe42b40 | |||
| 67920840e1 | |||
| ebcb1eab81 | |||
| 68e63e5def | |||
| 99909a77c3 | |||
| 58bf2dcb54 |
@@ -1,7 +1,7 @@
|
||||
# node-red-contrib-matrix-chat
|
||||
[Matrix](https://matrix.org/) 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 in beta. Please report any issues in our repository to help us reach a stable, well-tested release. Breaking changes may occur before our first stable release, so be sure to check the changelog before updating.***
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
@@ -11,14 +11,16 @@ Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech
|
||||
|
||||
Supported functionality in this package includes:
|
||||
|
||||
- **End-to-end encryption (E2EE)**
|
||||
- [Work in progress](#end-to-end-encryption-notes)
|
||||
- Alternative: Use [Pantalaimon](https://github.com/matrix-org/pantalaimon) for E2EE key synchronization across sessions
|
||||
- **Receive events** from rooms: Messages, reactions, images, audio, locations, files, encrypted or unencrypted
|
||||
- **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** (encryption support for files coming soon)
|
||||
- **Send/edit messages** (supports plain text and HTML formats)
|
||||
- **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
|
||||
@@ -33,6 +35,8 @@ These features allow you to easily build bots, set up chat relays, or even admin
|
||||
|
||||
### Installing
|
||||
|
||||
**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
|
||||
@@ -51,11 +55,12 @@ You're not limited to just the nodes we've created. Enable global access in your
|
||||
|
||||
### End-to-End Encryption Notes
|
||||
|
||||
- This module doesn't handle encryption key synchronization between devices. It’s recommended to use the bot exclusively in Node-RED to prevent issues with E2EE messages.
|
||||
- **Storage:** Keys for E2EE are saved in a folder called `matrix-client-storage` within your Node-RED directory. Back up this folder regularly! If lost, you won’t be able to decrypt messages from E2EE rooms.
|
||||
- To move your bot to a different installation, migrate this folder and ensure the old and new clients don't run simultaneously.
|
||||
|
||||
Interested in helping? Contributions to finalize E2EE support are welcome!
|
||||
- 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).
|
||||
|
||||
### Registering a User
|
||||
|
||||
|
||||
@@ -183,6 +183,19 @@ Any messages containing "delete" will be removed by the client.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Send a location to a room</summary>
|
||||
|
||||
[View JSON](send-location-to-room.json)
|
||||
|
||||
Sends an `m.location` event (a map pin) for the country of Norway to the configured room. Element and other matrix-react-sdk clients render it as a pin on the map with the label *"Norway"*; clients without map rendering see an auto-generated text fallback.
|
||||
|
||||
Update the `Send Location` node's Room ID to your own room before deploying. The inject node is configured to fire a bare message (no payload, no topic), so the Send Location node falls back to its configured values for everything: asset type `m.pin`, description `Norway`, and geo URI `geo:60.4720,8.4689`.
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
### Event Handling
|
||||
|
||||
<details>
|
||||
@@ -431,6 +444,27 @@ Downloads received files/images. If the file is encrypted, it will decrypt it fo
|
||||
|
||||
</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>
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
@@ -0,0 +1,69 @@
|
||||
[
|
||||
{
|
||||
"id": "222bb5ef43d621b4",
|
||||
"type": "group",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"name": "Send location to room",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"ee742dfa934b4892",
|
||||
"1ef540382789ff9d"
|
||||
],
|
||||
"x": 354,
|
||||
"y": 5279,
|
||||
"w": 392,
|
||||
"h": 82
|
||||
},
|
||||
{
|
||||
"id": "ee742dfa934b4892",
|
||||
"type": "matrix-send-location",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"g": "222bb5ef43d621b4",
|
||||
"name": "",
|
||||
"server": "",
|
||||
"roomId": "!example:test.org",
|
||||
"latitudeType": "msg",
|
||||
"latitudeValue": "latitude",
|
||||
"longitudeType": "msg",
|
||||
"longitudeValue": "longitude",
|
||||
"altitudeType": "msg",
|
||||
"altitudeValue": "altitude",
|
||||
"geoUriType": "str",
|
||||
"geoUriValue": "geo:60.4720,8.4689",
|
||||
"descriptionType": "str",
|
||||
"descriptionValue": "Norway",
|
||||
"assetTypeType": "str",
|
||||
"assetTypeValue": "m.pin",
|
||||
"timestampType": "msg",
|
||||
"timestampValue": "timestamp",
|
||||
"textType": "msg",
|
||||
"textValue": "payload",
|
||||
"x": 640,
|
||||
"y": 5320,
|
||||
"wires": [
|
||||
[],
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1ef540382789ff9d",
|
||||
"type": "inject",
|
||||
"z": "f025a8b9fbd1b054",
|
||||
"g": "222bb5ef43d621b4",
|
||||
"name": "",
|
||||
"props": [],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"x": 460,
|
||||
"y": 5320,
|
||||
"wires": [
|
||||
[
|
||||
"ee742dfa934b4892"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
Generated
+185
-138
@@ -1,31 +1,34 @@
|
||||
{
|
||||
"name": "node-red-contrib-matrix-chat",
|
||||
"version": "0.8.0",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "node-red-contrib-matrix-chat",
|
||||
"version": "0.8.0",
|
||||
"version": "1.0.0",
|
||||
"license": "SEE LICENSE FILE",
|
||||
"dependencies": {
|
||||
"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": "34.11.1",
|
||||
"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/2572/download",
|
||||
"sharp": "^0.33.4",
|
||||
"tmp": "^0.2.1",
|
||||
"utf8": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -4188,18 +4191,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz",
|
||||
"integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==",
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz",
|
||||
"integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@matrix-org/olm": {
|
||||
"version": "3.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz",
|
||||
"integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -6035,11 +6034,6 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
@@ -6620,9 +6614,10 @@
|
||||
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
|
||||
},
|
||||
"node_modules/base-x": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz",
|
||||
"integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ=="
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
|
||||
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
@@ -6824,6 +6819,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
|
||||
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base-x": "^5.0.0"
|
||||
}
|
||||
@@ -7321,6 +7317,23 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/commonmark": {
|
||||
"version": "0.31.2",
|
||||
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.31.2.tgz",
|
||||
"integrity": "sha512-2fRLTyb9r/2835k5cwcAwOj0DEc44FARnMp5veGsJ+mEAZdi52sNopLu07ZyElQUz058H43whzlERDIaaSw4rg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"entities": "~3.0.1",
|
||||
"mdurl": "~1.0.1",
|
||||
"minimist": "~1.2.8"
|
||||
},
|
||||
"bin": {
|
||||
"commonmark": "bin/commonmark"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
|
||||
@@ -7887,6 +7900,18 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-editor": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||
@@ -8284,6 +8309,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fake-indexeddb": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz",
|
||||
"integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
@@ -9333,6 +9367,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
|
||||
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -10336,6 +10382,12 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/linkifyjs": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -10365,6 +10417,12 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.escape": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
|
||||
"integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
@@ -10629,9 +10687,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
|
||||
"integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==",
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
@@ -10713,46 +10771,33 @@
|
||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||
},
|
||||
"node_modules/matrix-js-sdk": {
|
||||
"version": "34.11.1",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.11.1.tgz",
|
||||
"integrity": "sha512-rDbIUIqEsN/pbHb6haBQmjxxgeb9G3Df2IhPPOotUbX6R1KseA8yJ6TAY0YySM2zVaBV3yZ6dnKWexF/uWvZfA==",
|
||||
"version": "41.5.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.5.0.tgz",
|
||||
"integrity": "sha512-CK3h+qQJ4wkVEUgEWc5MdLjccXyiFqncCC53P+auqOhnX2U6tAFsRfnbML1QQiKIsFMzqTrAnF/4a5LUUOIeXg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.8.2",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "10"
|
||||
"p-retry": "8",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/matrix-js-sdk/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/matrix-widget-api": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz",
|
||||
"integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==",
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz",
|
||||
"integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==",
|
||||
"dependencies": {
|
||||
"@types/events": "^3.0.0",
|
||||
"events": "^3.2.0"
|
||||
@@ -10793,6 +10838,12 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -11835,8 +11886,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -12292,13 +12341,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/olm": {
|
||||
"name": "@matrix-org/olm",
|
||||
"version": "3.2.15",
|
||||
"resolved": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download",
|
||||
"integrity": "sha512-5j5CLplrEodeqGMHEzwDZz6UlqWHyR8c1+tCpCj/yrIJ3V4kYbQVySUhrPNIz2k6j7Ief8GiNxxArjXfuGLfgg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -12553,15 +12595,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz",
|
||||
"integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
"is-network-error": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
@@ -13674,14 +13719,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
@@ -13767,9 +13804,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sdp-transform": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
|
||||
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz",
|
||||
"integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
@@ -18411,14 +18449,9 @@
|
||||
}
|
||||
},
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz",
|
||||
"integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og=="
|
||||
},
|
||||
"@matrix-org/olm": {
|
||||
"version": "3.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz",
|
||||
"integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz",
|
||||
"integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
@@ -19835,11 +19868,6 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
@@ -20333,9 +20361,9 @@
|
||||
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
|
||||
},
|
||||
"base-x": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz",
|
||||
"integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ=="
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
|
||||
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
@@ -20859,6 +20887,16 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"commonmark": {
|
||||
"version": "0.31.2",
|
||||
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.31.2.tgz",
|
||||
"integrity": "sha512-2fRLTyb9r/2835k5cwcAwOj0DEc44FARnMp5veGsJ+mEAZdi52sNopLu07ZyElQUz058H43whzlERDIaaSw4rg==",
|
||||
"requires": {
|
||||
"entities": "~3.0.1",
|
||||
"mdurl": "~1.0.1",
|
||||
"minimist": "~1.2.8"
|
||||
}
|
||||
},
|
||||
"compare-versions": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
|
||||
@@ -21307,6 +21345,11 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
|
||||
},
|
||||
"env-editor": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||
@@ -21625,6 +21668,11 @@
|
||||
"base64-js": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"fake-indexeddb": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz",
|
||||
"integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
@@ -22421,6 +22469,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"is-network-error": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
|
||||
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -23173,6 +23226,11 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"linkifyjs": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -23196,6 +23254,11 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"lodash.escape": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
|
||||
"integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw=="
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
@@ -23413,9 +23476,9 @@
|
||||
}
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
|
||||
"integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg=="
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
@@ -23477,38 +23540,29 @@
|
||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||
},
|
||||
"matrix-js-sdk": {
|
||||
"version": "34.11.1",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.11.1.tgz",
|
||||
"integrity": "sha512-rDbIUIqEsN/pbHb6haBQmjxxgeb9G3Df2IhPPOotUbX6R1KseA8yJ6TAY0YySM2zVaBV3yZ6dnKWexF/uWvZfA==",
|
||||
"version": "41.5.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.5.0.tgz",
|
||||
"integrity": "sha512-CK3h+qQJ4wkVEUgEWc5MdLjccXyiFqncCC53P+auqOhnX2U6tAFsRfnbML1QQiKIsFMzqTrAnF/4a5LUUOIeXg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.8.2",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "10"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
||||
}
|
||||
"p-retry": "8",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"matrix-widget-api": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz",
|
||||
"integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==",
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz",
|
||||
"integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==",
|
||||
"requires": {
|
||||
"@types/events": "^3.0.0",
|
||||
"events": "^3.2.0"
|
||||
@@ -23543,6 +23597,11 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -24370,9 +24429,7 @@
|
||||
"minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.6",
|
||||
@@ -24724,10 +24781,6 @@
|
||||
"jwt-decode": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"olm": {
|
||||
"version": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download",
|
||||
"integrity": "sha512-5j5CLplrEodeqGMHEzwDZz6UlqWHyR8c1+tCpCj/yrIJ3V4kYbQVySUhrPNIz2k6j7Ief8GiNxxArjXfuGLfgg=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -24921,12 +24974,11 @@
|
||||
}
|
||||
},
|
||||
"p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz",
|
||||
"integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==",
|
||||
"requires": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
"is-network-error": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
@@ -25798,11 +25850,6 @@
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="
|
||||
},
|
||||
"reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
@@ -25867,9 +25914,9 @@
|
||||
}
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
|
||||
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw=="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz",
|
||||
"integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.6.2",
|
||||
|
||||
+12
-6
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"name": "node-red-contrib-matrix-chat",
|
||||
"version": "0.9.2",
|
||||
"version": "1.0.0",
|
||||
"description": "Matrix chat server client for Node-RED",
|
||||
"dependencies": {
|
||||
"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": "34.11.1",
|
||||
"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/2572/download",
|
||||
"sharp": "^0.33.4",
|
||||
"tmp": "^0.2.1",
|
||||
"utf8": "^3.0.0"
|
||||
@@ -51,11 +54,14 @@
|
||||
"matrix-whois-user": "src/matrix-whois-user.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-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",
|
||||
@@ -66,7 +72,7 @@
|
||||
],
|
||||
"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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -32,7 +32,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -106,14 +106,14 @@ module.exports = function(RED) {
|
||||
let user2 = {};
|
||||
|
||||
try {
|
||||
let profileInfo = node.server.matrixClient.getProfileInfo(userId);
|
||||
if(Object.keys(profileInfo).length > 0) {
|
||||
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 = node.server.matrixClient.getPresence(userId);
|
||||
if(Object.keys(presence).length > 0) {
|
||||
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;
|
||||
|
||||
@@ -54,9 +54,10 @@ module.exports = function(RED) {
|
||||
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]);
|
||||
|
||||
@@ -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 };
|
||||
+34
-2
@@ -227,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>'
|
||||
@@ -352,10 +357,37 @@
|
||||
</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>URI format of the geolocation</dd>
|
||||
<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>
|
||||
|
||||
+67
-4
@@ -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);
|
||||
@@ -47,11 +60,31 @@ module.exports = function(RED) {
|
||||
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) {
|
||||
msg.url = node.server.matrixClient.mxcUrlToHttp(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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,8 +92,18 @@ module.exports = function(RED) {
|
||||
const thumbnailFile = msg.content.info?.[infoKey];
|
||||
const thumbnailUrl = thumbnailFile?.url;
|
||||
if (thumbnailUrl) {
|
||||
msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,11 +159,31 @@ module.exports = function(RED) {
|
||||
setThumbnailUrls('thumbnail_file');
|
||||
break;
|
||||
|
||||
case 'm.location':
|
||||
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;
|
||||
@@ -141,4 +204,4 @@ module.exports = function(RED) {
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-receive", MatrixReceiveMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -99,6 +99,7 @@
|
||||
</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>
|
||||
@@ -140,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>
|
||||
@@ -150,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,4 +1,5 @@
|
||||
const sdkPromise = import("matrix-js-sdk");
|
||||
const { Markdown } = require("./matrix-markdown");
|
||||
|
||||
module.exports = function(RED) {
|
||||
function MatrixSendImage(n) {
|
||||
@@ -143,7 +144,28 @@ module.exports = function(RED) {
|
||||
body: payload.toString()
|
||||
};
|
||||
|
||||
if (msgFormat === 'html') {
|
||||
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)
|
||||
|
||||
+806
-70
@@ -31,17 +31,765 @@
|
||||
accessToken: { type: "password", required: true },
|
||||
deviceId: { type: "text", 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 },
|
||||
allowUnknownDevices: { type: "checkbox", value: false }
|
||||
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>
|
||||
@@ -145,88 +896,73 @@
|
||||
Allow sending messages to a room with unknown devices which have not been verified.
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$("#matrix-login-btn").on("click", function() {
|
||||
function prettyPrintJson(json) {
|
||||
try{
|
||||
return typeof json === 'object' ? JSON.stringify(json, null, 2) : json;
|
||||
}
|
||||
catch (error){
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
let userId = $("#node-config-input-userId").val(),
|
||||
userPassword = $("#node-config-input-password").val(),
|
||||
serverUrl = $("#node-config-input-url").val();
|
||||
<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>
|
||||
|
||||
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-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>
|
||||
|
||||
$("#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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
+981
-331
File diff suppressed because it is too large
Load Diff
@@ -293,7 +293,8 @@ module.exports = function(RED) {
|
||||
msg.payload.url = file.content_uri;
|
||||
}
|
||||
msg.payload.msgtype = msgtype;
|
||||
msg.payload.body = msg.body || msg.filename || "";
|
||||
msg.payload.body = msg.body || filename || "";
|
||||
msg.payload.filename = filename;
|
||||
msg.payload.info = {
|
||||
"mimetype": contentType,
|
||||
"size": getFileSize(bufferOrPath),
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-verification-action',{
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
inputs: 1,
|
||||
outputs: 2,
|
||||
outputLabels: ["success", "error"],
|
||||
align: 'right',
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { type: "matrix-server-config" },
|
||||
mode: { value: "accept" }
|
||||
},
|
||||
label: function() {
|
||||
const labels = {
|
||||
request: "Request verification",
|
||||
accept: "Accept verification",
|
||||
start: "Start SAS verification",
|
||||
confirm: "Confirm SAS",
|
||||
mismatch: "Reject SAS (mismatch)",
|
||||
cancel: "Cancel verification"
|
||||
};
|
||||
return this.name || labels[this.mode] || "Verification Action";
|
||||
},
|
||||
paletteLabel: 'Verification Action'
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-verification-action">
|
||||
<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-shield"></i> Action</label>
|
||||
<select id="node-input-mode">
|
||||
<option value="request">Request verification</option>
|
||||
<option value="accept">Accept verification</option>
|
||||
<option value="start">Start SAS verification</option>
|
||||
<option value="confirm">Confirm SAS</option>
|
||||
<option value="mismatch">Reject SAS (mismatch)</option>
|
||||
<option value="cancel">Cancel verification</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
The action can also be overridden per message with <code>msg.mode</code>.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-verification-action">
|
||||
<h3>Details</h3>
|
||||
<p>Acts on a Matrix device verification: request, accept, start, confirm or cancel it.</p>
|
||||
|
||||
<p>
|
||||
This is the action counterpart to the <code>matrix-verification</code> node. The
|
||||
<b>Action</b> set on the node decides what it does; it can also be overridden per
|
||||
message with <code>msg.mode</code>. Every action except <b>Request verification</b>
|
||||
operates on an existing verification identified by <code>msg.verificationId</code>
|
||||
(as emitted by the <code>matrix-verification</code> node).
|
||||
</p>
|
||||
<p>
|
||||
A typical SAS (emoji) flow: receive a <code>requested</code> event →
|
||||
<b>Accept</b> → <b>Start SAS</b> → receive the <code>sas</code> event with
|
||||
the emoji → have a human confirm → <b>Confirm SAS</b>.
|
||||
</p>
|
||||
|
||||
<h3>Actions</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>Request verification</dt>
|
||||
<dd>Starts a new verification. With <code>msg.userId</code> + <code>msg.deviceId</code> it verifies a specific device; with <code>msg.userId</code> + <code>msg.topic</code> it verifies a user in that DM room; with neither it verifies the bot's own other devices.</dd>
|
||||
|
||||
<dt>Accept verification</dt>
|
||||
<dd>Accepts an incoming verification request (moves it to the <code>ready</code> phase).</dd>
|
||||
|
||||
<dt>Start SAS verification</dt>
|
||||
<dd>Begins SAS (emoji) verification. The emoji/decimal are delivered through the <code>matrix-verification</code> node as a <code>sas</code> phase event.</dd>
|
||||
|
||||
<dt>Confirm SAS</dt>
|
||||
<dd>Confirms that the SAS emoji/decimal match. Call this once a human has verified the emoji.</dd>
|
||||
|
||||
<dt>Reject SAS (mismatch)</dt>
|
||||
<dd>Declares that the SAS does not match, cancelling the verification.</dd>
|
||||
|
||||
<dt>Cancel verification</dt>
|
||||
<dd>Cancels the verification request.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt class="optional">msg.mode <span class="property-type">string</span></dt>
|
||||
<dd>optional. Overrides the node's configured Action. One of <code>request</code>, <code>accept</code>, <code>start</code>, <code>confirm</code>, <code>mismatch</code>, <code>cancel</code>.</dd>
|
||||
|
||||
<dt>msg.verificationId <span class="property-type">string</span></dt>
|
||||
<dd>required for every action except <code>request</code>. The id of the verification to act on, from the <code>matrix-verification</code> node.</dd>
|
||||
|
||||
<dt class="optional">msg.userId <span class="property-type">string</span></dt>
|
||||
<dd>used by <code>request</code>: the user to verify.</dd>
|
||||
|
||||
<dt class="optional">msg.deviceId <span class="property-type">string</span></dt>
|
||||
<dd>used by <code>request</code>: the specific device to verify (to-device verification).</dd>
|
||||
|
||||
<dt class="optional">msg.topic <span class="property-type">string</span></dt>
|
||||
<dd>used by <code>request</code>: a DM room id, to verify a user in-room.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Success
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verificationId <span class="property-type">string</span></dt>
|
||||
<dd>the id of the verification that was acted on (or created, for <code>request</code>).</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,139 @@
|
||||
module.exports = function(RED) {
|
||||
function MatrixVerificationAction(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
let node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.mode = n.mode || "accept";
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
const crypto = node.server.matrixClient.getCrypto();
|
||||
if (!crypto) {
|
||||
msg.error = "End-to-end encryption is not enabled on the Matrix server config";
|
||||
node.error(msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
// msg.mode overrides the node's configured mode if provided
|
||||
const mode = msg.mode || node.mode;
|
||||
|
||||
try {
|
||||
if (mode === "request") {
|
||||
// Start a new verification request.
|
||||
// - msg.userId + msg.deviceId : verify a specific device (to-device)
|
||||
// - msg.userId + msg.topic : verify a user in a DM room
|
||||
// - otherwise : verify our own other devices
|
||||
let request;
|
||||
if (msg.userId && msg.deviceId) {
|
||||
request = await crypto.requestDeviceVerification(msg.userId, msg.deviceId);
|
||||
} else if (msg.userId && msg.topic) {
|
||||
request = await crypto.requestVerificationDM(msg.userId, msg.topic);
|
||||
} else {
|
||||
request = await crypto.requestOwnUserVerification();
|
||||
}
|
||||
|
||||
if (typeof node.server.trackVerificationRequest === "function") {
|
||||
node.server.trackVerificationRequest(request);
|
||||
}
|
||||
msg.verificationId = request.transactionId;
|
||||
node.send([msg, null]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Every other mode acts on an existing tracked request.
|
||||
const request = node.server.verificationRequests.get(msg.verificationId);
|
||||
if (!request) {
|
||||
throw new Error(`No active verification found for msg.verificationId '${msg.verificationId}'`);
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case "accept":
|
||||
await request.accept();
|
||||
break;
|
||||
|
||||
case "start": {
|
||||
// Begin SAS (emoji) verification. The SAS emoji is delivered
|
||||
// through the matrix-verification node when it becomes ready.
|
||||
let verifier = request.verifier;
|
||||
if (!verifier) {
|
||||
verifier = await request.startVerification("m.sas.v1");
|
||||
}
|
||||
verifier.verify().catch(function(e) {
|
||||
node.warn("Verification ended: " + e);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "confirm": {
|
||||
const sas = node.server.verificationSas.get(msg.verificationId);
|
||||
if (!sas) {
|
||||
throw new Error("This verification has no SAS awaiting confirmation");
|
||||
}
|
||||
await sas.confirm();
|
||||
break;
|
||||
}
|
||||
|
||||
case "mismatch": {
|
||||
const sas = node.server.verificationSas.get(msg.verificationId);
|
||||
if (!sas) {
|
||||
throw new Error("This verification has no SAS awaiting confirmation");
|
||||
}
|
||||
sas.mismatch();
|
||||
break;
|
||||
}
|
||||
|
||||
case "cancel":
|
||||
await request.cancel();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown verification action mode: " + mode);
|
||||
}
|
||||
|
||||
msg.verificationId = request.transactionId;
|
||||
node.send([msg, null]);
|
||||
} catch (e) {
|
||||
msg.error = String(e && e.message || e);
|
||||
node.error("Verification action failed: " + msg.error, msg);
|
||||
node.send([null, msg]);
|
||||
}
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-verification-action", MatrixVerificationAction);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-verification',{
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
icon: "matrix.png",
|
||||
inputs: 0,
|
||||
outputs: 1,
|
||||
defaults: {
|
||||
name: { value: null },
|
||||
server: { type: "matrix-server-config" },
|
||||
phaseRequested: { value: true },
|
||||
phaseReady: { value: true },
|
||||
phaseStarted: { value: true },
|
||||
phaseSas: { value: true },
|
||||
phaseDone: { value: true },
|
||||
phaseCancelled: { value: true },
|
||||
initiatedBy: { value: "any" },
|
||||
verificationType: { value: "any" },
|
||||
selfVerification: { value: "any" },
|
||||
userFilter: { value: "" },
|
||||
roomFilter: { value: "" }
|
||||
},
|
||||
label: function() {
|
||||
return this.name || "Verification";
|
||||
},
|
||||
paletteLabel: 'Verification'
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-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-server"></i> Matrix Server</label>
|
||||
<input type="text" id="node-input-server">
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-left: 100px;margin-top:10px;font-weight:bold;">
|
||||
Phase filter
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:6px;">Emit only the ticked phases.</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input type="checkbox" id="node-input-phaseRequested" style="width:auto;margin-left:125px;vertical-align:top" />
|
||||
<label for="node-input-phaseRequested" style="width:auto">requested</label>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input type="checkbox" id="node-input-phaseReady" style="width:auto;margin-left:125px;vertical-align:top" />
|
||||
<label for="node-input-phaseReady" style="width:auto">ready</label>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input type="checkbox" id="node-input-phaseStarted" style="width:auto;margin-left:125px;vertical-align:top" />
|
||||
<label for="node-input-phaseStarted" style="width:auto">started</label>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input type="checkbox" id="node-input-phaseSas" style="width:auto;margin-left:125px;vertical-align:top" />
|
||||
<label for="node-input-phaseSas" style="width:auto">sas (emoji ready to confirm)</label>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<input type="checkbox" id="node-input-phaseDone" style="width:auto;margin-left:125px;vertical-align:top" />
|
||||
<label for="node-input-phaseDone" style="width:auto">done</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input type="checkbox" id="node-input-phaseCancelled" style="width:auto;margin-left:125px;vertical-align:top" />
|
||||
<label for="node-input-phaseCancelled" style="width:auto">cancelled</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-initiatedBy"><i class="fa fa-filter"></i> Initiated by</label>
|
||||
<select id="node-input-initiatedBy">
|
||||
<option value="any">Any</option>
|
||||
<option value="me">Me (the bot started it)</option>
|
||||
<option value="notme">Not me (the other party)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-verificationType"><i class="fa fa-filter"></i> Type</label>
|
||||
<select id="node-input-verificationType">
|
||||
<option value="any">Any</option>
|
||||
<option value="room">Room (in-room / DM)</option>
|
||||
<option value="device">Device (to-device)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-selfVerification"><i class="fa fa-filter"></i> Self-verify</label>
|
||||
<select id="node-input-selfVerification">
|
||||
<option value="any">Any</option>
|
||||
<option value="self">Self only (bot's own devices)</option>
|
||||
<option value="others">Others only (other users)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-userFilter"><i class="fa fa-user"></i> User IDs</label>
|
||||
<input type="text" id="node-input-userFilter" placeholder="@alice:example.org, @bob:example.org">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">User ID allowlist - only emit verifications involving these users. Comma separated, or blank for any.</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-roomFilter"><i class="fa fa-comments"></i> Room IDs</label>
|
||||
<input type="text" id="node-input-roomFilter" placeholder="!room:example.org, !other:example.org">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">Restrict room verifications to these rooms. Comma separated, or blank for any. Device verifications are not affected.</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-verification">
|
||||
<h3>Details</h3>
|
||||
<p>Emits a message when a device verification request is received or changes phase.</p>
|
||||
|
||||
<p>
|
||||
This node is the event source for device verification. It outputs a message every
|
||||
time a verification request is created (either incoming, or started with the
|
||||
<code>matrix-verification-action</code> node) and again on every phase change. Use
|
||||
it together with <code>matrix-verification-action</code> to build your own approval
|
||||
flow — for example, emailing the SAS emoji to a human for confirmation.
|
||||
</p>
|
||||
<p>
|
||||
Each message carries a <code>msg.verificationId</code> which is the handle you pass
|
||||
to <code>matrix-verification-action</code> to act on that verification.
|
||||
</p>
|
||||
|
||||
<h3>Filters</h3>
|
||||
<p>
|
||||
All filters are applied on the node so you don't need <code>switch</code> nodes
|
||||
downstream. They AND-combine, and each defaults to passing everything.
|
||||
</p>
|
||||
<dl class="message-properties">
|
||||
<dt>Phase filter</dt>
|
||||
<dd>Emits only the ticked phases (<code>requested</code>, <code>ready</code>, <code>started</code>, <code>sas</code>, <code>done</code>, <code>cancelled</code>). All ticked = emit every phase.</dd>
|
||||
|
||||
<dt>Initiated by</dt>
|
||||
<dd><code>Any</code>, <code>Me</code> (only verifications the bot started), or <code>Not me</code> (only verifications started by the other party).</dd>
|
||||
|
||||
<dt>Type</dt>
|
||||
<dd><code>Any</code>, <code>Room</code> (in-room / DM verifications), or <code>Device</code> (to-device verifications).</dd>
|
||||
|
||||
<dt>Self-verify</dt>
|
||||
<dd><code>Any</code>, <code>Self only</code> (the other party is another of the bot's own devices), or <code>Others only</code> (a different user).</dd>
|
||||
|
||||
<dt>User IDs</dt>
|
||||
<dd>Allowlist of user IDs (comma separated). When set, only verifications involving one of these users are emitted. Blank = any user.</dd>
|
||||
|
||||
<dt>Room IDs</dt>
|
||||
<dd>Comma separated room IDs. When set, room verifications are restricted to these rooms. Device verifications have no room and are not affected by this filter. Blank = any room.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.verificationId <span class="property-type">string</span></dt>
|
||||
<dd>unique id of the verification. Pass this as <code>msg.verificationId</code> to the <code>matrix-verification-action</code> node.</dd>
|
||||
|
||||
<dt>msg.phase <span class="property-type">string</span></dt>
|
||||
<dd>
|
||||
current phase of the verification, one of:
|
||||
<code>requested</code> (a request was received/sent),
|
||||
<code>ready</code> (the request was accepted),
|
||||
<code>started</code> (a method was chosen),
|
||||
<code>sas</code> (SAS emoji/decimal are ready to confirm — see <code>msg.sas</code>),
|
||||
<code>done</code> (verification completed),
|
||||
<code>cancelled</code> (verification cancelled).
|
||||
</dd>
|
||||
|
||||
<dt>msg.payload <span class="property-type">string</span></dt>
|
||||
<dd>same value as <code>msg.phase</code>, for convenience.</dd>
|
||||
|
||||
<dt>msg.userId <span class="property-type">string</span></dt>
|
||||
<dd>the user id of the other party in the verification.</dd>
|
||||
|
||||
<dt>msg.deviceId <span class="property-type">string | null</span></dt>
|
||||
<dd>the other party's device id, for to-device verifications. <code>null</code> for in-room verifications.</dd>
|
||||
|
||||
<dt>msg.topic <span class="property-type">string | null</span></dt>
|
||||
<dd>the room id, for in-room (DM) verifications. <code>null</code> for to-device verifications.</dd>
|
||||
|
||||
<dt>msg.sas <span class="property-type">object</span></dt>
|
||||
<dd>
|
||||
present only when <code>msg.phase</code> is <code>sas</code>. Contains
|
||||
<code>emoji</code> (an array of <code>[emoji, name]</code> pairs) and
|
||||
<code>decimal</code> (an array of three numbers). One or both may be
|
||||
populated depending on what was negotiated.
|
||||
</dd>
|
||||
|
||||
<dt>msg.isSelfVerification <span class="property-type">boolean</span></dt>
|
||||
<dd><code>true</code> if the other party is another device of the bot's own account.</dd>
|
||||
|
||||
<dt>msg.initiatedByMe <span class="property-type">boolean</span></dt>
|
||||
<dd><code>true</code> if this verification was started by the bot.</dd>
|
||||
|
||||
<dt>msg.chosenMethod <span class="property-type">string | null</span></dt>
|
||||
<dd>the verification method that was chosen (e.g. <code>m.sas.v1</code>), once one has been picked; <code>null</code> before then.</dd>
|
||||
|
||||
<dt>msg.cancellationCode <span class="property-type">string | null</span></dt>
|
||||
<dd>present when <code>msg.phase</code> is <code>cancelled</code>; the reason code (e.g. <code>m.user</code>).</dd>
|
||||
</dl>
|
||||
|
||||
<h3>References</h3>
|
||||
<ul>
|
||||
<li><a href="https://spec.matrix.org/latest/client-server-api/#device-verification">Matrix spec</a> - device verification</li>
|
||||
</ul>
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
module.exports = function(RED) {
|
||||
function MatrixVerification(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
|
||||
let node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
|
||||
// Phase filter - emit only the ticked phases. Undefined (config saved
|
||||
// before these options existed) is treated as ticked, so old nodes
|
||||
// keep emitting every phase.
|
||||
this.phases = {
|
||||
requested: n.phaseRequested !== false,
|
||||
ready: n.phaseReady !== false,
|
||||
started: n.phaseStarted !== false,
|
||||
sas: n.phaseSas !== false,
|
||||
done: n.phaseDone !== false,
|
||||
cancelled: n.phaseCancelled !== false,
|
||||
};
|
||||
this.initiatedBy = n.initiatedBy || 'any'; // any | me | notme
|
||||
this.verificationType = n.verificationType || 'any'; // any | room | device
|
||||
this.selfVerification = n.selfVerification || 'any'; // any | self | others
|
||||
this.userFilter = (n.userFilter || '').split(',')
|
||||
.map(function(s){ return s.trim().toLowerCase(); })
|
||||
.filter(Boolean);
|
||||
this.roomFilter = (n.roomFilter || '').split(',')
|
||||
.map(function(s){ return s.trim(); })
|
||||
.filter(Boolean);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
|
||||
if (!node.server) {
|
||||
node.error("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
// Returns true if a verification update message passes every configured
|
||||
// filter. All filters AND-combine; each defaults to "pass everything".
|
||||
function passesFilters(m) {
|
||||
// phase
|
||||
if ((m.phase in node.phases) && !node.phases[m.phase]) {
|
||||
return false;
|
||||
}
|
||||
// initiated by
|
||||
if (node.initiatedBy === 'me' && !m.initiatedByMe) {
|
||||
return false;
|
||||
}
|
||||
if (node.initiatedBy === 'notme' && m.initiatedByMe) {
|
||||
return false;
|
||||
}
|
||||
// verification type - room verifications carry a roomId (msg.topic),
|
||||
// to-device verifications do not
|
||||
if (node.verificationType === 'room' && !m.topic) {
|
||||
return false;
|
||||
}
|
||||
if (node.verificationType === 'device' && m.topic) {
|
||||
return false;
|
||||
}
|
||||
// self-verification (the other party is one of the bot's own devices)
|
||||
if (node.selfVerification === 'self' && !m.isSelfVerification) {
|
||||
return false;
|
||||
}
|
||||
if (node.selfVerification === 'others' && m.isSelfVerification) {
|
||||
return false;
|
||||
}
|
||||
// user id allowlist
|
||||
if (node.userFilter.length &&
|
||||
(!m.userId || node.userFilter.indexOf(m.userId.toLowerCase()) === -1)) {
|
||||
return false;
|
||||
}
|
||||
// room id filter - only constrains room verifications; device
|
||||
// verifications have no room and are not affected
|
||||
if (node.roomFilter.length && m.topic &&
|
||||
node.roomFilter.indexOf(m.topic) === -1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const onConnected = function() {
|
||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||
};
|
||||
const onDisconnected = function() {
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
};
|
||||
const onVerificationUpdate = function(verificationMsg) {
|
||||
if (!passesFilters(verificationMsg)) {
|
||||
return;
|
||||
}
|
||||
node.status({ fill: "blue", shape: "dot", text: verificationMsg.phase });
|
||||
// clone so multiple verification nodes don't share/mutate one object
|
||||
node.send(RED.util.cloneMessage(verificationMsg));
|
||||
};
|
||||
|
||||
node.server.on("connected", onConnected);
|
||||
node.server.on("disconnected", onDisconnected);
|
||||
node.server.on("Verification.update", onVerificationUpdate);
|
||||
|
||||
if (node.server.isConnected && node.server.isConnected()) {
|
||||
onConnected();
|
||||
}
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.removeListener("connected", onConnected);
|
||||
node.server.removeListener("disconnected", onDisconnected);
|
||||
node.server.removeListener("Verification.update", onVerificationUpdate);
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("matrix-verification", MatrixVerification);
|
||||
}
|
||||
Reference in New Issue
Block a user