From bde495fcce431e0e712d8b0f0b3d2a2effddc90c Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Wed, 2 Apr 2025 00:34:00 +0100 Subject: [PATCH] Add command/menu item to export log files --- package-lock.json | 254 ++++++++++++++++++++++------ package.json | 4 +- resources/common/remote-config.json | 1 + src/app/i18n/locale/de-de.ts | 4 +- src/app/i18n/locale/en-gb.ts | 1 + src/app/main/app-menu.ts | 7 + src/app/main/ipc.ts | 2 +- src/app/main/support.ts | 88 ++++++++++ src/cli/util/commands.ts | 2 + src/cli/util/decrypt-log-archive.ts | 34 ++++ src/cli/util/log-archive.ts | 70 ++++++++ src/common/remote-config.ts | 2 + src/util/support.ts | 119 +++++++++++++ 13 files changed, 536 insertions(+), 52 deletions(-) create mode 100644 src/app/main/support.ts create mode 100644 src/cli/util/decrypt-log-archive.ts create mode 100644 src/cli/util/log-archive.ts create mode 100644 src/util/support.ts diff --git a/package-lock.json b/package-lock.json index 9ae8899..c78976f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.6.1", "license": "AGPL-3.0-or-later", "dependencies": { + "@samuelthomas2774/saltpack": "^0.4.0", "body-parser": "^1.20.2", "cli-table": "^0.3.11", "debug": "^4.3.4", @@ -23,6 +24,7 @@ "sharp": "^0.33.3", "splatnet3-types": "^0.2.20231119210145", "supports-color": "^9.4.0", + "tar": "^7.4.3", "tslib": "^2.6.2", "undici": "^6.15.0", "yargs": "^17.7.2" @@ -2982,6 +2984,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -3223,6 +3246,15 @@ "node": ">= 10.0.0" } }, + "node_modules/@msgpack/msgpack": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-1.12.2.tgz", + "integrity": "sha512-Vwhc3ObxmDZmA5hY8mfsau2rJ4vGPvzbj20QSZ2/E1GDPF61QVyjLfNHak9xmel6pW4heRt3v1fHa6np9Ehfeg==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4262,6 +4294,21 @@ "win32" ] }, + "node_modules/@samuelthomas2774/saltpack": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@samuelthomas2774/saltpack/-/saltpack-0.4.0.tgz", + "integrity": "sha512-QmG4/hHBefj4ph5HBfK5ZMzRM0lPFLozltQjddy58XaFupPBSgydHWKwut5bVmxcyRNRqefSvRoLrcnqObYbKQ==", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^1.12.2", + "lodash.chunk": "^4.2.0", + "pumpify": "^2.0.1", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": "^12.0.0 || >=14.0.0" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -5032,6 +5079,16 @@ "electron-builder-squirrel-windows": "24.13.3" } }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -5060,6 +5117,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/app-builder-lib/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -5073,6 +5170,24 @@ "node": ">=10" } }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/app-builder-lib/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -5884,13 +5999,12 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/chrome-launcher": { @@ -6988,6 +7102,18 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7237,7 +7363,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -9342,6 +9467,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.chunk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10360,30 +10491,24 @@ } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" + "node": ">= 18" } }, "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { @@ -10713,7 +10838,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -11251,13 +11375,23 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11713,9 +11847,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -12694,13 +12826,17 @@ "node": ">= 0.8" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12872,21 +13008,20 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-stream": { @@ -12907,17 +13042,37 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "license": "MIT", "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/temp": { @@ -13295,6 +13450,12 @@ "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -13510,9 +13671,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -13709,7 +13868,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index d38c773..5243931 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "@samuelthomas2774/saltpack": "^0.4.0", "body-parser": "^1.20.2", "cli-table": "^0.3.11", "debug": "^4.3.4", @@ -50,6 +51,7 @@ "sharp": "^0.33.3", "splatnet3-types": "^0.2.20231119210145", "supports-color": "^9.4.0", + "tar": "^7.4.3", "tslib": "^2.6.2", "undici": "^6.15.0", "yargs": "^17.7.2" @@ -76,8 +78,8 @@ "@types/yargs": "^17.0.32", "electron": "^30.0.1", "electron-builder": "^24.13.3", - "mime-types": "^2.1.35", "i18next": "^22.4.6", + "mime-types": "^2.1.35", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^12.1.1", diff --git a/resources/common/remote-config.json b/resources/common/remote-config.json index b73f119..aae7bbe 100644 --- a/resources/common/remote-config.json +++ b/resources/common/remote-config.json @@ -1,5 +1,6 @@ { "require_version": [], + "log_encryption_key": "E2Sii_7drCzK-68RsEoArmopiAIlZD_6TMA2F_UAAU0", "coral": { "znca_version": "2.12.0" }, diff --git a/src/app/i18n/locale/de-de.ts b/src/app/i18n/locale/de-de.ts index fb39064..c9e4fd9 100644 --- a/src/app/i18n/locale/de-de.ts +++ b/src/app/i18n/locale/de-de.ts @@ -69,7 +69,7 @@ export const menus = { friend: { presence_online: 'Online', game_first_played: 'Zuerst gespielt: {{date, datetime}}', - + game_play_time_h: 'Spielzeit: $t(friend.hours, {"count": {{hours}}})', game_play_time_hm: 'Spielzeit: $t(friend.hours, {"count": {{hours}}}), $t(friend.minutes, {"count": {{minutes}}})', game_play_time_m: 'Spielzeit: $t(friend.minutes, {"count": {{minutes}}})', @@ -109,7 +109,7 @@ export const na_auth = { text: `Um Zugriff auf die API der Nintendo Switch Online App zu erhalten, muss nxapi einige Daten an Drittanbieter-APIs senden. Dieser Schritt wird benötigt, um Daten zu generieren, damit Nintendo denkt, dass du die echte Nintendo Switch Online App verwendest. Standardmäßig wird nxapi-znca-api.fancy.org.uk oder api.imink.app benutzt. Ein anderer Service kann ebenfalls benutzt werden, indem eine Umgebungsvariable gesetzt wird. Die standardmäßige API könnte sich jederzeit ohne Hinweis ändern, wenn du keinen spezifischen Service erzwingst. - + Die gesendeten Daten beinhalten: - Deine Nintendo Account ID diff --git a/src/app/i18n/locale/en-gb.ts b/src/app/i18n/locale/en-gb.ts index dd93912..395e5b4 100644 --- a/src/app/i18n/locale/en-gb.ts +++ b/src/app/i18n/locale/en-gb.ts @@ -14,6 +14,7 @@ export const app_menu = { learn_more: 'Learn More', learn_more_github: 'Learn More (GitHub)', search_issues: 'Search Issues', + export_logs: 'Export Logs', refresh: 'Refresh', }; diff --git a/src/app/main/app-menu.ts b/src/app/main/app-menu.ts index 22f68ad..c40d041 100644 --- a/src/app/main/app-menu.ts +++ b/src/app/main/app-menu.ts @@ -2,6 +2,7 @@ import { i18n } from 'i18next'; import { GITHUB_MIRROR_URL, GITLAB_URL, ISSUES_URL } from '../../common/constants.js'; import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'; import { App } from './index.js'; +import { createLogArchive } from './support.js'; let appinstance: App | null; @@ -59,6 +60,12 @@ function createAppMenuItems(i18n?: i18n) { await shell.openExternal(ISSUES_URL); }, }, + { + label: i18n?.t('app_menu:export_logs') ?? 'Export Logs', + click: () => { + createLogArchive(); + }, + }, ], }); diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts index b3d5323..f5e0634 100644 --- a/src/app/main/ipc.ts +++ b/src/app/main/ipc.ts @@ -205,7 +205,7 @@ function buildUserMenu(app: App, user: NintendoAccountUser, nso?: CurrentUser, m new MenuItem({label: t('discord_disable')!, click: () => app.menu?.setActiveDiscordPresenceUser(null)}), ] : monitor?.presence_user ? [ - new MenuItem({label: t('discord_enabled_for', {name: + new MenuItem({label: t('discord_enabled_for', {name: monitor.user?.friends.result.friends.find(f => f.nsaId === monitor.presence_user)?.name ?? monitor.presence_user})!, enabled: false}), diff --git a/src/app/main/support.ts b/src/app/main/support.ts new file mode 100644 index 0000000..fd5643b --- /dev/null +++ b/src/app/main/support.ts @@ -0,0 +1,88 @@ +import { Buffer } from 'node:buffer'; +import { createWriteStream, WriteStream } from 'node:fs'; +import { app, dialog, Notification, shell } from 'electron'; +import createDebug from '../../util/debug.js'; +import { generateEncryptedLogArchive } from '../../util/support.js'; +import { join } from 'node:path'; +import { showErrorDialog } from './util.js'; + +const debug = createDebug('app:main:support'); + +export async function createLogArchive() { + let start_notification: Notification | null = null; + + try { + const { default: config } = await import('../../common/remote-config.js'); + + if (!config.log_encryption_key) { + throw new Error('No log encryption key in remote configuration'); + } + + const default_name = 'nxapi-logs-' + + new Date().toISOString().replace(/[-:Z]/g, '').replace(/\.\d+/, '').replace(/T/, '-') + + '.tar.gz'; + + const result = await dialog.showSaveDialog({ + defaultPath: join(app.getPath('downloads'), default_name), + filters: [{name: 'Tape archive (encrypted)', extensions: ['tgz', 'tar.gz']}], + }); + + if (result.canceled) return; + + const out = await createOutputStream(result.filePath); + + debug('creating log archive'); + + start_notification = new Notification({ + title: 'Creating log archive', + }); + start_notification.show(); + + const key = Buffer.from(config.log_encryption_key, 'base64url'); + const [encrypt] = await generateEncryptedLogArchive(key); + + encrypt.pipe(out); + + await new Promise((rs, rj) => { + encrypt.on('end', rs); + encrypt.on('error', rj); + }); + + debug('done'); + + start_notification.close(); + + new Notification({ + title: 'Created log archive', + }).show(); + + shell.showItemInFolder(result.filePath); + } catch (err) { + start_notification?.close(); + + showErrorDialog({ + message: 'Error creating log archive', + error: err, + }); + } +} + +async function createOutputStream(path: string) { + return new Promise((rs, rj) => { + const out = createWriteStream(path); + + const onready = () => { + out.removeListener('ready', onready); + out.removeListener('error', onerror); + rs(out); + }; + const onerror = () => { + out.removeListener('ready', onready); + out.removeListener('error', onerror); + rs(out); + }; + + out.on('ready', onready); + out.on('error', onerror); + }); +} diff --git a/src/cli/util/commands.ts b/src/cli/util/commands.ts index 6857546..be00abb 100644 --- a/src/cli/util/commands.ts +++ b/src/cli/util/commands.ts @@ -7,3 +7,5 @@ export * as remoteConfig from './remote-config.js'; export * as storage from './storage.js'; export * as presenceEmbedRender from './presence-embed-render.js'; export * as presenceEmbedServer from './presence-embed-server.js'; +export * as logArchive from './log-archive.js'; +export * as decryptLogArchive from './decrypt-log-archive.js'; diff --git a/src/cli/util/decrypt-log-archive.ts b/src/cli/util/decrypt-log-archive.ts new file mode 100644 index 0000000..72b78d8 --- /dev/null +++ b/src/cli/util/decrypt-log-archive.ts @@ -0,0 +1,34 @@ +import { Buffer } from 'node:buffer'; +import { DecryptStream } from '@samuelthomas2774/saltpack'; +import tweetnacl from 'tweetnacl'; +import type { Arguments as ParentArguments } from './index.js'; +import createDebug from '../../util/debug.js'; +import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; + +const debug = createDebug('cli:util:decrypt-log-archive'); + +export const command = 'decrypt-log-archive'; +export const desc = null; + +export function builder(yargs: Argv) { + return yargs; +} + +type Arguments = YargsArguments>; + +export async function handler(argv: ArgumentsCamelCase) { + if (!process.env.NXAPI_SUPPORT_SECRET_KEY) { + throw new Error('Missing NXAPI_SUPPORT_SECRET_KEY environment variable'); + } + + const key = Buffer.from(process.env.NXAPI_SUPPORT_SECRET_KEY, 'base64url'); + const keypair = tweetnacl.box.keyPair.fromSecretKey(key); + + const decrypt = new DecryptStream(keypair); + + decrypt.pipe(process.stdout); + + debug('decrypting tar.gz to stdout'); + + process.stdin.pipe(decrypt); +} diff --git a/src/cli/util/log-archive.ts b/src/cli/util/log-archive.ts new file mode 100644 index 0000000..b6b9ea8 --- /dev/null +++ b/src/cli/util/log-archive.ts @@ -0,0 +1,70 @@ +import { Buffer } from 'node:buffer'; +import { createWriteStream, WriteStream } from 'node:fs'; +import type { Arguments as ParentArguments } from './index.js'; +import createDebug from '../../util/debug.js'; +import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; +import { generateEncryptedLogArchive } from '../../util/support.js'; + +const debug = createDebug('cli:util:log-archive'); + +export const command = 'log-archive [output]'; +export const desc = 'Create an encrypted log archive for support'; + +export function builder(yargs: Argv) { + return yargs.positional('output', { + describe: 'Output path', + type: 'string', + }); +} + +type Arguments = YargsArguments>; + +export async function handler(argv: ArgumentsCamelCase) { + const { default: config } = await import('../../common/remote-config.js'); + + if (!config.log_encryption_key) { + throw new Error('No log encryption key in remote configuration'); + } + + const out = await createOutputStream(argv.output); + + debug('creating log archive'); + + const key = Buffer.from(config.log_encryption_key, 'base64url'); + const [encrypt] = await generateEncryptedLogArchive(key); + + encrypt.pipe(out); + + encrypt.on('end', () => { + debug('done'); + }); +} + +async function createOutputStream(path?: string) { + if (!path && process.stdout.isTTY) { + console.error('No output path set but stdout is a TTY. Run `nxapi util log-archive -` to force output to a terminal.'); + process.exit(1); + } + + if (!path || path === '-') { + return process.stdout; + } + + return new Promise((rs, rj) => { + const out = createWriteStream(path); + + const onready = () => { + out.removeListener('ready', onready); + out.removeListener('error', onerror); + rs(out); + }; + const onerror = () => { + out.removeListener('ready', onready); + out.removeListener('error', onerror); + rs(out); + }; + + out.on('ready', onready); + out.on('error', onerror); + }); +} diff --git a/src/common/remote-config.ts b/src/common/remote-config.ts index 28a0be0..fb5bea0 100644 --- a/src/common/remote-config.ts +++ b/src/common/remote-config.ts @@ -251,6 +251,8 @@ export interface NxapiRemoteConfig { */ require_version: string[]; + log_encryption_key?: string; + // If null the API should not be used coral: CoralRemoteConfig | null; coral_auth: { diff --git a/src/util/support.ts b/src/util/support.ts new file mode 100644 index 0000000..8403e4d --- /dev/null +++ b/src/util/support.ts @@ -0,0 +1,119 @@ +import { resolve } from 'node:path'; +import * as os from 'node:os'; +import { EncryptStream } from '@samuelthomas2774/saltpack'; +import { Header, list, Pack, ReadEntry } from 'tar'; +import createDebug from './debug.js'; +import { dev, docker, git, paths, product, release, version } from './product.js'; +import { getUserAgent } from './useragent.js'; + +const debug = createDebug('nxapi:util:support'); + +export async function createLogArchive(log_path = paths.log) { + const tar = new Pack({ + gzip: true, + cwd: log_path, + preservePaths: true, + onWriteEntry: e => { + if (e.path === 'info.json') return; + if (e.path.startsWith(log_path)) { + e.path = e.path.substring(log_path.length + 1); + } + e.path = 'log/' + e.path; + }, + }); + + tar.on('error', err => { + debug('archive error', err); + }); + + const data = getSystemInfo(); + tar.add(createJsonFileEntry(data, 'info.json')); + + await addFiles(tar, log_path); + + tar.end(); + + return tar; +} + +async function addFiles(tar: Pack, file: string) { + if (file.charAt(0) === '@') { + await list({ + file: resolve(tar.cwd, file.slice(1)), + noResume: true, + onReadEntry: entry => { + tar.add(entry); + }, + }); + } else { + tar.add(file); + } +} + +function getSystemInfo() { + return { + version, + created_at: new Date(), + product: { + release, + docker, + git, + dev, + product, + user_agent: getUserAgent(), + }, + environment: { + execPath: process.execPath, + execArgv: process.execArgv, + argv: process.argv, + env: process.env, + paths, + }, + node: { + versions: process.versions, + features: process.features, + }, + system: { + platform: process.platform, + arch: process.arch, + uname: os.version(), + }, + }; +} + +function createJsonFileEntry(data: unknown, name: string, date = new Date()) { + debug('adding file', name, data); + + const buffer = Buffer.from(JSON.stringify(data, null, 4) + '\n', 'utf-8'); + + const header = new Header({ + path: name, + mode: 0o600, + uid: process.getuid?.() ?? 0, + gid: process.getgid?.() ?? 0, + ctime: date, + mtime: date, + size: buffer.length, + type: 'File', + }); + + const entry = new ReadEntry(header); + + entry.end(buffer); + + return entry; +} + +export async function generateEncryptedLogArchive(key: Uint8Array, log_path = paths.log) { + const encrypt = new EncryptStream(null, [key]); + + encrypt.on('error', err => { + debug('encrypt error', err); + }); + + const tar = await createLogArchive(); + + tar.pipe(encrypt); + + return [encrypt, tar] as const; +}