Add command/menu item to export log files

This commit is contained in:
Samuel Elliott 2025-04-02 00:34:00 +01:00
parent 83456275fb
commit bde495fcce
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
13 changed files with 536 additions and 52 deletions

254
package-lock.json generated
View File

@ -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": {

View File

@ -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",

View File

@ -1,5 +1,6 @@
{
"require_version": [],
"log_encryption_key": "E2Sii_7drCzK-68RsEoArmopiAIlZD_6TMA2F_UAAU0",
"coral": {
"znca_version": "2.12.0"
},

View File

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

View File

@ -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',
};

View File

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

View File

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

88
src/app/main/support.ts Normal file
View File

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

View File

@ -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';

View File

@ -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<ParentArguments>) {
return yargs;
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
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);
}

View File

@ -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<ParentArguments>) {
return yargs.positional('output', {
describe: 'Output path',
type: 'string',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
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<WriteStream>((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);
});
}

View File

@ -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: {

119
src/util/support.ts Normal file
View File

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