From e8506d888757358a41a1ac40af89ac46baf681c4 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 5 Nov 2023 00:06:52 -0600 Subject: [PATCH] #102 Added new node for file uploading #102 File upload node automatically detects mime type from name #102 File upload node automatically fills in information for m.video, m.audio, and m.image (resolution, duration, etc) #102 File upload node can generate a thumbnail for videos #102 Send message node now accepts an object to override the message content --- package-lock.json | 250 +++++++++++-------- package.json | 5 + src/matrix-file-crypt.js | 0 src/matrix-send-message.html | 7 +- src/matrix-send-message.js | 91 +++---- src/matrix-typing.html | 2 +- src/matrix-upload-file.html | 163 ++++++++++++ src/matrix-upload-file.js | 471 +++++++++++++++++++++++++++++++++++ 8 files changed, 838 insertions(+), 151 deletions(-) create mode 100644 src/matrix-file-crypt.js create mode 100644 src/matrix-upload-file.html create mode 100644 src/matrix-upload-file.js diff --git a/package-lock.json b/package-lock.json index 337704d..a03e54f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,27 @@ { "name": "node-red-contrib-matrix-chat", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-red-contrib-matrix-chat", - "version": "0.7.0", + "version": "0.7.1", "license": "SEE LICENSE FILE", "dependencies": { "abort-controller": "^3.0.0", + "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": "^28.0.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", "request": "^2.88.2", + "tmp": "^0.2.1", "utf8": "^3.0.0" }, "engines": { @@ -3107,6 +3111,19 @@ "ms": "^2.1.1" } }, + "node_modules/@expo/devcert/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "optional": true, + "peer": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/@expo/env": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.0.5.tgz", @@ -3167,6 +3184,19 @@ "node": ">=10" } }, + "node_modules/@expo/image-utils/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/@expo/image-utils/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -4992,6 +5022,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native-community/cli-tools/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/@react-native-community/cli-tools/node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5909,9 +5952,7 @@ "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "optional": true, - "peer": true + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "node_modules/async-limiter": { "version": "1.0.1", @@ -6168,9 +6209,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "optional": true, - "peer": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base-64": { "version": "0.1.0", @@ -6336,8 +6375,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6948,9 +6985,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "optional": true, - "peer": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/connect": { "version": "3.7.0", @@ -8137,6 +8172,18 @@ "node": ">=0.4.0" } }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fontfaceobserver": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", @@ -8235,9 +8282,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "optional": true, - "peer": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -8339,8 +8384,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "optional": true, - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8716,8 +8759,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", - "optional": true, - "peer": true, "dependencies": { "queue": "6.0.2" }, @@ -8781,8 +8822,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "optional": true, - "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8791,9 +8830,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "optional": true, - "peer": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -9098,9 +9135,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true, - "peer": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -11453,16 +11488,14 @@ } }, "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "optional": true, - "peer": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=10.0.0" } }, "node_modules/mime-db": { @@ -11509,8 +11542,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12022,8 +12053,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "optional": true, - "peer": true, "dependencies": { "wrappy": "1" } @@ -12404,8 +12433,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12886,8 +12913,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "optional": true, - "peer": true, "dependencies": { "inherits": "~2.0.3" } @@ -14478,16 +14503,28 @@ } }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "optional": true, - "peer": true, + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dependencies": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" }, "engines": { - "node": ">=0.6.0" + "node": ">=8.17.0" + } + }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tmpl": { @@ -15002,8 +15039,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -15059,9 +15094,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "optional": true, - "peer": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "2.4.3", @@ -17519,6 +17552,16 @@ "requires": { "ms": "^2.1.1" } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "optional": true, + "peer": true, + "requires": { + "os-tmpdir": "~1.0.2" + } } } }, @@ -17576,6 +17619,13 @@ "universalify": "^1.0.0" } }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "optional": true, + "peer": true + }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -19191,6 +19241,13 @@ "is-unicode-supported": "^0.1.0" } }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "optional": true, + "peer": true + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -19753,9 +19810,7 @@ "async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "optional": true, - "peer": true + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "async-limiter": { "version": "1.0.1", @@ -19981,9 +20036,7 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "optional": true, - "peer": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base-64": { "version": "0.1.0", @@ -20123,8 +20176,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "peer": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -20582,9 +20633,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "optional": true, - "peer": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "connect": { "version": "3.7.0", @@ -21524,6 +21573,15 @@ "optional": true, "peer": true }, + "fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "requires": { + "async": ">=0.2.9", + "which": "^1.1.1" + } + }, "fontfaceobserver": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", @@ -21598,9 +21656,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "optional": true, - "peer": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "2.3.3", @@ -21674,8 +21730,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "optional": true, - "peer": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -21949,8 +22003,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", - "optional": true, - "peer": true, "requires": { "queue": "6.0.2" } @@ -21998,8 +22050,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "optional": true, - "peer": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -22008,9 +22058,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "optional": true, - "peer": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -22242,9 +22290,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true, - "peer": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "isobject": { "version": "3.0.1", @@ -24103,11 +24149,9 @@ } }, "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "optional": true, - "peer": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" }, "mime-db": { "version": "1.52.0", @@ -24138,8 +24182,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "peer": true, "requires": { "brace-expansion": "^1.1.7" } @@ -24532,8 +24574,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "optional": true, - "peer": true, "requires": { "wrappy": "1" } @@ -24824,9 +24864,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "optional": true, - "peer": true + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "2.0.1", @@ -25193,8 +25231,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "optional": true, - "peer": true, "requires": { "inherits": "~2.0.3" } @@ -26474,13 +26510,21 @@ } }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "optional": true, - "peer": true, + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "requires": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } } }, "tmpl": { @@ -26887,8 +26931,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, - "peer": true, "requires": { "isexe": "^2.0.0" } @@ -26934,9 +26976,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "optional": true, - "peer": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { "version": "2.4.3", diff --git a/package.json b/package.json index b16db3f..377554e 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,18 @@ "description": "Matrix chat server client for Node-RED", "dependencies": { "abort-controller": "^3.0.0", + "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": "^28.0.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", "request": "^2.88.2", + "tmp": "^0.2.1", "utf8": "^3.0.0" }, "node-red": { @@ -23,6 +27,7 @@ "matrix-delete-event": "src/matrix-delete-event.js", "matrix-send-file": "src/matrix-send-file.js", "matrix-send-image": "src/matrix-send-image.js", + "matrix-upload-file": "src/matrix-upload-file.js", "matrix-react": "src/matrix-react.js", "matrix-create-room": "src/matrix-create-room.js", "matrix-invite-room": "src/matrix-invite-room.js", diff --git a/src/matrix-file-crypt.js b/src/matrix-file-crypt.js new file mode 100644 index 0000000..e69de29 diff --git a/src/matrix-send-message.html b/src/matrix-send-message.html index 5c714ac..3010f6c 100644 --- a/src/matrix-send-message.html +++ b/src/matrix-send-message.html @@ -43,6 +43,9 @@ +
+ If message is an object it sets the full content of the message. +
Room ID to send image to. Optional if configured on the node. If configured on the node this input will be overridden.
msg.payload - string + string|object
-
the message text. If configured on the node this is ignored otherwise it required.
+
the message text or an object to customize the full content. If configured on the node this is ignored otherwise it required.
msg.replace bool diff --git a/src/matrix-send-message.js b/src/matrix-send-message.js index 632ace5..c975348 100644 --- a/src/matrix-send-message.js +++ b/src/matrix-send-message.js @@ -70,22 +70,6 @@ module.exports = function(RED) { let msgType = node.messageType, msgFormat = node.messageFormat; - if(msgType === 'msg.type') { - if(!msg.type) { - node.error("msg.type type is set to be passed in via msg.type but was not defined", msg); - return; - } - msgType = msg.type; - } - - if(msgFormat === 'msg.format') { - if(!msg.format) { - node.error("Message format is set to be passed in via msg.format but was not defined", msg); - return; - } - msgFormat = msg.format; - } - if (!node.server || !node.server.matrixClient) { node.warn("No matrix server selected"); return; @@ -109,36 +93,57 @@ module.exports = function(RED) { return; } - let content = { - msgtype: msgType, - body: payload.toString() - }; - - if(msgFormat === 'html') { - content.format = "org.matrix.custom.html"; - content.formatted_body = - (typeof msg.formatted_payload !== 'undefined' && msg.formatted_payload) - ? msg.formatted_payload.toString() - : payload.toString(); - } - - if((node.replaceMessage || msg.replace) && msg.eventId) { - content['m.new_content'] = { - msgtype: content.msgtype, - body: content.body - }; - if('format' in content) { - content['m.new_content']['format'] = content['format']; - } - if('formatted_body' in content) { - content['m.new_content']['formatted_body'] = content['formatted_body']; + let content = null; + if(typeof payload === 'object') { + content = payload; + } else { + if(msgType === 'msg.type') { + if(!msg.type) { + node.error("msg.type type is set to be passed in via msg.type but was not defined", msg); + return; + } + msgType = msg.type; } - content['m.relates_to'] = { - rel_type: RelationType.Replace, - event_id: msg.eventId + if(msgFormat === 'msg.format') { + if(!msg.format) { + node.error("Message format is set to be passed in via msg.format but was not defined", msg); + return; + } + msgFormat = msg.format; + } + + content = { + msgtype: msgType, + body: payload.toString() }; - content['body'] = ' * ' + content['body']; + + if(msgFormat === 'html') { + content.format = "org.matrix.custom.html"; + content.formatted_body = + (typeof msg.formatted_payload !== 'undefined' && msg.formatted_payload) + ? msg.formatted_payload.toString() + : payload.toString(); + } + + if((node.replaceMessage || msg.replace) && msg.eventId) { + content['m.new_content'] = { + msgtype: content.msgtype, + body: content.body + }; + if('format' in content) { + content['m.new_content']['format'] = content['format']; + } + if('formatted_body' in content) { + content['m.new_content']['formatted_body'] = content['formatted_body']; + } + + content['m.relates_to'] = { + rel_type: RelationType.Replace, + event_id: msg.eventId + }; + content['body'] = ' * ' + content['body']; + } } node.server.matrixClient.sendMessage(msg.topic, content) diff --git a/src/matrix-typing.html b/src/matrix-typing.html index b426357..cd6cf74 100644 --- a/src/matrix-typing.html +++ b/src/matrix-typing.html @@ -76,7 +76,7 @@
- Timeout MS is how many milliseconds the server should show the user typing for. + Timeout Milliseconds is how many milliseconds the server should show the user typing for. Ignored if setting typing to false.
diff --git a/src/matrix-upload-file.html b/src/matrix-upload-file.html new file mode 100644 index 0000000..e0aa396 --- /dev/null +++ b/src/matrix-upload-file.html @@ -0,0 +1,163 @@ + + + + + + \ No newline at end of file diff --git a/src/matrix-upload-file.js b/src/matrix-upload-file.js new file mode 100644 index 0000000..a203615 --- /dev/null +++ b/src/matrix-upload-file.js @@ -0,0 +1,471 @@ +const crypto = require("isomorphic-webcrypto"); +const ffmpeg = require('fluent-ffmpeg'); +const getImageSize = require('image-size'); +const tmp = require('tmp'); +const fs = require('fs'); +const path = require('path'); +module.exports = function(RED) { + function MatrixUploadFile(n) { + RED.nodes.createNode(this, n); + + let node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.inputType = n.inputType; + this.inputValue = n.inputValue; + this.fileNameType = n.fileNameType; + this.fileNameValue = n.fileNameValue; + this.contentType = n.contentType; + this.generateThumbnails = n.generateThumbnails; + + if (!node.server) { + node.warn("No configuration node"); + return; + } + node.server.register(node); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + async function detectFileType(filename, bufferOrPath) + { + const Mime = require('mime'); + let file = Buffer.isBuffer(bufferOrPath) ? filename : bufferOrPath; + + if(file) + { + let type = Mime.getType(file); + let ext = Mime.getExtension(file); + if(type) { + return {ext: ext, mime: type} + } + } + } + + function getFileBuffer(data) + { + if(Buffer.isBuffer(data)) { + return data; + } + + if (data && RED.settings.fileWorkingDirectory && !path.isAbsolute(data)) { + return fs.readFileSync(path.resolve(path.join(RED.settings.fileWorkingDirectory,data))); + } + return fs.readFileSync(data); + } + + function getToValue(msg, type, property) { + let value = property; + if (type === "msg") { + value = RED.util.getMessageProperty(msg, property); + } else if ((type === 'flow') || (type === 'global')) { + try { + value = RED.util.evaluateNodeProperty(property, type, node, msg); + } catch(e2) { + throw new Error("Invalid value evaluation"); + } + } else if(type === "bool") { + value = (property === 'true'); + } else if(type === "num") { + value = Number(property); + } + return value; + } + + node.on("input", onInput); + async function onInput(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); + msg.error = "Matrix server connection is currently closed"; + node.send([null, msg]); + return; + } + + let bufferOrPath = getToValue(msg, node.inputType, node.inputValue); + if(!bufferOrPath) { + node.error('Missing file path/buffer input', msg); + msg.error = 'Missing file path/buffer input'; + node.send([null, msg]); + return; + } + + let filename = getToValue(msg, node.fileNameType, node.fileNameValue); + if(!filename || typeof filename !== 'string') { + if(!Buffer.isBuffer(bufferOrPath)) { + filename = path.basename(bufferOrPath); + } else { + node.error('Missing filename, this is required if input is a file buffer', msg); + msg.error = 'Missing filename, this is required if input is a file buffer'; + node.send([null, msg]); + return; + } + } + + msg.contentType = node.contentType || msg.contentType || null; + let detectedFileType = await detectFileType(filename, bufferOrPath); + node.log("Detected file type " + JSON.stringify(detectedFileType) + " for " + (Buffer.isBuffer(bufferOrPath) ? 'buffer' : `file ${bufferOrPath}`), msg); + + let contentType = msg.contentType || detectedFileType?.mime || null, + msgtype = msg.msgtype || null; + if(!contentType) { + node.warn("Content-type failed to detect, falling back to text/plain", msg); + contentType = 'text/plain'; + } + if(!msgtype) { + msgtype = autoDetectMatrixMessageType(detectedFileType); + } + + let encryptedFile = null; + if(msg.encrypted) { + encryptedFile = await encryptAttachment(getFileBuffer(bufferOrPath)); + } + + node.log("Uploading file ", msg); + let file; + try { + file = await node.server.matrixClient.uploadContent( + encryptedFile?.data || getFileBuffer(bufferOrPath), + { + name: filename, // Name to give the file on the server. + rawResponse: false, // Return the raw body, rather than parsing the JSON. + type: contentType, // Content-type for the upload. Defaults to file.type, or applicaton/octet-stream. + onlyContentUri: false // Just return the content URI, rather than the whole body. Defaults to false. Ignored if opts.rawResponse is true. + }); + } catch(e) { + node.error("Upload content error " + e); + msg.error = e; + node.send([null, msg]); + return; + } + + // we call this method when we need a file and cannot use the buffer + // so if we get passed a buffer we write it to a tmp file and return that + // otherwise we just return the string because it's already a file + let tempFile = null; + function getFile(bufferOrFile) { + if(!Buffer.isBuffer(bufferOrFile)) { + return bufferOrFile; // already a file + } + + if(tempFile) { + return tempFile; + } + + // write buffer to tmp file and return path + let tmpObj = tmp.fileSync({ postfix: `.${detectedFileType.ext}` }); + fs.writeFileSync(tmpObj.name, bufferOrFile); + tempFile = tmpObj.name; + return tmpObj.name; + } + + function deleteTempFile() { + if(!tempFile) return null; + fs.rmSync(tempFile); + } + + // get size of a buffer or file in bytes + function getFileSize(bufferOrPath) { + if(Buffer.isBuffer(bufferOrPath)) { + return Buffer.byteLength(bufferOrPath); + } + + return fs.statSync(bufferOrPath).size; + } + + async function addThumbnail(buffer) { + let imageSize = getImageSize(Buffer.isBuffer(buffer) ? buffer : buffer.data); + msg.payload.info.thumbnail_info = { + w: imageSize.width, + h: imageSize.height, + size: getFileSize(Buffer.isBuffer(buffer) ? buffer : buffer.data) + } + let uploadedThumbnail = await node.server.matrixClient.uploadContent( + Buffer.isBuffer(buffer) ? buffer : buffer.data, + { + name: "thumbnail.png", // Name to give the file on the server. + rawResponse: false, // Return the raw body, rather than parsing the JSON. + type: "image/png", // Content-type for the upload. Defaults to file.type, or applicaton/octet-stream. + onlyContentUri: false // Just return the content URI, rather than the whole body. Defaults to false. Ignored if opts.rawResponse is true. + }); + // delete local file + if(msg.encrypted) { + msg.payload.info.thumbnail_file.url = uploadedThumbnail.content_uri; + } else { + msg.payload.info.thumbnail_url = uploadedThumbnail.content_uri; + } + } + + function _ffmpegVideoThumbnail(filepath){ + return new Promise((resolve,reject) => { + let filename = `${msg._msgid}-screenshot.png`; + ffmpeg(filepath) + .on('end', async function() { + let path = `/tmp/${filename}`; + let buffer = getFileBuffer(path); + let encryptedThumbnail = null; + if(msg.encrypted) { + encryptedThumbnail = await encryptAttachment(buffer); + msg.payload.info.thumbnail_file = encryptedFile.info; + } + try { + await addThumbnail(encryptedThumbnail || buffer); + fs.rmSync(path); // delete temporary thumbnail file + resolve(); + } catch(e) { + return reject(new Error("Thumbnail upload failure: " + e)); + } + }) + .on('error', function(err) { + return reject(err); + }) + .screenshots({ + timestamps: [0], + filename: filename, + folder: '/tmp', + size: '320x?' + }); + }); + } + + msg.payload = {}; + if(msg.encrypted) { + msg.payload.file = encryptedFile?.info || {}; + msg.payload.file.url = file.content_uri; + } else { + msg.payload.url = file.content_uri; + } + msg.payload.msgtype = msgtype; + msg.payload.body = msg.body || msg.filename || ""; + msg.payload.info = { + "mimetype": contentType, + "size": getFileSize(bufferOrPath), + }; + if(msgtype === 'm.image') { + // detect size of image + try { + let imageSize = getImageSize(buffer); + msg.payload.info.h = imageSize.height; + msg.payload.info.w = imageSize.width; + } catch(e) { + node.error("Failed to get image size: " + e, msg); + } + } else if(msgtype === 'm.audio' && detectedFileType) { + try { + // detect duration of audio clip + let filepath = getFile(bufferOrPath); + let metadata = await _ffprobe(filepath); + let audioStream = metadata?.streams.filter(function(stream){return stream.codec_type === "audio" || false;})[0]; + if(audioStream?.duration) { + msg.payload.info.duration = audioStream?.duration * 1000; + } + } catch(e) { + node.error(e, msg); + } + deleteTempFile(); + } else if(msgtype === 'm.video' && detectedFileType) { + let filepath = getFile(bufferOrPath); + + try { + // detect duration & width/height of video clip + let metadata = await _ffprobe(filepath); + let videoStream = metadata?.streams.filter(function(stream){return stream.codec_type === "video" || false;})[0]; + if(videoStream) { + msg.payload.info.duration = videoStream.duration * 1000; + msg.payload.info.w = videoStream.width; + msg.payload.info.h = videoStream.height; + } + } catch(e) { + node.error("ffprobe error: " + e); + } + + if(node.generateThumbnails) { + try { + await _ffmpegVideoThumbnail(filepath); + } catch(e) { + node.error("Screenshot generation error: " + e); + } + } + deleteTempFile(); + } + + node.send(msg, null); + } + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-upload-file", MatrixUploadFile); + + // the following was taken & modified from https://github.com/matrix-org/browser-encrypt-attachment/blob/master/index.js + /** + * Encrypt an attachment. + * @param {ArrayBuffer} plaintextBuffer The attachment data buffer. + * @return {Promise} A promise that resolves with an object when the attachment is encrypted. + * The object has a "data" key with an ArrayBuffer of encrypted data and an "info" key + * with an object containing the info needed to decrypt the data. + */ + function encryptAttachment(plaintextBuffer) { + let cryptoKey; // The AES key object. + let exportedKey; // The AES key exported as JWK. + let ciphertextBuffer; // ArrayBuffer of encrypted data. + let sha256Buffer; // ArrayBuffer of digest. + let ivArray; // Uint8Array of AES IV + // Generate an IV where the first 8 bytes are random and the high 8 bytes + // are zero. We set the counter low bits to 0 since it makes it unlikely + // that the 64 bit counter will overflow. + ivArray = new Uint8Array(16); + crypto.getRandomValues(ivArray.subarray(0,8)); + // Load the encryption key. + return crypto.subtle.generateKey( + {"name": "AES-CTR", length: 256}, true, ["encrypt", "decrypt"] + ).then(function(generateKeyResult) { + cryptoKey = generateKeyResult; + // Export the Key as JWK. + return crypto.subtle.exportKey("jwk", cryptoKey); + }).then(function(exportKeyResult) { + exportedKey = exportKeyResult; + // Encrypt the input ArrayBuffer. + // Use half of the iv as the counter by setting the "length" to 64. + return crypto.subtle.encrypt( + {name: "AES-CTR", counter: ivArray, length: 64}, cryptoKey, plaintextBuffer + ); + }).then(function(encryptResult) { + ciphertextBuffer = encryptResult; + // SHA-256 the encrypted data. + return crypto.subtle.digest("SHA-256", ciphertextBuffer); + }).then(function (digestResult) { + sha256Buffer = digestResult; + + return { + data: ciphertextBuffer, + info: { + v: "v2", + key: exportedKey, + iv: encodeBase64(ivArray), + hashes: { + sha256: encodeBase64(new Uint8Array(sha256Buffer)), + }, + }, + }; + }); + } + + /** + * Decrypt an attachment. + * @param {ArrayBuffer} ciphertextBuffer The encrypted attachment data buffer. + * @param {Object} info The information needed to decrypt the attachment. + * @param {Object} info.key AES-CTR JWK key object. + * @param {string} info.iv Base64 encoded 16 byte AES-CTR IV. + * @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext. + * @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted. + */ + function decryptAttachment(ciphertextBuffer, info) { + + if (info === undefined || info.key === undefined || info.iv === undefined + || info.hashes === undefined || info.hashes.sha256 === undefined) { + throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key"); + } + + let cryptoKey; // The AES key object. + let ivArray = decodeBase64(info.iv); + let expectedSha256base64 = info.hashes.sha256; + // Load the AES from the "key" key of the info object. + return crypto.subtle.importKey( + "jwk", info.key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"] + ).then(function (importKeyResult) { + cryptoKey = importKeyResult; + // Check the sha256 hash + return crypto.subtle.digest("SHA-256", ciphertextBuffer); + }).then(function (digestResult) { + if (encodeBase64(new Uint8Array(digestResult)) !== expectedSha256base64) { + throw new Error("Mismatched SHA-256 digest (expected: " + encodeBase64(new Uint8Array(digestResult)) + ") got (" + expectedSha256base64 + ")"); + } + let counterLength; + if (info.v.toLowerCase() === "v1" || info.v.toLowerCase() === "v2") { + // Version 1 and 2 use a 64 bit counter. + counterLength = 64; + } else { + // Version 0 uses a 128 bit counter. + counterLength = 128; + } + return crypto.subtle.decrypt( + {name: "AES-CTR", counter: ivArray, length: counterLength}, cryptoKey, ciphertextBuffer + ); + }); + } + + /** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64 without padding. + */ + function encodeBase64(uint8Array) { + // Misinterpt the Uint8Array as Latin-1. + // window.btoa expects a unicode string with codepoints in the range 0-255. + // var latin1String = String.fromCharCode.apply(null, uint8Array); + // Use the builtin base64 encoder. + var paddedBase64 = btoa(uint8Array); + // Calculate the unpadded length. + var inputLength = uint8Array.length; + var outputLength = 4 * Math.floor((inputLength + 2) / 3) + (inputLength + 2) % 3 - 2; + // Return the unpadded base64. + return paddedBase64.slice(0, outputLength); + } + + /** + * Decode a base64 string to a typed array of uint8. + * This will decode unpadded base64, but will also accept base64 with padding. + * @param {string} base64 The unpadded base64 to decode. + * @return {Uint8Array} The decoded data. + */ + function decodeBase64(base64) { + // Pad the base64 up to the next multiple of 4. + var paddedBase64 = base64 + "===".slice(0, (4 - base64.length % 4) % 4); + // Decode the base64 as a misinterpreted Latin-1 string. + // window.atob returns a unicode string with codepoints in the range 0-255. + var latin1String = atob(paddedBase64); + // Encode the string as a Uint8Array as Latin-1. + var uint8Array = new Uint8Array(latin1String.length); + for (var i = 0; i < latin1String.length; i++) { + uint8Array[i] = latin1String.charCodeAt(i); + } + return uint8Array; + } + + function autoDetectMatrixMessageType(fileType) { + switch(fileType ? fileType.mime.split('/')[0].toLowerCase() : undefined) { + case 'video': return 'm.video'; + case 'image': return 'm.image'; + case 'audio': return 'm.audio'; + default: return 'm.file'; + } + } + + // ffprobe method for getting metadata from a file wrapped in a promise + function _ffprobe(filepath){ + return new Promise((resolve,reject) => { + ffmpeg.ffprobe(filepath, function(err, metadata) { + if(err) { + return reject(new Error(err)); + } + + resolve(metadata); + }); + }); + } +} \ No newline at end of file