mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-17 18:46:42 -05:00
Merge branch 'i18n' into pull/63
# Conflicts: # src/app/i18n/index.ts
This commit is contained in:
commit
b306d1d786
33
README.md
33
README.md
|
|
@ -212,6 +212,18 @@ DEBUG=nxapi:api:* nxapi ...
|
|||
DEBUG=* nxapi ...
|
||||
```
|
||||
|
||||
By default all nxapi logs will be written to a platform-specific location:
|
||||
|
||||
Platform | Log path
|
||||
----------------|----------------
|
||||
macOS | `Library/Logs/nxapi-nodejs`
|
||||
Windows | `%localappdata%\nxapi-nodejs\Log`
|
||||
Linux | `$XDG_STATE_HOME/nxapi-nodejs` or `.local/state/nxapi-nodejs`
|
||||
|
||||
This only applies to the command line and Electron app and can be disabled by setting `NXAPI_DEBUG_FILE` to `0`. Each process writes to a new file. nxapi will automatically delete log files older than 14 days.
|
||||
|
||||
nxapi logs may contain sensitive information such as Nintendo Account access tokens.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
Some options can be set using environment variables. These can be stored in a `.env` file in the data location. Environment variables will be read from the `.env` file in the default location, then the `.env` file in `NXAPI_DATA_PATH` location. `.env` files will not be read from the location set in the `--data-path` option.
|
||||
|
|
@ -234,6 +246,7 @@ Environment variable | Description
|
|||
`NXAPI_SPLATNET3_UPGRADE_QUERIES` | Sets when the SplatNet 3 client is allowed to upgrade persisted query IDs to newer versions. If `0` queries are never upgraded (not recommended). If `1` queries are upgraded if they do not contain potentially breaking changes (not recommended, as like `0` this allows older queries to be sent to the API). If `2` queries are upgraded, requests that would include breaking changes are rejected. If `3` all queries are upgraded, even if they contain potentially breaking changes (default).
|
||||
`NXAPI_SPLATNET3_STRICT` | Disables strict handling of errors from the SplatNet 3 GraphQL API if set to `0`. If set to `1` (default) requests will be rejected if the response includes any errors, even if the response includes a result.
|
||||
`DEBUG` | Used by the [debug](https://github.com/debug-js/debug) package. Sets which modules should have debug logging enabled. See [debug logs](#debug-logs).
|
||||
`NXAPI_DEBUG_FILE` | Disables writing debug logs to a file if set to `0`.
|
||||
|
||||
Other environment variables may also be used by Node.js, Electron or other packages nxapi depends on.
|
||||
|
||||
|
|
@ -254,13 +267,19 @@ When using nxapi as a TypeScript/JavaScript library, the `addUserAgent` function
|
|||
import { addUserAgent } from 'nxapi';
|
||||
|
||||
addUserAgent('your-script/1.0.0 (+https://github.com/...)');
|
||||
```
|
||||
|
||||
// This could also be read from a package.json file
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
import { readFile } from 'node:fs/promises':
|
||||
const pkg = JSON.parse(await readFile(resolve(fileURLToPath(import.meta.url), '..', 'package.json'), 'utf-8'));
|
||||
addUserAgent(pkg.name + '/' + pkg.version + ' (+' + pkg.repository.url + ')');
|
||||
The `addUserAgentFromPackageJson` function can be used to add data from a package.json file.
|
||||
|
||||
```ts
|
||||
import { addUserAgentFromPackageJson } from 'nxapi';
|
||||
|
||||
await addUserAgentFromPackageJson(new URL('../package.json', import.meta.url));
|
||||
await addUserAgentFromPackageJson(path.resolve(fileURLToString(import.meta.url), '..', 'package.json'));
|
||||
// adds "test-package/0.1.0 (+https://github.com/ghost/example.git)"
|
||||
|
||||
await addUserAgentFromPackageJson(new URL('../package.json', import.meta.url), 'additional information');
|
||||
// adds "test-package/0.1.0 (+https://github.com/ghost/example.git; additional information)"
|
||||
```
|
||||
|
||||
### Usage as a TypeScript/JavaScript library
|
||||
|
|
@ -296,7 +315,7 @@ The reason Nintendo added this is probably to try and stop people automating acc
|
|||
- Nintendo Switch Online app API docs
|
||||
- https://github.com/ZekeSnider/NintendoSwitchRESTAPI
|
||||
- https://dev.to/mathewthe2/intro-to-nintendo-switch-rest-api-2cm7
|
||||
- nxapi includes TypeScript definitions of all API resources and JSON Web Token payloads at [src/api](src/api)
|
||||
- nxapi includes TypeScript definitions for all API resources and JSON Web Token payloads at [src/api](src/api)
|
||||
- Coral client authentication (`f` parameter)
|
||||
- https://github.com/samuelthomas2774/nxapi/discussions/10
|
||||
- ~~https://github.com/frozenpandaman/splatnet2statink/wiki/api-docs - splatnet2statink and flapg API docs~~
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import createDebug from 'debug';
|
||||
|
||||
createDebug.log = console.warn.bind(console);
|
||||
|
||||
import('../dist/cli.js').then(cli => cli.main.call(null));
|
||||
import('../dist/cli-entry.js');
|
||||
|
|
|
|||
|
|
@ -40,18 +40,18 @@ services:
|
|||
|
||||
presence-server:
|
||||
build: .
|
||||
command: presence-server --listen \[::]:80 --splatnet3
|
||||
command: presence-server --listen \[::]:80 --splatnet3 --splatnet3-fest-votes
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- presence-server
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nxapi-presence.entrypoints: websecure
|
||||
traefik.http.routers.nxapi-presence.rule: Host(`${TRAEFIK_HOST:-nxapi.ta.fancy.org.uk}`) && (Path(`/api/presence`) || PathPrefix(`/api/presence/`))
|
||||
traefik.http.routers.nxapi-presence.rule: Host(`${TRAEFIK_HOST:-nxapi.ta.fancy.org.uk}`) && (Path(`/api/presence`) || PathPrefix(`/api/presence/`) || PathPrefix(`/api/splatnet3/resources/`))
|
||||
traefik.http.routers.nxapi-presence.tls: true
|
||||
traefik.http.services.nxapi-presence.loadbalancer.server.port: 80
|
||||
environment:
|
||||
DEBUG: '*,-express:*'
|
||||
DEBUG: '*,-express:*,-send'
|
||||
ZNC_PROXY_URL: http://znc-proxy/api/znc
|
||||
NXAPI_PRESENCE_SERVER_USER: ${NXAPI_PRESENCE_SERVER_USER:-}
|
||||
NXAPI_PRESENCE_SERVER_SPLATNET3_PROXY_URL: http://presence-splatnet3-proxy/api/splatnet3-presence
|
||||
|
|
@ -60,7 +60,7 @@ services:
|
|||
|
||||
presence-splatnet3-proxy:
|
||||
build: .
|
||||
command: presence-server --listen \[::]:80 --splatnet3 --splatnet3-proxy
|
||||
command: presence-server --listen \[::]:80 --splatnet3 --splatnet3-proxy --splatnet3-record-fest-votes
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- presence-server
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ try {
|
|||
|
||||
This function is used to set the user agent string to use for non-Nintendo API requests. Any project using nxapi (including as a dependency of another project) must call this function with an appropriate user agent string segment. See [user agent strings](../../README.md#user-agent-strings).
|
||||
|
||||
#### `addUserAgentFromPackageJson`
|
||||
|
||||
This function is used to set the user agent string to use for non-Nintendo API requests using data from a package.json file. A string, URL object or the package.json data can be provided, as well as optional additional data. If a string/URL is provided this will return a Promise that will be resolved once the user agent is updated. See [user agent strings](../../README.md#user-agent-strings).
|
||||
|
||||
#### `version`
|
||||
|
||||
nxapi's version number.
|
||||
|
|
|
|||
52
package-lock.json
generated
52
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "nxapi",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nxapi",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.1",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
"node-notifier": "^10.0.1",
|
||||
"node-persist": "^3.1.0",
|
||||
"read": "^1.0.7",
|
||||
"splatnet3-types": "^0.2.20230125112953",
|
||||
"splatnet3-types": "^0.2.20230601143335",
|
||||
"supports-color": "^8.1.1",
|
||||
"tslib": "^2.4.1",
|
||||
"uuid": "^8.3.2",
|
||||
|
|
@ -39,7 +39,6 @@
|
|||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^14.1.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.5.0",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cli-table": "^0.3.1",
|
||||
"@types/debug": "^4.1.7",
|
||||
|
|
@ -362,29 +361,6 @@
|
|||
"rollup": "^1.20.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-typescript": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz",
|
||||
"integrity": "sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^3.1.0",
|
||||
"resolve": "^1.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.14.0",
|
||||
"tslib": "*",
|
||||
"typescript": ">=3.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tslib": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
|
||||
|
|
@ -4139,9 +4115,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/splatnet3-types": {
|
||||
"version": "0.2.20230125112953",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.2.20230125112953.tgz",
|
||||
"integrity": "sha512-ZoyYHjRlq0ZIg8ZWVnQ6MYWSjEv7nVMgSImyDqho9taZS31yxrSj/xZer6teTSrCTGIabYDvx+MPEhAHg8Jbpw=="
|
||||
"version": "0.2.20230601143335",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.2.20230601143335.tgz",
|
||||
"integrity": "sha512-gZO2DUohuPhhPhwJrEcOR07fYgAvA42ZuXEvaw3x7c5LQzjNzgjkwOQNnpHAMZcE6lIfT2L45v8cjfyu8VcBgA=="
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
|
|
@ -4942,16 +4918,6 @@
|
|||
"magic-string": "^0.25.7"
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-typescript": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz",
|
||||
"integrity": "sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/pluginutils": "^3.1.0",
|
||||
"resolve": "^1.17.0"
|
||||
}
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
|
||||
|
|
@ -7895,9 +7861,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"splatnet3-types": {
|
||||
"version": "0.2.20230125112953",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.2.20230125112953.tgz",
|
||||
"integrity": "sha512-ZoyYHjRlq0ZIg8ZWVnQ6MYWSjEv7nVMgSImyDqho9taZS31yxrSj/xZer6teTSrCTGIabYDvx+MPEhAHg8Jbpw=="
|
||||
"version": "0.2.20230601143335",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.2.20230601143335.tgz",
|
||||
"integrity": "sha512-gZO2DUohuPhhPhwJrEcOR07fYgAvA42ZuXEvaw3x7c5LQzjNzgjkwOQNnpHAMZcE6lIfT2L45v8cjfyu8VcBgA=="
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "nxapi",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Nintendo Switch app APIs",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Samuel Elliott <samuel+nxapi@fancy.org.uk>",
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
"node-notifier": "^10.0.1",
|
||||
"node-persist": "^3.1.0",
|
||||
"read": "^1.0.7",
|
||||
"splatnet3-types": "^0.2.20230125112953",
|
||||
"splatnet3-types": "^0.2.20230601143335",
|
||||
"supports-color": "^8.1.1",
|
||||
"tslib": "^2.4.1",
|
||||
"uuid": "^8.3.2",
|
||||
|
|
@ -62,7 +62,6 @@
|
|||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^14.1.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.5.0",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cli-table": "^0.3.1",
|
||||
"@types/debug": "^4.1.7",
|
||||
|
|
@ -101,7 +100,8 @@
|
|||
"!dist/app/package",
|
||||
"!**/node_modules/**/*",
|
||||
"resources/app",
|
||||
"resources/common"
|
||||
"resources/common",
|
||||
"!resources/common/remote-config.json"
|
||||
],
|
||||
"asar": false,
|
||||
"extraMetadata": {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ const git = (...args) => execFile('git', args, options).then(({stdout}) => stdou
|
|||
const pkg = JSON.parse(await fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8'));
|
||||
|
||||
const [revision, branch_str, changed_files_str, tags_str, commit_count_str] = await Promise.all([
|
||||
git('rev-parse', 'HEAD'),
|
||||
git('rev-parse', '--abbrev-ref', 'HEAD'),
|
||||
process.env.CI_COMMIT_SHA || git('rev-parse', 'HEAD'),
|
||||
process.env.CI_COMMIT_BRANCH || git('rev-parse', '--abbrev-ref', 'HEAD'),
|
||||
git('diff', '--name-only', 'HEAD'),
|
||||
git('log', '--tags', '--no-walk', '--pretty=%D'),
|
||||
git('rev-list', '--count', 'HEAD'),
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ const git = (...args) => execFile('git', args, options).then(({stdout}) => stdou
|
|||
const pkg = JSON.parse(await fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8'));
|
||||
|
||||
const [revision, branch, changed_files] = await Promise.all([
|
||||
git('rev-parse', 'HEAD'),
|
||||
git('rev-parse', '--abbrev-ref', 'HEAD'),
|
||||
process.env.CI_COMMIT_SHA || git('rev-parse', 'HEAD'),
|
||||
process.env.CI_COMMIT_BRANCH || git('rev-parse', '--abbrev-ref', 'HEAD'),
|
||||
git('diff', '--name-only', 'HEAD'),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"require_version": [],
|
||||
"coral": {
|
||||
"znca_version": "2.4.0"
|
||||
"znca_version": "2.5.1"
|
||||
},
|
||||
"coral_auth": {
|
||||
"default": "imink",
|
||||
|
|
@ -10,15 +10,15 @@
|
|||
"imink": {}
|
||||
},
|
||||
"moon": {
|
||||
"znma_version": "1.17.0",
|
||||
"znma_build": "261"
|
||||
"znma_version": "1.18.0",
|
||||
"znma_build": "275"
|
||||
},
|
||||
"coral_gws_nooklink": {
|
||||
"blanco_version": "2.1.1"
|
||||
},
|
||||
"coral_gws_splatnet3": {
|
||||
"app_ver": "2.0.0-bd36a652",
|
||||
"version": "2.0.0",
|
||||
"revision": "bd36a652913aa26b132b84df921e07b20f4a414d"
|
||||
"app_ver": "4.0.0-e2ee936d",
|
||||
"version": "4.0.0",
|
||||
"revision": "e2ee936dbecad1fd8582c2a35c2603c63767263f"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,9 @@
|
|||
|
||||
mkdir -p /data/android
|
||||
|
||||
# Logs will be captured by Docker if enabled
|
||||
# This is set here so that running another process with `docker exec` (which
|
||||
# doesn't capture logs) will still write to a file by default
|
||||
export NXAPI_DEBUG_FILE=0
|
||||
|
||||
exec /app/bin/nxapi.js --data-path /data "$@"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import * as path from 'path';
|
|||
import { fileURLToPath } from 'url';
|
||||
import * as fs from 'fs';
|
||||
import * as child_process from 'child_process';
|
||||
import { Module } from 'module';
|
||||
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import alias from '@rollup/plugin-alias';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
|
|
@ -18,6 +18,14 @@ const default_remote_config =
|
|||
JSON.parse(fs.readFileSync(path.join(dir, 'resources', 'common', 'remote-config.json'), 'utf-8'));
|
||||
|
||||
const git = (() => {
|
||||
if (process.env.GITLAB_CI && process.env.CI_COMMIT_SHA) {
|
||||
return {
|
||||
revision: process.env.CI_COMMIT_SHA,
|
||||
branch: process.env.CI_COMMIT_BRANCH || null,
|
||||
changed_files: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
fs.statSync(path.join(dir, '.git'));
|
||||
} catch (err) {
|
||||
|
|
@ -34,7 +42,7 @@ const git = (() => {
|
|||
branch: branch && branch !== 'HEAD' ? branch : null,
|
||||
changed_files: changed_files.length ? changed_files.split('\n') : [],
|
||||
};
|
||||
})();;
|
||||
})();
|
||||
|
||||
// If CI_COMMIT_TAG is set this is a tagged version for release
|
||||
const release = process.env.NODE_ENV === 'production' ? process.env.CI_COMMIT_TAG || null : null;
|
||||
|
|
@ -43,7 +51,7 @@ const release = process.env.NODE_ENV === 'production' ? process.env.CI_COMMIT_TA
|
|||
* @type {import('@rollup/plugin-replace').RollupReplaceOptions}
|
||||
*/
|
||||
const replace_options = {
|
||||
include: ['src/util/product.ts'],
|
||||
include: ['dist/util/product.js'],
|
||||
values: {
|
||||
'globalThis.__NXAPI_BUNDLE_PKG__': JSON.stringify(pkg),
|
||||
'globalThis.__NXAPI_BUNDLE_GIT__': JSON.stringify(git),
|
||||
|
|
@ -58,20 +66,21 @@ const replace_options = {
|
|||
* @type {import('rollup').RollupOptions['watch']}
|
||||
*/
|
||||
const watch = {
|
||||
include: 'src/**',
|
||||
include: 'dist/**',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('rollup').RollupOptions}
|
||||
*/
|
||||
const main = {
|
||||
input: ['src/cli-entry.ts', 'src/app/main/index.ts'],
|
||||
input: ['dist/cli-entry.js', 'dist/app/app-init.js', 'dist/app/main/index.js'],
|
||||
output: {
|
||||
dir: 'dist/bundle',
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
entryFileNames: chunk => {
|
||||
if (chunk.name === 'cli-entry') return 'cli-bundle.js';
|
||||
if (chunk.name === 'app-init') return 'app-init-bundle.js';
|
||||
if (chunk.name === 'index') return 'app-main-bundle.js';
|
||||
return 'entry-' + chunk.name + '.js';
|
||||
},
|
||||
|
|
@ -79,19 +88,17 @@ const main = {
|
|||
},
|
||||
plugins: [
|
||||
replace(replace_options),
|
||||
typescript({
|
||||
outDir: 'dist/bundle/ts',
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
// events and stream modify module.exports
|
||||
requireReturnsDefault: 'preferred',
|
||||
}),
|
||||
json(),
|
||||
alias({
|
||||
entries: [
|
||||
...Module.builtinModules.map(m => ({find: m, replacement: 'node:' + m})),
|
||||
],
|
||||
}),
|
||||
nodeResolve({
|
||||
exportConditions: ['node'],
|
||||
browser: false,
|
||||
|
|
@ -110,29 +117,27 @@ const main = {
|
|||
* @type {import('rollup').RollupOptions}
|
||||
*/
|
||||
const app_entry = {
|
||||
input: 'src/app/app-entry.cts',
|
||||
input: 'dist/app/app-entry.cjs',
|
||||
output: {
|
||||
file: 'dist/bundle/app-entry.cjs',
|
||||
format: 'iife',
|
||||
inlineDynamicImports: true,
|
||||
sourcemap: true,
|
||||
globals: {
|
||||
'electron': 'require("electron")',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
replace(replace_options),
|
||||
replace({
|
||||
include: ['src/app/app-entry.cts'],
|
||||
include: ['dist/app/app-entry.cjs'],
|
||||
values: {
|
||||
'__NXAPI_BUNDLE_APP_MAIN__': JSON.stringify('./app-main-bundle.js'),
|
||||
'__NXAPI_BUNDLE_APP_INIT__': JSON.stringify('./app-init-bundle.js'),
|
||||
},
|
||||
preventAssignment: true,
|
||||
}),
|
||||
typescript({
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
// events and stream modify module.exports
|
||||
requireReturnsDefault: 'preferred',
|
||||
|
|
@ -146,7 +151,10 @@ const app_entry = {
|
|||
],
|
||||
external: [
|
||||
'electron',
|
||||
path.resolve(__dirname, 'src/app/app-main-bundle.js'),
|
||||
path.resolve(__dirname, 'dist/app/app-main-bundle.js'),
|
||||
path.resolve(__dirname, 'dist/app/app-init-bundle.js'),
|
||||
path.resolve(__dirname, 'dist/app/app-init.js'),
|
||||
path.resolve(__dirname, 'dist/app/main/index.js'),
|
||||
],
|
||||
watch,
|
||||
};
|
||||
|
|
@ -155,7 +163,7 @@ const app_entry = {
|
|||
* @type {import('rollup').RollupOptions}
|
||||
*/
|
||||
const app_preload = {
|
||||
input: 'src/app/preload/index.ts',
|
||||
input: 'dist/app/preload/index.js',
|
||||
output: {
|
||||
file: 'dist/app/bundle/preload.cjs',
|
||||
format: 'cjs',
|
||||
|
|
@ -163,13 +171,7 @@ const app_preload = {
|
|||
},
|
||||
plugins: [
|
||||
replace(replace_options),
|
||||
typescript({
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
}),
|
||||
nodeResolve({
|
||||
|
|
@ -187,20 +189,14 @@ const app_preload = {
|
|||
* @type {import('rollup').RollupOptions}
|
||||
*/
|
||||
const app_preload_webservice = {
|
||||
input: 'src/app/preload-webservice/index.ts',
|
||||
input: 'dist/app/preload-webservice/index.js',
|
||||
output: {
|
||||
file: 'dist/app/bundle/preload-webservice.cjs',
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [
|
||||
replace(replace_options),
|
||||
typescript({
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
}),
|
||||
nodeResolve({
|
||||
|
|
@ -218,7 +214,7 @@ const app_preload_webservice = {
|
|||
* @type {import('rollup').RollupOptions}
|
||||
*/
|
||||
const app_browser = {
|
||||
input: 'src/app/browser/index.ts',
|
||||
input: 'dist/app/browser/index.js',
|
||||
output: {
|
||||
dir: 'dist/app/bundle',
|
||||
format: 'es',
|
||||
|
|
@ -238,14 +234,7 @@ const app_browser = {
|
|||
title: 'nxapi',
|
||||
}),
|
||||
replace(replace_options),
|
||||
typescript({
|
||||
outDir: 'dist/app/bundle/ts',
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
}),
|
||||
nodePolyfill(),
|
||||
|
|
@ -255,6 +244,7 @@ const app_browser = {
|
|||
// used when resolving react-native-web. For some reason this causes both versions
|
||||
// to be included in the bundle, so here we explicitly use the CommonJS build.
|
||||
{find: 'react-native', replacement: path.resolve(__dirname, 'node_modules', 'react-native-web', 'dist', 'cjs', 'index.js')},
|
||||
{find: 'react-native-web', replacement: path.resolve(__dirname, 'node_modules', 'react-native-web', 'dist', 'cjs', 'index.js')},
|
||||
|
||||
// rollup-plugin-polyfill-node doesn't support node: module identifiers
|
||||
{find: /^node:(.+)/, replacement: '$1'},
|
||||
|
|
@ -268,10 +258,12 @@ const app_browser = {
|
|||
watch,
|
||||
};
|
||||
|
||||
const skip = process.env.BUNDLE_SKIP?.split(',') ?? [];
|
||||
|
||||
export default [
|
||||
main,
|
||||
app_entry,
|
||||
app_preload,
|
||||
app_preload_webservice,
|
||||
app_browser,
|
||||
];
|
||||
!skip?.includes('main') && main,
|
||||
!skip?.includes('app-entry') && app_entry,
|
||||
!skip?.includes('app-preload') && app_preload,
|
||||
!skip?.includes('app-preload-webservice') && app_preload_webservice,
|
||||
!skip?.includes('app-browser') && app_browser,
|
||||
].filter(c => c);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import fetch, { Response } from 'node-fetch';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import createDebug from 'debug';
|
||||
import { f, FResult, HashMethod } from './f.js';
|
||||
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl, AccountTokenParameter, AccountLoginParameter, WebServiceTokenParameter } from './coral-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse, ResponseSymbol } from './util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { JwtPayload } from '../util/jwt.js';
|
||||
import { getAdditionalUserAgents } from '../util/useragent.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
|
@ -41,13 +41,17 @@ export interface ResultData<T> {
|
|||
}
|
||||
|
||||
export default class CoralApi {
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<CoralAuthData | void>) | null = null;
|
||||
onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise<CoralAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
/** @internal */
|
||||
_token_expired = false;
|
||||
|
||||
protected constructor(
|
||||
public token: string,
|
||||
public useragent: string | null = getAdditionalUserAgents(),
|
||||
public coral_user_id: string,
|
||||
public na_id: string,
|
||||
readonly znca_version = ZNCA_VERSION,
|
||||
readonly znca_useragent = ZNCA_USER_AGENT,
|
||||
) {}
|
||||
|
|
@ -55,8 +59,18 @@ export default class CoralApi {
|
|||
async fetch<T = unknown>(
|
||||
url: string, method = 'GET', body?: string, headers?: object,
|
||||
/** @internal */ _autoRenewToken = true,
|
||||
/** @internal */ _attempt = 0
|
||||
/** @internal */ _attempt = 0,
|
||||
): Promise<Result<T>> {
|
||||
if (this._token_expired && _autoRenewToken && !this._renewToken) {
|
||||
if (!this.onTokenExpired || _attempt) throw new Error('Token expired');
|
||||
|
||||
this._renewToken = this.onTokenExpired.call(null).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (this._renewToken && _autoRenewToken) {
|
||||
await this._renewToken;
|
||||
}
|
||||
|
|
@ -84,6 +98,7 @@ export default class CoralApi {
|
|||
const data = await response.json() as CoralResponse<T>;
|
||||
|
||||
if (data.status === CoralStatus.TOKEN_EXPIRED && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
this._token_expired = true;
|
||||
// _renewToken will be awaited when calling fetch
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
|
|
@ -210,7 +225,12 @@ export default class CoralApi {
|
|||
async getWebServiceToken(id: number, /** @internal */ _attempt = 0): Promise<Result<WebServiceToken>> {
|
||||
await this._renewToken;
|
||||
|
||||
const data = await f(this.token, HashMethod.WEB_SERVICE, this.useragent ?? getAdditionalUserAgents());
|
||||
const data = await f(this.token, HashMethod.WEB_SERVICE, {
|
||||
platform: ZNCA_PLATFORM,
|
||||
version: this.znca_version,
|
||||
useragent: this.useragent ?? getAdditionalUserAgents(),
|
||||
user: {na_id: this.na_id, coral_user_id: this.coral_user_id},
|
||||
});
|
||||
|
||||
const req: WebServiceTokenParameter = {
|
||||
id,
|
||||
|
|
@ -242,8 +262,12 @@ export default class CoralApi {
|
|||
// Nintendo Account token
|
||||
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
|
||||
|
||||
const fdata = await f(nintendoAccountToken.id_token, HashMethod.CORAL,
|
||||
this.useragent ?? getAdditionalUserAgents());
|
||||
const fdata = await f(nintendoAccountToken.id_token, HashMethod.CORAL, {
|
||||
platform: ZNCA_PLATFORM,
|
||||
version: this.znca_version,
|
||||
useragent: this.useragent ?? getAdditionalUserAgents(),
|
||||
user: {na_id: user.id, coral_user_id: this.coral_user_id},
|
||||
});
|
||||
|
||||
const req: AccountTokenParameter = {
|
||||
naBirthday: user.birthday,
|
||||
|
|
@ -273,6 +297,9 @@ export default class CoralApi {
|
|||
/** @private */
|
||||
setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
|
||||
this.token = data.credential.accessToken;
|
||||
this.coral_user_id = '' + data.nsoAccount.user.id;
|
||||
if ('user' in data) this.na_id = data.user.id;
|
||||
this._token_expired = false;
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string, useragent = getAdditionalUserAgents()) {
|
||||
|
|
@ -292,6 +319,8 @@ export default class CoralApi {
|
|||
return new this(
|
||||
data.credential.accessToken,
|
||||
useragent,
|
||||
'' + data.nsoAccount.user.id,
|
||||
data.user.id,
|
||||
data.znca_version,
|
||||
data.znca_useragent,
|
||||
);
|
||||
|
|
@ -313,14 +342,19 @@ export default class CoralApi {
|
|||
static async loginWithNintendoAccountToken(
|
||||
nintendoAccountToken: NintendoAccountToken,
|
||||
user: NintendoAccountUser,
|
||||
useragent = getAdditionalUserAgents()
|
||||
useragent = getAdditionalUserAgents(),
|
||||
) {
|
||||
const { default: { coral: config } } = await import('../common/remote-config.js');
|
||||
|
||||
if (!config) throw new Error('Remote configuration prevents Coral authentication');
|
||||
const znca_useragent = `com.nintendo.znca/${config.znca_version}(${ZNCA_PLATFORM}/${ZNCA_PLATFORM_VERSION})`;
|
||||
|
||||
const fdata = await f(nintendoAccountToken.id_token, HashMethod.CORAL, useragent);
|
||||
const fdata = await f(nintendoAccountToken.id_token, HashMethod.CORAL, {
|
||||
platform: ZNCA_PLATFORM,
|
||||
version: config.znca_version,
|
||||
useragent,
|
||||
user: {na_id: user.id},
|
||||
});
|
||||
|
||||
debug('Getting Nintendo Switch Online app token');
|
||||
|
||||
|
|
|
|||
141
src/api/f.ts
141
src/api/f.ts
|
|
@ -1,12 +1,11 @@
|
|||
import process from 'node:process';
|
||||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import fetch, { Headers } from 'node-fetch';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import { defineResponse, ErrorResponse } from './util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
import { getUserAgent } from '../util/useragent.js';
|
||||
|
||||
const debugS2s = createDebug('nxapi:api:s2s');
|
||||
const debugFlapg = createDebug('nxapi:api:flapg');
|
||||
const debugImink = createDebug('nxapi:api:imink');
|
||||
const debugZncaApi = createDebug('nxapi:api:znca-api');
|
||||
|
|
@ -16,7 +15,10 @@ export abstract class ZncaApi {
|
|||
public useragent?: string
|
||||
) {}
|
||||
|
||||
abstract genf(token: string, hash_method: HashMethod): Promise<FResult>;
|
||||
abstract genf(
|
||||
token: string, hash_method: HashMethod,
|
||||
user?: {na_id: string; coral_user_id?: string;},
|
||||
): Promise<FResult>;
|
||||
}
|
||||
|
||||
export enum HashMethod {
|
||||
|
|
@ -28,49 +30,6 @@ export enum HashMethod {
|
|||
// flapg
|
||||
//
|
||||
|
||||
/** @deprecated The flapg API no longer requires client authentication */
|
||||
export async function getLoginHash(token: string, timestamp: string | number, useragent?: string) {
|
||||
const { default: { coral_auth: { splatnet2statink: config } } } = await import('../common/remote-config.js');
|
||||
if (!config) throw new Error('Remote configuration prevents splatnet2statink API use');
|
||||
|
||||
debugS2s('Getting login hash');
|
||||
|
||||
const [signal, cancel] = timeoutSignal();
|
||||
const response = await fetch('https://elifessler.com/s2s/api/gen2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': getUserAgent(useragent),
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
naIdToken: token,
|
||||
timestamp: '' + timestamp,
|
||||
}).toString(),
|
||||
signal,
|
||||
}).finally(cancel);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorResponse('[s2s] Non-200 status code', response, await response.text());
|
||||
}
|
||||
|
||||
const data = await response.json() as LoginHashApiResponse | LoginHashApiError;
|
||||
|
||||
if ('error' in data) {
|
||||
throw new ErrorResponse('[s2s] ' + data.error, response, data);
|
||||
}
|
||||
|
||||
debugS2s('Got login hash "%s"', data.hash, data);
|
||||
|
||||
return data.hash;
|
||||
}
|
||||
|
||||
export interface LoginHashApiResponse {
|
||||
hash: string;
|
||||
}
|
||||
export interface LoginHashApiError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export async function flapg(
|
||||
hash_method: HashMethod, token: string,
|
||||
timestamp?: string | number, request_id?: string,
|
||||
|
|
@ -131,11 +90,6 @@ export type FlapgApiResponse = IminkFResponse;
|
|||
export type FlapgApiError = IminkFError;
|
||||
|
||||
export class ZncaApiFlapg extends ZncaApi {
|
||||
/** @deprecated */
|
||||
async getLoginHash(id_token: string, timestamp: string) {
|
||||
return getLoginHash(id_token, timestamp, this.useragent);
|
||||
}
|
||||
|
||||
async genf(token: string, hash_method: HashMethod) {
|
||||
const request_id = uuidgen();
|
||||
|
||||
|
|
@ -158,7 +112,8 @@ export class ZncaApiFlapg extends ZncaApi {
|
|||
export async function iminkf(
|
||||
hash_method: HashMethod, token: string,
|
||||
timestamp?: number, request_id?: string,
|
||||
useragent?: string
|
||||
user?: {na_id: string; coral_user_id?: string;},
|
||||
useragent?: string,
|
||||
) {
|
||||
const { default: { coral_auth: { imink: config } } } = await import('../common/remote-config.js');
|
||||
if (!config) throw new Error('Remote configuration prevents imink API use');
|
||||
|
|
@ -172,6 +127,7 @@ export async function iminkf(
|
|||
token,
|
||||
timestamp: typeof timestamp === 'number' ? '' + timestamp : undefined,
|
||||
request_id,
|
||||
...user,
|
||||
};
|
||||
|
||||
const [signal, cancel] = timeoutSignal();
|
||||
|
|
@ -217,16 +173,17 @@ export interface IminkFError {
|
|||
}
|
||||
|
||||
export class ZncaApiImink extends ZncaApi {
|
||||
async genf(token: string, hash_method: HashMethod) {
|
||||
async genf(token: string, hash_method: HashMethod, user?: {na_id: string; coral_user_id?: string;}) {
|
||||
const request_id = uuidgen();
|
||||
|
||||
const result = await iminkf(hash_method, token, undefined, request_id, this.useragent);
|
||||
const result = await iminkf(hash_method, token, undefined, request_id, user, this.useragent);
|
||||
|
||||
return {
|
||||
provider: 'imink' as const,
|
||||
hash_method, token, request_id,
|
||||
timestamp: result.timestamp,
|
||||
f: result.f,
|
||||
user,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
|
@ -239,10 +196,13 @@ export class ZncaApiImink extends ZncaApi {
|
|||
export async function genf(
|
||||
url: string, hash_method: HashMethod,
|
||||
token: string, timestamp?: number, request_id?: string,
|
||||
useragent?: string
|
||||
user?: {na_id: string; coral_user_id?: string;},
|
||||
app?: {platform?: string; version?: string;},
|
||||
useragent?: string,
|
||||
) {
|
||||
debugZncaApi('Getting f parameter', {
|
||||
url, hash_method, token, timestamp, request_id,
|
||||
url, hash_method, token, timestamp, request_id, user,
|
||||
znca_platform: app?.platform, znca_version: app?.version,
|
||||
});
|
||||
|
||||
const req: AndroidZncaFRequest = {
|
||||
|
|
@ -250,15 +210,20 @@ export async function genf(
|
|||
token,
|
||||
timestamp,
|
||||
request_id,
|
||||
...user,
|
||||
};
|
||||
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': getUserAgent(useragent),
|
||||
});
|
||||
if (app?.platform) headers.append('X-znca-Platform', app.platform);
|
||||
if (app?.version) headers.append('X-znca-Version', app.version);
|
||||
|
||||
const [signal, cancel] = timeoutSignal();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': getUserAgent(useragent),
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(req),
|
||||
signal,
|
||||
}).finally(cancel);
|
||||
|
|
@ -289,21 +254,27 @@ export interface AndroidZncaFResponse {
|
|||
f: string;
|
||||
timestamp?: number;
|
||||
request_id?: string;
|
||||
|
||||
warnings?: {error: string; error_message: string}[];
|
||||
}
|
||||
export interface AndroidZncaFError {
|
||||
error: string;
|
||||
error_message?: string;
|
||||
|
||||
errors?: {error: string; error_message: string}[];
|
||||
warnings?: {error: string; error_message: string}[];
|
||||
}
|
||||
|
||||
export class ZncaApiNxapi extends ZncaApi {
|
||||
constructor(readonly url: string, useragent?: string) {
|
||||
constructor(readonly url: string, readonly app?: {platform?: string; version?: string;}, useragent?: string) {
|
||||
super(useragent);
|
||||
}
|
||||
|
||||
async genf(token: string, hash_method: HashMethod) {
|
||||
async genf(token: string, hash_method: HashMethod, user?: {na_id: string; coral_user_id?: string}) {
|
||||
const request_id = uuidgen();
|
||||
|
||||
const result = await genf(this.url + '/f', hash_method, token, undefined, request_id, this.useragent);
|
||||
const result = await genf(this.url + '/f', hash_method, token, undefined, request_id,
|
||||
user, this.app, this.useragent);
|
||||
|
||||
return {
|
||||
provider: 'nxapi' as const,
|
||||
|
|
@ -311,17 +282,21 @@ export class ZncaApiNxapi extends ZncaApi {
|
|||
hash_method, token, request_id,
|
||||
timestamp: result.timestamp!, // will be included as not sent in request
|
||||
f: result.f,
|
||||
user,
|
||||
result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, useragent?: string): Promise<FResult> {
|
||||
export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, options?: ZncaApiOptions): Promise<FResult>;
|
||||
export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, useragent?: string): Promise<FResult>;
|
||||
export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, options?: ZncaApiOptions | string): Promise<FResult> {
|
||||
if (typeof options === 'string') options = {useragent: options};
|
||||
if (typeof hash_method === 'string') hash_method = parseInt(hash_method);
|
||||
|
||||
const provider = getPreferredZncaApiFromEnvironment(useragent) ?? await getDefaultZncaApi(useragent);
|
||||
const provider = getPreferredZncaApiFromEnvironment(options) ?? await getDefaultZncaApi(options);
|
||||
|
||||
return provider.genf(token, hash_method);
|
||||
return provider.genf(token, hash_method, options?.user);
|
||||
}
|
||||
|
||||
export type FResult = {
|
||||
|
|
@ -331,6 +306,7 @@ export type FResult = {
|
|||
timestamp: number;
|
||||
request_id: string;
|
||||
f: string;
|
||||
user?: {na_id: string; coral_user_id?: string;};
|
||||
result: unknown;
|
||||
} & ({
|
||||
provider: 'flapg';
|
||||
|
|
@ -344,37 +320,52 @@ export type FResult = {
|
|||
result: AndroidZncaFResponse;
|
||||
});
|
||||
|
||||
export function getPreferredZncaApiFromEnvironment(useragent?: string): ZncaApi | null {
|
||||
interface ZncaApiOptions {
|
||||
useragent?: string;
|
||||
platform?: string;
|
||||
version?: string;
|
||||
user?: {na_id: string; coral_user_id?: string;};
|
||||
}
|
||||
|
||||
export function getPreferredZncaApiFromEnvironment(options?: ZncaApiOptions): ZncaApi | null;
|
||||
export function getPreferredZncaApiFromEnvironment(useragent?: string): ZncaApi | null;
|
||||
export function getPreferredZncaApiFromEnvironment(options?: ZncaApiOptions | string): ZncaApi | null {
|
||||
if (typeof options === 'string') options = {useragent: options};
|
||||
|
||||
if (process.env.NXAPI_ZNCA_API) {
|
||||
if (process.env.NXAPI_ZNCA_API === 'flapg') {
|
||||
return new ZncaApiFlapg(useragent);
|
||||
return new ZncaApiFlapg(options?.useragent);
|
||||
}
|
||||
if (process.env.NXAPI_ZNCA_API === 'imink') {
|
||||
return new ZncaApiImink(useragent);
|
||||
return new ZncaApiImink(options?.useragent);
|
||||
}
|
||||
|
||||
throw new Error('Unknown znca API provider');
|
||||
}
|
||||
|
||||
if (process.env.ZNCA_API_URL) {
|
||||
return new ZncaApiNxapi(process.env.ZNCA_API_URL, useragent);
|
||||
return new ZncaApiNxapi(process.env.ZNCA_API_URL, options, options?.useragent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getDefaultZncaApi(useragent?: string) {
|
||||
export async function getDefaultZncaApi(options?: ZncaApiOptions): Promise<ZncaApi>;
|
||||
export async function getDefaultZncaApi(useragent?: string): Promise<ZncaApi>;
|
||||
export async function getDefaultZncaApi(options?: ZncaApiOptions | string) {
|
||||
if (typeof options === 'string') options = {useragent: options};
|
||||
|
||||
const { default: { coral_auth: { default: provider } } } = await import('../common/remote-config.js');
|
||||
|
||||
if (provider === 'flapg') {
|
||||
return new ZncaApiFlapg(useragent);
|
||||
return new ZncaApiFlapg(options?.useragent);
|
||||
}
|
||||
if (provider === 'imink') {
|
||||
return new ZncaApiImink(useragent);
|
||||
return new ZncaApiImink(options?.useragent);
|
||||
}
|
||||
|
||||
if (provider[0] === 'nxapi') {
|
||||
return new ZncaApiNxapi(provider[1], useragent);
|
||||
return new ZncaApiNxapi(provider[1], options, options?.useragent);
|
||||
}
|
||||
|
||||
throw new Error('Invalid znca API provider');
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import fetch, { Response } from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
|
||||
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
|
||||
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, ParentalControlSettingState, SmartDevices, User } from './moon-types.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:moon');
|
||||
|
|
@ -16,9 +16,10 @@ const ZNMA_USER_AGENT = 'moon_ANDROID/' + ZNMA_VERSION + ' (com.nintendo.znma; b
|
|||
'; ANDROID 26)';
|
||||
|
||||
export default class MoonApi {
|
||||
onTokenExpired: ((data: MoonError, res: Response) => Promise<MoonAuthData | PartialMoonAuthData | void>) | null = null;
|
||||
onTokenExpired: ((data?: MoonError, res?: Response) => Promise<MoonAuthData | PartialMoonAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
protected _token_expired = false;
|
||||
|
||||
protected constructor(
|
||||
public token: string,
|
||||
|
|
@ -31,8 +32,18 @@ export default class MoonApi {
|
|||
async fetch<T extends object>(
|
||||
url: string, method = 'GET', body?: string, headers?: object,
|
||||
/** @internal */ _autoRenewToken = true,
|
||||
/** @internal */ _attempt = 0
|
||||
/** @internal */ _attempt = 0,
|
||||
): Promise<HasResponse<T, Response>> {
|
||||
if (this._token_expired && _autoRenewToken && !this._renewToken) {
|
||||
if (!this.onTokenExpired || _attempt) throw new Error('Token expired');
|
||||
|
||||
this._renewToken = this.onTokenExpired.call(null).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (this._renewToken && _autoRenewToken) {
|
||||
await this._renewToken;
|
||||
}
|
||||
|
|
@ -62,6 +73,7 @@ export default class MoonApi {
|
|||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
this._token_expired = true;
|
||||
const data = await response.json() as MoonError;
|
||||
|
||||
// _renewToken will be awaited when calling fetch
|
||||
|
|
@ -123,6 +135,7 @@ export default class MoonApi {
|
|||
private setTokenWithSavedToken(data: MoonAuthData | PartialMoonAuthData) {
|
||||
this.token = data.nintendoAccountToken.access_token!;
|
||||
if ('user' in data) this.naId = data.user.id;
|
||||
this._token_expired = false;
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { defineResponse, ErrorResponse } from './util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { JwtPayload } from '../util/jwt.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
|
|
@ -14,8 +14,7 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Platform': 'Android',
|
||||
'X-ProductVersion': '2.0.0',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'NASDKAPI; Android',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import fetch, { Response } from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { WebServiceToken } from './coral-types.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { WebServiceError, Users, AuthToken, UserProfile, Newspapers, Newspaper, Emoticons, Reaction, IslandProfile } from './nooklink-types.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:nooklink');
|
||||
|
|
@ -17,9 +17,10 @@ const NOOKLINK_URL = NOOKLINK_WEBSERVICE_URL + '/api';
|
|||
const BLANCO_VERSION = '2.1.1';
|
||||
|
||||
export default class NooklinkApi {
|
||||
onTokenExpired: ((data: WebServiceError, res: Response) => Promise<NooklinkAuthData | void>) | null = null;
|
||||
onTokenExpired: ((data?: WebServiceError, res?: Response) => Promise<NooklinkAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
protected _token_expired = false;
|
||||
|
||||
protected constructor(
|
||||
public gtoken: string,
|
||||
|
|
@ -30,8 +31,18 @@ export default class NooklinkApi {
|
|||
async fetch<T extends object>(
|
||||
url: string, method = 'GET', body?: string | FormData, headers?: object,
|
||||
/** @internal */ _autoRenewToken = true,
|
||||
/** @internal */ _attempt = 0
|
||||
/** @internal */ _attempt = 0,
|
||||
): Promise<HasResponse<T, Response>> {
|
||||
if (this._token_expired && _autoRenewToken && !this._renewToken) {
|
||||
if (!this.onTokenExpired || _attempt) throw new Error('Token expired');
|
||||
|
||||
this._renewToken = this.onTokenExpired.call(null).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (this._renewToken && _autoRenewToken) {
|
||||
await this._renewToken;
|
||||
}
|
||||
|
|
@ -57,6 +68,7 @@ export default class NooklinkApi {
|
|||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
this._token_expired = true;
|
||||
const data = await response.json() as WebServiceError;
|
||||
|
||||
// _renewToken will be awaited when calling fetch
|
||||
|
|
@ -109,6 +121,7 @@ export default class NooklinkApi {
|
|||
|
||||
private setTokenWithSavedToken(data: NooklinkAuthData) {
|
||||
this.gtoken = data.gtoken;
|
||||
this._token_expired = false;
|
||||
}
|
||||
|
||||
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
|
|
@ -195,9 +208,10 @@ export default class NooklinkApi {
|
|||
}
|
||||
|
||||
export class NooklinkUserApi {
|
||||
onTokenExpired: ((data: WebServiceError, res: Response) => Promise<NooklinkUserAuthData | PartialNooklinkUserAuthData | void>) | null = null;
|
||||
onTokenExpired: ((data?: WebServiceError, res?: Response) => Promise<NooklinkUserAuthData | PartialNooklinkUserAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
protected _token_expired = false;
|
||||
|
||||
protected constructor(
|
||||
public user_id: string,
|
||||
|
|
@ -211,8 +225,18 @@ export class NooklinkUserApi {
|
|||
async fetch<T extends object>(
|
||||
url: string, method = 'GET', body?: string | FormData, headers?: object,
|
||||
/** @internal */ _autoRenewToken = true,
|
||||
/** @internal */ _attempt = 0
|
||||
/** @internal */ _attempt = 0,
|
||||
): Promise<HasResponse<T, Response>> {
|
||||
if (this._token_expired && _autoRenewToken && !this._renewToken) {
|
||||
if (!this.onTokenExpired || _attempt) throw new Error('Token expired');
|
||||
|
||||
this._renewToken = this.onTokenExpired.call(null).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (this._renewToken && _autoRenewToken) {
|
||||
await this._renewToken;
|
||||
}
|
||||
|
|
@ -239,6 +263,7 @@ export class NooklinkUserApi {
|
|||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
this._token_expired = true;
|
||||
const data = await response.json() as WebServiceError;
|
||||
|
||||
// _renewToken will be awaited when calling fetch
|
||||
|
|
@ -327,6 +352,7 @@ export class NooklinkUserApi {
|
|||
this.user_id = data.user_id;
|
||||
this.auth_token = data.token.token;
|
||||
this.gtoken = data.gtoken;
|
||||
this._token_expired = false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import { WebServiceToken } from './coral-types.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { defineResponse, ErrorResponse } from './util.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { ActiveFestivals, CoopResult, CoopResults, CoopSchedules, HeroRecords, LeagueMatchRankings, NicknameAndIcons, PastFestivals, Records, Result, Results, Schedules, ShareResponse, ShopMerchandises, Stages, Timeline, WebServiceError, XPowerRankingRecords, XPowerRankingSummary } from './splatnet2-types.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
import { toSeasonId, Rule as XPowerRankingRule, Season } from './splatnet2-xrank.js';
|
||||
|
||||
|
|
@ -25,6 +25,8 @@ export const updateIksmSessionLastUsed: {
|
|||
} = {};
|
||||
|
||||
export default class SplatNet2Api {
|
||||
protected _session_expired = false;
|
||||
|
||||
protected constructor(
|
||||
public iksm_session: string,
|
||||
public unique_id: string,
|
||||
|
|
@ -32,6 +34,10 @@ export default class SplatNet2Api {
|
|||
) {}
|
||||
|
||||
async fetch<T extends object>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
|
||||
if (this._session_expired) {
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
|
||||
const [signal, cancel] = timeoutSignal();
|
||||
const response = await fetch(SPLATNET2_URL + url, {
|
||||
method,
|
||||
|
|
@ -52,6 +58,10 @@ export default class SplatNet2Api {
|
|||
|
||||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status === 401) {
|
||||
this._session_expired = true;
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorResponse('[splatnet2] Non-200 status code', response, await response.text());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import createDebug from 'debug';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { BankaraBattleHistoriesRefetchResult, BankaraBattleHistoriesRefetchVariables, GraphQLRequest, GraphQLResponse, GraphQLSuccessResponse, KnownRequestId, LatestBattleHistoriesRefetchResult, LatestBattleHistoriesRefetchVariables, MyOutfitInput, PagerUpdateBattleHistoriesByVsModeResult, PagerUpdateBattleHistoriesByVsModeVariables, PrivateBattleHistoriesRefetchResult, PrivateBattleHistoriesRefetchVariables, RegularBattleHistoriesRefetchResult, RegularBattleHistoriesRefetchVariables, RequestId, ResultTypes, VariablesTypes, XBattleHistoriesRefetchResult, XBattleHistoriesRefetchVariables } from 'splatnet3-types/splatnet3';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
import { WebServiceToken } from './coral-types.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { BulletToken } from './splatnet3-types.js';
|
||||
import { defineResponse, ErrorResponse, HasResponse, ResponseSymbol } from './util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:splatnet3');
|
||||
const debugGraphQl = createDebug('nxapi:api:splatnet3:graphql');
|
||||
|
|
@ -74,9 +74,10 @@ enum MapQueriesMode {
|
|||
|
||||
export default class SplatNet3Api {
|
||||
onTokenShouldRenew: ((remaining: number, res: Response) => Promise<SplatNet3AuthData | void>) | null = null;
|
||||
onTokenExpired: ((res: Response) => Promise<SplatNet3AuthData | void>) | null = null;
|
||||
onTokenExpired: ((res?: Response) => Promise<SplatNet3AuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
protected _token_expired = false;
|
||||
|
||||
graphql_strict = process.env.NXAPI_SPLATNET3_STRICT !== '0';
|
||||
|
||||
|
|
@ -85,6 +86,7 @@ export default class SplatNet3Api {
|
|||
public version: string,
|
||||
public map_queries: Partial<Record<string, [/** new query ID */ string, /** unsafe */ boolean] | null>>,
|
||||
readonly map_queries_mode: MapQueriesMode,
|
||||
readonly na_country: string,
|
||||
public language: string,
|
||||
public useragent: string,
|
||||
) {}
|
||||
|
|
@ -92,8 +94,18 @@ export default class SplatNet3Api {
|
|||
async fetch<T = unknown>(
|
||||
url: string, method = 'GET', body?: string | FormData, headers?: object,
|
||||
/** @internal */ _log?: string,
|
||||
/** @internal */ _attempt = 0
|
||||
/** @internal */ _attempt = 0,
|
||||
): Promise<HasResponse<T, Response>> {
|
||||
if (this._token_expired && !this._renewToken) {
|
||||
if (!this.onTokenExpired || _attempt) throw new Error('Token expired');
|
||||
|
||||
this._renewToken = this.onTokenExpired.call(null).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (this._renewToken) {
|
||||
await this._renewToken;
|
||||
}
|
||||
|
|
@ -120,6 +132,7 @@ export default class SplatNet3Api {
|
|||
response.status, version);
|
||||
|
||||
if (response.status === 401 && !_attempt && this.onTokenExpired) {
|
||||
this._token_expired = true;
|
||||
// _renewToken will be awaited when calling fetch
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
|
|
@ -244,12 +257,16 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / */
|
||||
async getHome() {
|
||||
return this.persistedQuery(RequestId.HomeQuery, {});
|
||||
return this.persistedQuery(RequestId.HomeQuery, {
|
||||
naCountry: this.na_country,
|
||||
});
|
||||
}
|
||||
|
||||
/** / -> /setting */
|
||||
async getSettings() {
|
||||
return this.persistedQuery(RequestId.SettingQuery, {});
|
||||
return this.persistedQuery(RequestId.SettingQuery, {
|
||||
naCountry: this.na_country,
|
||||
});
|
||||
}
|
||||
|
||||
/** / -> /photo_album */
|
||||
|
|
@ -693,6 +710,14 @@ export default class SplatNet3Api {
|
|||
});
|
||||
}
|
||||
|
||||
/** / -> /my_outfits [-> /my_outfits/{id}] -> share */
|
||||
async shareOutfit(index: number, timezone_offset_minutes = 0) {
|
||||
return this.persistedQuery(RequestId.ShareMyOutfitQuery, {
|
||||
myOutfitIndex: index,
|
||||
timezoneOffset: timezone_offset_minutes, // (new Date()).getTimezoneOffset()
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Replays
|
||||
//
|
||||
|
|
@ -848,7 +873,7 @@ export default class SplatNet3Api {
|
|||
PagerUpdateBattleHistoriesByVsModeVariables
|
||||
>(RequestId.PagerUpdateBattleHistoriesByVsModeQuery, {
|
||||
isBankara: false,
|
||||
isLeague: false,
|
||||
isEvent: false,
|
||||
isPrivate: false,
|
||||
isRegular: false,
|
||||
isXBattle: false,
|
||||
|
|
@ -920,6 +945,7 @@ export default class SplatNet3Api {
|
|||
this.version = data.version;
|
||||
this.language = data.bullet_token.lang;
|
||||
this.useragent = data.useragent;
|
||||
this._token_expired = false;
|
||||
}
|
||||
|
||||
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
|
|
@ -933,6 +959,7 @@ export default class SplatNet3Api {
|
|||
data.version,
|
||||
data.queries ?? {},
|
||||
getMapPersistedQueriesModeFromEnvironment(),
|
||||
data.country,
|
||||
data.bullet_token.lang,
|
||||
data.useragent,
|
||||
);
|
||||
|
|
@ -944,6 +971,7 @@ export default class SplatNet3Api {
|
|||
data.version,
|
||||
data.queries ?? {},
|
||||
getMapPersistedQueriesModeFromEnvironment(),
|
||||
data.country ?? 'GB',
|
||||
data.language,
|
||||
SPLATNET3_WEBSERVICE_USERAGENT,
|
||||
);
|
||||
|
|
@ -1081,6 +1109,7 @@ export interface SplatNet3CliTokenData {
|
|||
bullet_token: string;
|
||||
expires_at: number;
|
||||
language: string;
|
||||
country: string;
|
||||
version: string;
|
||||
queries?: Partial<Record<string, [/** new query ID */ string, /** unsafe */ boolean] | null>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import fetch, { Response } from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralErrorResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
|
||||
import { defineResponse, ErrorResponse, ResponseSymbol } from './util.js';
|
||||
import CoralApi, { CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, ResponseDataSymbol, Result } from './coral.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { SavedToken } from '../common/auth/coral.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
import { getAdditionalUserAgents, getUserAgent } from '../util/useragent.js';
|
||||
|
||||
|
|
@ -12,10 +12,16 @@ const debug = createDebug('nxapi:api:znc-proxy');
|
|||
|
||||
export default class ZncProxyApi implements CoralApi {
|
||||
// Not used by ZncProxyApi
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<CoralAuthData | void>) | null = null;
|
||||
onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise<CoralAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
|
||||
/** @internal */
|
||||
_token_expired = false;
|
||||
/** @internal */
|
||||
na_id = '';
|
||||
/** @internal */
|
||||
coral_user_id = '';
|
||||
readonly znca_version = '';
|
||||
readonly znca_useragent = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ const electron = require('electron');
|
|||
|
||||
// Do anything that must be run before the app is ready...
|
||||
|
||||
electron.app.whenReady()
|
||||
Promise.all([
|
||||
// @ts-expect-error
|
||||
typeof __NXAPI_BUNDLE_APP_INIT__ !== 'undefined' ? import(__NXAPI_BUNDLE_APP_INIT__) : import('./app-init.js'),
|
||||
electron.app.whenReady(),
|
||||
])
|
||||
// @ts-expect-error
|
||||
.then(() => typeof __NXAPI_BUNDLE_APP_MAIN__ !== 'undefined' ? import(__NXAPI_BUNDLE_APP_MAIN__) : import('./main/index.js'))
|
||||
.then(m => m.init.call(null))
|
||||
|
|
|
|||
5
src/app/app-init.ts
Normal file
5
src/app/app-init.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { join } from 'node:path';
|
||||
import { init as initDebug } from '../util/debug.js';
|
||||
import { paths } from '../util/product.js';
|
||||
|
||||
await initDebug(join(paths.log, 'app'));
|
||||
|
|
@ -19,7 +19,7 @@ export default function DiscordPresenceSource(props: {
|
|||
return <TouchableOpacity onPress={() => ipc.showDiscordModal()}>
|
||||
<View style={[styles.discord, !props.source ? styles.discordInactive : null]}>
|
||||
{renderDiscordPresenceSource(props.source)}
|
||||
{props.presence && props.user ? <DiscordPresence presence={props.presence} user={props.user} /> : null}
|
||||
{props.presence || props.user ? <DiscordPresence presence={props.presence} user={props.user} /> : null}
|
||||
</View>
|
||||
</TouchableOpacity>;
|
||||
}
|
||||
|
|
@ -86,28 +86,37 @@ function DiscordPresenceInactive() {
|
|||
}
|
||||
|
||||
function DiscordPresence(props: {
|
||||
presence: DiscordPresence;
|
||||
user: User;
|
||||
presence: DiscordPresence | null;
|
||||
user: User | null;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation('main_window', { keyPrefix: 'sidebar' });
|
||||
|
||||
const large_image_url = props.presence.activity.largeImageKey?.match(/^\d{16}$/) ?
|
||||
const large_image_url = props.presence ? props.presence.activity.largeImageKey?.match(/^\d{16}$/) ?
|
||||
'https://cdn.discordapp.com/app-assets/' + props.presence.id + '/' +
|
||||
props.presence.activity.largeImageKey + '.png' :
|
||||
props.presence.activity.largeImageKey;
|
||||
const user_image_url = 'https://cdn.discordapp.com/avatars/' + props.user.id + '/' + props.user.avatar + '.png';
|
||||
props.presence.activity.largeImageKey : undefined;
|
||||
|
||||
return <>
|
||||
<View style={styles.discordPresence}>
|
||||
const user_image_url = props.user ?
|
||||
props.user.avatar ? 'https://cdn.discordapp.com/avatars/' + props.user.id + '/' + props.user.avatar + '.png' :
|
||||
!props.user.discriminator || props.user.discriminator === '0' ?
|
||||
'https://cdn.discordapp.com/embed/avatars/' + ((parseInt(props.user.id) >> 22) % 5) + '.png' :
|
||||
'https://cdn.discordapp.com/embed/avatars/' + (parseInt(props.user.discriminator) % 5) + '.png' : undefined;
|
||||
|
||||
return <View style={styles.discordPresenceContainer}>
|
||||
{props.presence ? <View style={styles.discordPresence}>
|
||||
<Image source={{uri: large_image_url, width: 18, height: 18}} style={styles.discordPresenceImage} />
|
||||
<Text style={styles.discordPresenceText} numberOfLines={1} ellipsizeMode="tail">{t('discord_playing')}</Text>
|
||||
</View>
|
||||
</View> : null}
|
||||
|
||||
<View style={styles.discordUser}>
|
||||
{props.user ? <View style={styles.discordUser}>
|
||||
<Image source={{uri: user_image_url, width: 18, height: 18}} style={styles.discordUserImage} />
|
||||
<Text style={styles.discordUserText} numberOfLines={1} ellipsizeMode="tail">{props.user.username}#{props.user.discriminator}</Text>
|
||||
</View>
|
||||
</>;
|
||||
<Text style={styles.discordUserText} numberOfLines={1} ellipsizeMode="tail">
|
||||
{props.user.username}<Text style={styles.discordUserDiscriminator}>#{props.user.discriminator}</Text>
|
||||
</Text>
|
||||
</View> : <View style={styles.discordUser}>
|
||||
<Text style={styles.discordUserText} numberOfLines={1} ellipsizeMode="tail">{t('discord_not_connected')}</Text>
|
||||
</View>}
|
||||
</View>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -131,8 +140,12 @@ const styles = StyleSheet.create({
|
|||
userSelect: 'all',
|
||||
},
|
||||
|
||||
discordPresenceContainer: {
|
||||
marginTop: 2,
|
||||
},
|
||||
|
||||
discordPresence: {
|
||||
marginTop: 12,
|
||||
marginTop: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
|
@ -156,4 +169,7 @@ const styles = StyleSheet.create({
|
|||
discordUserText: {
|
||||
color: TEXT_COLOUR_DARK,
|
||||
},
|
||||
discordUserDiscriminator: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ export const languages = {
|
|||
['Samuel Elliott', 'https://gitlab.fancy.org.uk/samuel', 'https://github.com/samuelthomas2774'],
|
||||
],
|
||||
},
|
||||
'de-DE': {
|
||||
name: 'Deutsch',
|
||||
app: () => import('./locale/de-de.js'),
|
||||
authors: [
|
||||
['Inkception', 'https://github.com/Inkception'],
|
||||
],
|
||||
},
|
||||
'ja-JP': {
|
||||
name: 'Japanese',
|
||||
app: () => import('./locale/ja-jp.js'),
|
||||
|
|
|
|||
393
src/app/i18n/locale/de-de.ts
Normal file
393
src/app/i18n/locale/de-de.ts
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { CREDITS_NOTICE, LICENCE_NOTICE } from '../../../common/constants.js';
|
||||
|
||||
export const app = {
|
||||
default_title: 'Nintendo Switch Online',
|
||||
|
||||
licence: LICENCE_NOTICE,
|
||||
credits: CREDITS_NOTICE,
|
||||
translation_credits: '{{language}} Übersetzungen von {{authors, list}}.',
|
||||
};
|
||||
|
||||
export const app_menu = {
|
||||
preferences: 'Einstellungen',
|
||||
view: 'Anschauen',
|
||||
learn_more: 'Mehr erfahren',
|
||||
learn_more_github: 'Mehr erfahren (GitHub)',
|
||||
search_issues: 'Probleme untersuchen',
|
||||
|
||||
refresh: 'Aktualisieren',
|
||||
};
|
||||
|
||||
export const menu_app = {
|
||||
coral_heading: 'Nintendo Switch Online',
|
||||
na_id: 'Nintendo Account ID: {{id}}',
|
||||
coral_id: 'Coral ID: {{id}}',
|
||||
nsa_id: 'NSA ID: {{id}}',
|
||||
discord_presence_enable: 'Discord Rich Presence aktivieren',
|
||||
user_notifications_enable: 'Aktiviere Benachrichtigungen für diesen User',
|
||||
friend_notifications_enable: 'Aktiviere Benachrichtigungen für Freunde von diesen User',
|
||||
refresh: 'Jetzt aktualisieren',
|
||||
add_friend: 'Freund hinzufügen',
|
||||
web_services: 'Web-Services',
|
||||
|
||||
moon_heading: 'Nintendo Switch-Altersbeschränkungen',
|
||||
|
||||
add_account: 'Account hinzufügen',
|
||||
|
||||
show_main_window: 'Zeige Hauptfenster',
|
||||
preferences: 'Einstellungen',
|
||||
quit: 'Beenden',
|
||||
};
|
||||
|
||||
export const menus = {
|
||||
add_account: {
|
||||
add_account_coral: 'Nintendo Switch Online Account hinzufügen',
|
||||
add_account_moon: 'Altersbeschränkter Nintendo Switch Account hinzufügen',
|
||||
},
|
||||
|
||||
friend_code: {
|
||||
share: 'Teilen',
|
||||
copy: 'Kopieren',
|
||||
friend_code_regenerable: 'Regeneriere durch Nintendo Switch Konsole',
|
||||
friend_code_regenerable_at: 'Kann {{date, datetime}} neu generiert werden',
|
||||
},
|
||||
|
||||
user: {
|
||||
na_id: 'Nintendo Account ID: {{id}}',
|
||||
coral_id: 'Coral ID: {{id}}',
|
||||
nsa_id: 'NSA ID: {{id}}',
|
||||
discord_disable: 'Deaktiviere Discord Rich Presence',
|
||||
discord_enabled_for: 'Discord Rich Presence für {{name}} aktiviert',
|
||||
discord_enabled_via: 'Discord Rich Presence durch {{name}} aktiviert',
|
||||
discord_enable: 'Aktiviere Discord Rich Presence für diesen User...',
|
||||
friend_notifications_enable: 'Aktiviere Freund-Benachrichtigungen',
|
||||
refresh: 'Jetzt aktualisieren',
|
||||
add_friend: 'Freund hinzufügen',
|
||||
remove_help: 'Benutze den nxapi Befehl, um diesen User zu entfernen',
|
||||
},
|
||||
|
||||
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}}})',
|
||||
hours_one: '{{count}} Stunde',
|
||||
hours_other: '{{count}} Stunden',
|
||||
minutes_one: '{{count}} Minute',
|
||||
minutes_other: '{{count}} Minuten',
|
||||
|
||||
presence_inactive: 'Offline (Konsole online)',
|
||||
presence_offline: 'Offline',
|
||||
presence_updated: 'Aktualisiert: {{date, datetime}}',
|
||||
presence_logout_time: 'Ausgeloggt: {{date, datetime}}',
|
||||
discord_presence_enable: 'Aktiviere Discord Rich Presence',
|
||||
},
|
||||
};
|
||||
|
||||
export const notifications = {
|
||||
playing: 'Spielt {{name}}',
|
||||
offline: 'Offline',
|
||||
};
|
||||
|
||||
export const handle_uri = {
|
||||
friend_code_select: 'Wähle einen User aus, um Freunde hinzuzufügen',
|
||||
web_service_select: 'Wähle einen User aus, um diesen Service zu öffnen',
|
||||
web_service_invalid_title: 'Unbekannter Titel',
|
||||
web_service_invalid_detail: 'Die angegebene URL verwies nicht auf einen existierenden Web-Service.',
|
||||
cancel: 'Abbrechen',
|
||||
};
|
||||
|
||||
export const na_auth = {
|
||||
window: {
|
||||
title: 'Nintendo Account',
|
||||
},
|
||||
|
||||
znca_api_use: {
|
||||
title: 'Verwendung einer Drittanbieter-API',
|
||||
|
||||
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
|
||||
- Bei Authorisierung mit der Nintendo Switch Online App: Ein Nintendo Account ID Token, welcher dein Land beinhaltet und für 15 Minuten gültig ist
|
||||
- Bei Authorisierung mit Spielspezifischen Services: Deine Coral (Nintendo Switch Online App) User ID und ein Coral ID Token, welcher deine aktuelle Nintendo Switch Online Mitgliedschaft und Altersbeschränkungsstatus beinhaltet und für 2 Stunden gültig ist`,
|
||||
|
||||
ok: 'OK',
|
||||
cancel: 'Abbrechen',
|
||||
more_information: 'Weitere Informationen',
|
||||
},
|
||||
|
||||
notification_coral: {
|
||||
title: 'Nintendo Switch Online',
|
||||
body_existing: 'Bereits als {{name}} (Nintendo Account {{na_name}} / {{na_username}}) eingeloggt',
|
||||
body_authenticated: 'Authentifiziert als {{name}} (Nintendo Account {{na_name}} / {{na_username}})',
|
||||
body_reauthenticated: 'Erneut als {{name}} (Nintendo Account {{na_name}} / {{na_username}}) authentifiziert',
|
||||
},
|
||||
|
||||
notification_moon: {
|
||||
title: 'Nintendo Switch-Altersbeschränkungen',
|
||||
body_existing: 'Bereits als {{na_name}} ({{na_username}}) eingeloggt',
|
||||
body_authenticated: 'Authentifiziert als {{na_name}} ({{na_username}})',
|
||||
body_reauthenticated: 'Erneut als {{na_name}} ({{na_username}}) authentifiziert',
|
||||
},
|
||||
|
||||
error: {
|
||||
title: 'Fehler beim Hinzufügen des Accounts',
|
||||
},
|
||||
};
|
||||
|
||||
export const time_since = {
|
||||
default: {
|
||||
now: 'jetzt',
|
||||
seconds_one: 'Vor {{count}} Sekunde',
|
||||
seconds_other: 'Vor {{count}} Sekunden',
|
||||
minutes_one: 'Vor {{count}} Minute',
|
||||
minutes_other: 'Vor {{count}} Minuten',
|
||||
hours_one: 'Vor {{count}} Stunde',
|
||||
hours_other: 'Vor {{count}} Stunden',
|
||||
days_one: 'Vor {{count}} Tag',
|
||||
days_other: 'Vor {{count}} Tagen',
|
||||
},
|
||||
|
||||
short: {
|
||||
now: 'jetzt',
|
||||
seconds_one: '{{count}} Sek',
|
||||
seconds_other: '{{count}} Sek',
|
||||
minutes_one: '{{count}} Min',
|
||||
minutes_other: '{{count}} Min',
|
||||
hours_one: '{{count}} Std',
|
||||
hours_other: '{{count}} Std',
|
||||
days_one: '{{count}} Tg',
|
||||
days_other: '{{count}} Tg',
|
||||
},
|
||||
};
|
||||
|
||||
export const main_window = {
|
||||
sidebar: {
|
||||
discord_active: 'Discord Rich Presence aktiv',
|
||||
discord_active_friend: 'Discord Rich Presence aktiv: <0></0>',
|
||||
discord_not_active: 'Discord Rich Presence nicht aktiv',
|
||||
discord_playing: 'Spielt',
|
||||
discord_not_connected: 'Nicht mit Discord verbunden',
|
||||
|
||||
add_user: 'User hinzufügen',
|
||||
discord_setup: 'Discord Rich Presence einrichten',
|
||||
|
||||
enable_auto_refresh: 'Automatisch aktualisieren',
|
||||
},
|
||||
|
||||
update: {
|
||||
update_available: 'Update verfügbar: {{name}}',
|
||||
download: 'Download',
|
||||
error: 'Fehler beim Überprüfen von Updates aufgetreten: {{message}}',
|
||||
retry: 'Erneut versuchen',
|
||||
},
|
||||
|
||||
main_section: {
|
||||
error: {
|
||||
title: 'Fehler beim Laden der Daten',
|
||||
message: 'Ein Fehler ist beim Laden der {{errors, list}} Daten aufgetreten.',
|
||||
message_friends: 'freunde',
|
||||
message_webservices: 'spielspezifische services',
|
||||
message_event: 'sprachchat',
|
||||
retry: 'Erneut versuchen',
|
||||
view_details: 'Details anschauen',
|
||||
},
|
||||
|
||||
moon_only_user: {
|
||||
title: 'Nintendo Switch Online',
|
||||
desc_1: 'Dieser User ist mit der Nintendo Switch-Altersbeschränkungen App angemeldet, aber nicht mit der Nintendo Switch Online App.',
|
||||
desc_2: 'Logge dich mit der Nintendo Switch Online App ein, um Details einzusehen oder benutze den nxapi Befehl, um auf Altersbeschränkungen zuzugreifen.',
|
||||
login: 'Login',
|
||||
},
|
||||
|
||||
section_error: 'Fehler beim Aktualisieren der Daten',
|
||||
},
|
||||
|
||||
discord_section: {
|
||||
title: 'Discord Rich Presence',
|
||||
|
||||
setup_with_existing_user: 'Benutze einer dieser Account, um die Discord Rich Presence einzurichten: <0></0>.',
|
||||
add_user: 'Füge einen Nintendo Switch Online Account mit diesem User als Freund hinzu, um die Discord Rich Presence einzurichten.',
|
||||
active_self: 'Die Aktivität wird mit diesem User auf Discord geteilt.',
|
||||
active_friend: '<0></0>\'s Aktivität wird mit diesem Account auf Discord geteilt.',
|
||||
active_unknown: 'Die Aktivität eines unbekannten Users wird mit Discord geteilt.',
|
||||
active_via: 'Die Aktivität wird mit diesem User auf Discord über <0></0> geteilt.',
|
||||
|
||||
setup: 'Einrichten',
|
||||
disable: 'Deaktivieren',
|
||||
},
|
||||
|
||||
friends_section: {
|
||||
title: 'Freunde',
|
||||
|
||||
no_friends: 'Füge Freunde mit deiner Nintendo Switch Konsole hinzu.',
|
||||
friend_code: 'Dein Freundescode: <0></0>',
|
||||
|
||||
presence_playing: 'Spielt',
|
||||
presence_offline: 'Offline',
|
||||
},
|
||||
|
||||
webservices_section: {
|
||||
title: 'Spielspezifische Services',
|
||||
},
|
||||
|
||||
event_section: {
|
||||
title: 'Sprachchat',
|
||||
|
||||
members: '{{event}} im Spiel, {{voip}} im Sprachchat',
|
||||
members_with_total: '{{event}} im Spiel, {{voip}} von {{total}} im Sprachkanal',
|
||||
|
||||
app_start: 'Benutze die Nintendo Switch Online App auf iOS oder Android, um einen Sprachchat zu starten.',
|
||||
app_join: 'Benutze die Nintendo Switch Online App auf iOS oder Android, um einem Sprachchat beizutreten.',
|
||||
|
||||
share: 'Teilen',
|
||||
},
|
||||
};
|
||||
|
||||
export const preferences_window = {
|
||||
title: 'Einstellungen',
|
||||
|
||||
startup: {
|
||||
heading: 'Start',
|
||||
login: 'Öffne beim Start',
|
||||
background: 'Öffne im Hintergrund',
|
||||
},
|
||||
|
||||
sleep: {
|
||||
heading: 'Sleep',
|
||||
},
|
||||
|
||||
discord: {
|
||||
heading: 'Discord Rich Presence',
|
||||
enabled: 'Discord Rich Presence ist aktiviert.',
|
||||
disabled: 'Discord Rich Presence ist deaktiviert.',
|
||||
setup: 'Discord Rich Presence Setup',
|
||||
|
||||
user: 'Discord User',
|
||||
user_any: 'Zuerst gesehen',
|
||||
|
||||
friend_code: 'Freundescode',
|
||||
friend_code_help: 'Wenn du deinen Freundescode hinzufügst, wird ebenfalls dein User Icon in Discord angezeigt.',
|
||||
friend_code_self: 'Teile meinen Freundescode',
|
||||
friend_code_custom: 'Eigenen Freundescode festlegen',
|
||||
|
||||
inactive_presence: 'Zeige inaktive Aktivität',
|
||||
inactive_presence_help: 'Zeigt "Spielt nicht", wenn eine verbundene Konsole online ist, du aber nicht spielst.',
|
||||
|
||||
play_time: 'Spielzeit',
|
||||
play_time_hidden: 'Spielzeit niemals anzeigen',
|
||||
play_time_nintendo: 'Zeige Spielzeit wie sie auf der Nintendo Switch Konsole angezeigt wird',
|
||||
play_time_approximate_play_time: 'Zeige ungefähre Spielzeit (nächste 5 Stunden) an',
|
||||
play_time_approximate_play_time_since: 'Zeige ungefähre Spielzeit (nächste 5 Stunden) mit erstem Startdatum an',
|
||||
play_time_hour_play_time: 'Zeige ungefähre Spielzeit (nächste Stunde) an',
|
||||
play_time_hour_play_time_since: 'Zeige ungefähre Spielzeit (nächste Stunde) mit erstem Startdatum an',
|
||||
play_time_detailed_play_time: 'Zeige exakte Spielzeit an',
|
||||
play_time_detailed_play_time_since: 'Zeige exakte Spielzeit mit erstem Startdatum an',
|
||||
},
|
||||
|
||||
splatnet3: {
|
||||
heading: 'SplatNet 3',
|
||||
discord: 'Aktiviere die erweiterte Discord Rich Presence für Splatoon 3',
|
||||
discord_help_1: 'Benutzt SplatNet 3, um zusätzliche Informationen anzuzeigen, während du Splatoon 3 spielst. Du musst einen zweiten Nintendo Switch Account hinzufügen, welcher mit deinem Hauptaccount befreundet ist und Zugriff auf SplatNet 3 hat.',
|
||||
discord_help_2: 'Wenn du eine Presence URL benutzt, werden die zusätzlichen Informationen angezeigt, ohne diese Einstellung zu berücksichtigen.',
|
||||
},
|
||||
};
|
||||
|
||||
export const friend_window = {
|
||||
no_presence: 'Du hast keinen Zugriff auf die Aktivität von diesem User oder er war nie online.',
|
||||
|
||||
nsa_id: 'NSA ID',
|
||||
coral_id: 'Coral user ID',
|
||||
no_coral_user: 'Hat nie die Nintendo Switch Online App genutzt',
|
||||
|
||||
friends_since: 'Freunde seit: {{date, datetime}}',
|
||||
presence_updated_at: 'Aktivität aktualisiert: {{date, datetime}}',
|
||||
presence_logout_at: 'Zuletzt online: {{date, datetime}}',
|
||||
|
||||
presence_sharing: 'Dieser User kann deine Aktivität sehen.',
|
||||
presence_not_sharing: 'Dieser User kann deine Aktivität nicht sehen.',
|
||||
|
||||
discord_presence: 'Aktivität auf Discord teilen',
|
||||
close: 'Schließen',
|
||||
|
||||
presence_playing: 'Spielt {{game}}',
|
||||
presence_offline: 'Offline',
|
||||
presence_last_seen: 'Zuletzt gesehen: {{since_logout}}',
|
||||
|
||||
game_played_for_h: 'Gespielt für $t(hours, {"count": {{hours}}})',
|
||||
game_played_for_hm: 'Gespielt für $t(hours, {"count": {{hours}}}), $t(minutes, {"count": {{minutes}}})',
|
||||
game_played_for_m: 'Gespielt für $t(minutes, {"count": {{minutes}}})',
|
||||
hours_one: '{{count}} Stunde',
|
||||
hours_other: '{{count}} Stunden',
|
||||
minutes_one: '{{count}} Minute',
|
||||
minutes_other: '{{count}} Minuten',
|
||||
|
||||
game_first_played: 'Zuerst gespielt am {{date, datetime}}',
|
||||
game_first_played_now: 'Zuerst gespielt jetzt',
|
||||
game_title_id: 'Titel ID',
|
||||
game_shop: 'Nintendo eShop',
|
||||
};
|
||||
|
||||
export const addfriend_window = {
|
||||
title: 'Freund hinzufügen',
|
||||
help: 'Gebe oder füge einen Freundescode oder eine Freundescode-URL ein, um eine Freundschaftsanfrage zu senden.',
|
||||
|
||||
lookup_error: 'Fehler beim Aufrufen des Freundescodes aufgetreten: {{message}}',
|
||||
|
||||
nsa_id: 'NSA ID',
|
||||
coral_id: 'Coral user ID',
|
||||
no_coral_user: 'Hat nie die Nintendo Switch Online App genutzt',
|
||||
|
||||
send_added: 'Du bist nun mit diesem User befreundet.',
|
||||
send_sent: 'Freundschaftsanfrage gesendet. {{user}} kann deine Freundschaftsanfrage über die Nintendo Switch Konsole annehmen oder dir eine Freundschaftsanfrage über die Nintendo Switch Online App oder nxapi senden.',
|
||||
send_sending: 'Sende Freundschaftsanfrage...',
|
||||
send_error: 'Fehler beim Senden der Freundschaftsanfrage aufgetreten: {{message}}',
|
||||
|
||||
already_friends: 'Du bist bereits mit diesem User befreundet',
|
||||
|
||||
close: 'Schließen',
|
||||
send: 'Anfrage senden',
|
||||
};
|
||||
|
||||
export const discordsetup_window = {
|
||||
title: 'Discord Rich Presence Setup',
|
||||
|
||||
mode_heading: '1. Wähle den Modus aus',
|
||||
mode_coral_friend: 'Wähle einen User, der mit dir befreundet ist, aus.',
|
||||
mode_url: 'Gebe eine URL ein, die deine Aktivität zurückgibt.',
|
||||
mode_none: 'Deaktivieren',
|
||||
|
||||
coral_user_heading: '2. Wähle den User aus',
|
||||
coral_user_help: 'Der User muss mit dir befreundet sein, um die Aktivität zu teilen.',
|
||||
coral_friend_heading: '3. Wähle einen Freund aus',
|
||||
coral_friend_help: 'Das ist der User, den du teilen möchtest.',
|
||||
|
||||
url_heading: '2. Gebe eine Presence-URL ein',
|
||||
url_help: 'Der Link muss eine HTTPS-URL sein, die ein JSON-Objekt mit einem User, Freund oder Aktivitätsschlüssel zurückgibt. Diese Funktion ist für nxapi\'s znc API Proxy vorgesehen.',
|
||||
|
||||
preferences_heading: 'Konfiguriere zusätzliche Optionen für die Discord Rich Presence',
|
||||
preferences: 'Einstellungen',
|
||||
|
||||
cancel: 'Abbrechen',
|
||||
save: 'Speichern',
|
||||
};
|
||||
|
||||
export const addaccountmanual_window = {
|
||||
title: 'Account hinzufügen',
|
||||
|
||||
authorise_heading: '1. Logge dich in deinen Nintendo Account ein.',
|
||||
authorise_help: 'Wähle noch keinen Account aus.',
|
||||
authorise_open: 'Öffne die Nintendo Account Authorisierung',
|
||||
|
||||
response_heading: '2. Gebe den Weiterleitungslink ein',
|
||||
response_help_1: 'Rechtsklick auf der "Externen Account verlinken" Seite, Rechtsklick auf "Diese Person auswählen" und den Link kopieren. Der Link sollte mit "{{url}}". beginnen',
|
||||
response_help_2: 'Wenn du einen Kinder-Account, welcher mit deinem Account verlinkt ist, hinzufügen möchtest, klicke auf \"Diese Person auswählen\". Wenn dann nur der Kinder-Account angezeigt wird, Rechtsklick auf \"Diese Person auswählen\" und den Link kopieren.',
|
||||
|
||||
cancel: 'Abbrechen',
|
||||
save: 'Account hinzufügen',
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { CREDITS_NOTICE, LICENCE_NOTICE } from '../../../common/constants.js';
|
||||
import { CREDITS_NOTICE, LICENCE_NOTICE, ZNCA_API_USE_TEXT } from '../../../common/constants.js';
|
||||
|
||||
export const app = {
|
||||
default_title: 'Nintendo Switch Online',
|
||||
|
|
@ -99,6 +99,41 @@ export const handle_uri = {
|
|||
cancel: 'Cancel',
|
||||
};
|
||||
|
||||
export const na_auth = {
|
||||
window: {
|
||||
title: 'Nintendo Account',
|
||||
},
|
||||
|
||||
znca_api_use: {
|
||||
title: 'Third-party API usage',
|
||||
|
||||
// This should be translated in other languages
|
||||
text: ZNCA_API_USE_TEXT,
|
||||
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
more_information: 'More information',
|
||||
},
|
||||
|
||||
notification_coral: {
|
||||
title: 'Nintendo Switch Online',
|
||||
body_existing: 'Already signed in as {{name}} (Nintendo Account {{na_name}} / {{na_username}})',
|
||||
body_authenticated: 'Authenticated as {{name}} (Nintendo Account {{na_name}} / {{na_username}})',
|
||||
body_reauthenticated: 'Reauthenticated to {{name}} (Nintendo Account {{na_name}} / {{na_username}})',
|
||||
},
|
||||
|
||||
notification_moon: {
|
||||
title: 'Nintendo Switch Parental Controls',
|
||||
body_existing: 'Already signed in as {{na_name}} ({{na_username}})',
|
||||
body_authenticated: 'Authenticated as {{na_name}} ({{na_username}})',
|
||||
body_reauthenticated: 'Reauthenticated to {{na_name}} ({{na_username}})',
|
||||
},
|
||||
|
||||
error: {
|
||||
title: 'Error adding account',
|
||||
},
|
||||
};
|
||||
|
||||
export const time_since = {
|
||||
default: {
|
||||
now: 'just now',
|
||||
|
|
@ -131,6 +166,7 @@ export const main_window = {
|
|||
discord_active_friend: 'Discord Rich Presence active: <0></0>',
|
||||
discord_not_active: 'Discord Rich Presence not active',
|
||||
discord_playing: 'Playing',
|
||||
discord_not_connected: 'Not connected to Discord',
|
||||
|
||||
add_user: 'Add user',
|
||||
discord_setup: 'Set up Discord Rich Presence',
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@ import { app, BrowserWindow, dialog, ipcMain, LoginItemSettingsOptions, Menu } f
|
|||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import createDebug from 'debug';
|
||||
import * as persist from 'node-persist';
|
||||
import { i18n } from 'i18next';
|
||||
import { init as initGlobals } from '../../common/globals.js';
|
||||
import MenuApp from './menu.js';
|
||||
import { handleOpenWebServiceUri } from './webservices.js';
|
||||
import { EmbeddedPresenceMonitor, PresenceMonitorManager } from './monitor.js';
|
||||
import { createWindow } from './windows.js';
|
||||
import { DiscordPresenceConfiguration, LoginItem, LoginItemOptions, WindowType } from '../common/types.js';
|
||||
import { initStorage, paths } from '../../util/storage.js';
|
||||
import { checkUpdates, UpdateCacheData } from '../../common/update.js';
|
||||
import Users, { CoralUser } from '../../common/users.js';
|
||||
import { createModalWindow, createWindow } from './windows.js';
|
||||
import { sendToAllWindows, setupIpc } from './ipc.js';
|
||||
import { dev, dir, git, release, version } from '../../util/product.js';
|
||||
import { addUserAgent } from '../../util/useragent.js';
|
||||
import { askUserForUri } from './util.js';
|
||||
import { setAppInstance, updateMenuLanguage } from './app-menu.js';
|
||||
import { handleAuthUri } from './na-auth.js';
|
||||
import { DiscordPresenceConfiguration, LoginItem, LoginItemOptions, WindowType } from '../common/types.js';
|
||||
import { init as initGlobals } from '../../common/globals.js';
|
||||
import { CREDITS_NOTICE, GITLAB_URL, LICENCE_NOTICE } from '../../common/constants.js';
|
||||
import { checkUpdates, UpdateCacheData } from '../../common/update.js';
|
||||
import Users, { CoralUser } from '../../common/users.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { dev, dir, git, release, version } from '../../util/product.js';
|
||||
import { addUserAgent } from '../../util/useragent.js';
|
||||
import { initStorage, paths } from '../../util/storage.js';
|
||||
import createI18n, { languages } from '../i18n/index.js';
|
||||
|
||||
const debug = createDebug('app:main');
|
||||
|
|
@ -101,17 +101,7 @@ export class App {
|
|||
return this.preferences_window;
|
||||
}
|
||||
|
||||
const window = createWindow(WindowType.PREFERENCES, {}, {
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 580,
|
||||
height: 400,
|
||||
minWidth: 580,
|
||||
maxWidth: 580,
|
||||
minHeight: 400,
|
||||
maxHeight: 400,
|
||||
});
|
||||
const window = createModalWindow(WindowType.PREFERENCES, {});
|
||||
|
||||
window.on('closed', () => this.preferences_window = null);
|
||||
|
||||
|
|
@ -125,7 +115,7 @@ export class App {
|
|||
debug('Initialising i18n with language %s', language);
|
||||
|
||||
await i18n.init({lng: language ?? undefined});
|
||||
await i18n.loadNamespaces(['app', 'app_menu', 'menus', 'handle_uri']);
|
||||
await i18n.loadNamespaces(['app', 'app_menu', 'menus', 'handle_uri', 'na_auth']);
|
||||
|
||||
return i18n;
|
||||
}
|
||||
|
|
@ -290,19 +280,9 @@ export async function handleOpenFriendCodeUri(app: App, uri: string) {
|
|||
const selected_user = await askUserForUri(app, uri, app.i18n.t('handle_uri:friend_code_select'));
|
||||
if (!selected_user) return;
|
||||
|
||||
createWindow(WindowType.ADD_FRIEND, {
|
||||
createModalWindow(WindowType.ADD_FRIEND, {
|
||||
user: selected_user[1].user.id,
|
||||
friendcode,
|
||||
}, {
|
||||
// show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,24 @@
|
|||
import { BrowserWindow, clipboard, dialog, IpcMain, KeyboardEvent, Menu, MenuItem, Settings, ShareMenu, SharingItem, shell, systemPreferences } from './electron.js';
|
||||
import { BrowserWindow, clipboard, dialog, IpcMain, KeyboardEvent, Menu, MenuItem, ShareMenu, SharingItem, shell, systemPreferences } from './electron.js';
|
||||
import * as util from 'node:util';
|
||||
import createDebug from 'debug';
|
||||
import { User } from 'discord-rpc';
|
||||
import openWebService, { QrCodeReaderOptions, WebServiceIpc, WebServiceValidationError } from './webservices.js';
|
||||
import { createWindow, getWindowConfiguration } from './windows.js';
|
||||
import { DiscordPresenceConfiguration, DiscordPresenceSource, LoginItemOptions, WindowType } from '../common/types.js';
|
||||
import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js';
|
||||
import { createModalWindow, createWindow, getWindowConfiguration, setWindowHeight } from './windows.js';
|
||||
import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js';
|
||||
import { App } from './index.js';
|
||||
import { EmbeddedPresenceMonitor } from './monitor.js';
|
||||
import { DiscordPresenceConfiguration, DiscordPresenceSource, LoginItemOptions, WindowType } from '../common/types.js';
|
||||
import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js';
|
||||
import { NintendoAccountUser } from '../../api/na.js';
|
||||
import { hrduration } from '../../util/misc.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { DiscordPresence } from '../../discord/types.js';
|
||||
import { getDiscordRpcClients } from '../../discord/rpc.js';
|
||||
import { defaultTitle } from '../../discord/titles.js';
|
||||
import type { FriendProps } from '../browser/friend/index.js';
|
||||
import type { DiscordSetupProps } from '../browser/discord/index.js';
|
||||
import type { AddFriendProps } from '../browser/add-friend/index.js';
|
||||
import { EmbeddedPresenceMonitor } from './monitor.js';
|
||||
|
||||
const debug = createDebug('app:main:ipc');
|
||||
|
||||
const shown_modal_windows = new WeakSet<BrowserWindow>();
|
||||
|
||||
export function setupIpc(appinstance: App, ipcMain: IpcMain) {
|
||||
const store = appinstance.store;
|
||||
const storage = appinstance.store.storage;
|
||||
|
|
@ -54,8 +51,8 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
|
|||
}, 60 * 60 * 1000);
|
||||
|
||||
ipcMain.handle('nxapi:accounts:list', () => storage.getItem('NintendoAccountIds'));
|
||||
ipcMain.handle('nxapi:accounts:add-coral', () => askAddNsoAccount(store.storage).then(u => u?.data.user.id));
|
||||
ipcMain.handle('nxapi:accounts:add-moon', () => askAddPctlAccount(store.storage).then(u => u?.data.user.id));
|
||||
ipcMain.handle('nxapi:accounts:add-coral', () => askAddNsoAccount(appinstance).then(u => u?.data.user.id));
|
||||
ipcMain.handle('nxapi:accounts:add-moon', () => askAddPctlAccount(appinstance).then(u => u?.data.user.id));
|
||||
|
||||
ipcMain.handle('nxapi:coral:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken.' + id));
|
||||
ipcMain.handle('nxapi:coral:getcachedtoken', (e, token: string) => storage.getItem('NsoToken.' + token));
|
||||
|
|
@ -85,61 +82,15 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
|
|||
ipcMain.handle('nxapi:coral:addfriend', (e, token: string, nsaid: string) => store.users.get(token).then(u => u.addFriend(nsaid)));
|
||||
|
||||
ipcMain.handle('nxapi:window:showpreferences', () => appinstance.showPreferencesWindow().id);
|
||||
ipcMain.handle('nxapi:window:showfriend', (e, props: FriendProps) => createWindow(WindowType.FRIEND, props, {
|
||||
parent: BrowserWindow.fromWebContents(e.sender) ?? undefined,
|
||||
modal: true,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
}).id);
|
||||
ipcMain.handle('nxapi:window:discord', (e, props: DiscordSetupProps) => createWindow(WindowType.DISCORD_PRESENCE, props, {
|
||||
parent: BrowserWindow.fromWebContents(e.sender) ?? undefined,
|
||||
modal: true,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
}).id);
|
||||
ipcMain.handle('nxapi:window:addfriend', (e, props: AddFriendProps) => createWindow(WindowType.ADD_FRIEND, props, {
|
||||
parent: BrowserWindow.fromWebContents(e.sender) ?? undefined,
|
||||
modal: true,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
}).id);
|
||||
ipcMain.handle('nxapi:window:showfriend', (e, props: FriendProps) =>
|
||||
createModalWindow(WindowType.FRIEND, props, e.sender).id);
|
||||
ipcMain.handle('nxapi:window:discord', (e, props: DiscordSetupProps) =>
|
||||
createModalWindow(WindowType.DISCORD_PRESENCE, props).id);
|
||||
ipcMain.handle('nxapi:window:addfriend', (e, props: AddFriendProps) =>
|
||||
createModalWindow(WindowType.ADD_FRIEND, props, e.sender).id);
|
||||
ipcMain.handle('nxapi:window:setheight', (e, height: number) => {
|
||||
const window = BrowserWindow.fromWebContents(e.sender)!;
|
||||
const [curWidth, curHeight] = window.getSize();
|
||||
const [curContentWidth, curContentHeight] = window.getContentSize();
|
||||
const [minWidth, minHeight] = window.getMinimumSize();
|
||||
const [maxWidth, maxHeight] = window.getMaximumSize();
|
||||
if (height !== curContentHeight && curHeight === minHeight && curHeight === maxHeight) {
|
||||
window.setMinimumSize(minWidth, height + (curHeight - curContentHeight));
|
||||
window.setMaximumSize(maxWidth, height + (curHeight - curContentHeight));
|
||||
}
|
||||
window.setContentSize(curContentWidth, height);
|
||||
|
||||
if (!shown_modal_windows.has(window)) {
|
||||
window.show();
|
||||
shown_modal_windows.add(window);
|
||||
}
|
||||
setWindowHeight(window, height);
|
||||
});
|
||||
|
||||
ipcMain.handle('nxapi:discord:config', () => appinstance.monitors.getDiscordPresenceConfiguration());
|
||||
|
|
@ -155,8 +106,12 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
|
|||
const users: User[] = [];
|
||||
|
||||
for (const client of await getDiscordRpcClients()) {
|
||||
await client.connect(defaultTitle.client);
|
||||
if (client.user && !users.find(u => u.id === client.user!.id)) users.push(client.user);
|
||||
try {
|
||||
await client.connect(defaultTitle.client);
|
||||
if (client.user && !users.find(u => u.id === client.user!.id)) users.push(client.user);
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
|
|
@ -175,10 +130,10 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
|
|||
ipcMain.handle('nxapi:menu:add-user', e => (Menu.buildFromTemplate([
|
||||
new MenuItem({label: t('add_account.add_account_coral')!, click:
|
||||
(item: MenuItem, window: BrowserWindow | undefined, event: KeyboardEvent) =>
|
||||
askAddNsoAccount(storage, !event.shiftKey)}),
|
||||
askAddNsoAccount(appinstance, !event.shiftKey)}),
|
||||
new MenuItem({label: t('add_account.add_account_moon')!, click:
|
||||
(item: MenuItem, window: BrowserWindow | undefined, event: KeyboardEvent) =>
|
||||
askAddPctlAccount(storage, !event.shiftKey)}),
|
||||
askAddPctlAccount(appinstance, !event.shiftKey)}),
|
||||
]).popup({window: BrowserWindow.fromWebContents(e.sender)!}), undefined));
|
||||
ipcMain.handle('nxapi:menu:friend-code', (e, fc: CurrentUser['links']['friendCode']) => (Menu.buildFromTemplate([
|
||||
new MenuItem({label: 'SW-' + fc.id, enabled: false}),
|
||||
|
|
@ -252,21 +207,9 @@ function buildUserMenu(app: App, user: NintendoAccountUser, nso?: CurrentUser, m
|
|||
click: () => app.menu?.setActiveDiscordPresenceUser(null)}),
|
||||
] : [
|
||||
new MenuItem({label: t('discord_enable')!,
|
||||
click: () => createWindow(WindowType.DISCORD_PRESENCE, {
|
||||
click: () => createModalWindow(WindowType.DISCORD_PRESENCE, {
|
||||
friend_nsa_id: nso.nsaId,
|
||||
}, {
|
||||
parent: window,
|
||||
modal: true,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
})}),
|
||||
}, window)}),
|
||||
]),
|
||||
new MenuItem({label: t('friend_notifications_enable')!, type: 'checkbox',
|
||||
checked: monitor?.friend_notifications,
|
||||
|
|
@ -275,21 +218,9 @@ function buildUserMenu(app: App, user: NintendoAccountUser, nso?: CurrentUser, m
|
|||
click: () => monitor?.skipIntervalInCurrentLoop(true)}),
|
||||
new MenuItem({type: 'separator'}),
|
||||
new MenuItem({label: t('add_friend')!,
|
||||
click: () => createWindow(WindowType.ADD_FRIEND, {
|
||||
click: () => createModalWindow(WindowType.ADD_FRIEND, {
|
||||
user: user.id,
|
||||
}, {
|
||||
parent: window,
|
||||
modal: true,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
})}),
|
||||
}, window)}),
|
||||
] : []),
|
||||
new MenuItem({type: 'separator'}),
|
||||
new MenuItem({label: t('remove_help')!, enabled: false}),
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import { app, dialog, Menu, Tray, nativeImage, MenuItem, BrowserWindow, KeyboardEvent } from './electron.js';
|
||||
import path from 'node:path';
|
||||
import * as util from 'node:util';
|
||||
import createDebug from 'debug';
|
||||
import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js';
|
||||
import { App } from './index.js';
|
||||
import { WebService } from '../../api/coral-types.js';
|
||||
import openWebService, { WebServiceValidationError } from './webservices.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
import { SavedMoonToken } from '../../common/auth/moon.js';
|
||||
import { dev, dir } from '../../util/product.js';
|
||||
import { EmbeddedPresenceMonitor, EmbeddedProxyPresenceMonitor } from './monitor.js';
|
||||
import { createWindow } from './windows.js';
|
||||
import { createModalWindow, createWindow } from './windows.js';
|
||||
import { WindowType } from '../common/types.js';
|
||||
import CoralApi from '../../api/coral.js';
|
||||
import { WebService } from '../../api/coral-types.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
import { SavedMoonToken } from '../../common/auth/moon.js';
|
||||
import { CachedWebServicesList } from '../../common/users.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { dev, dir, git } from '../../util/product.js';
|
||||
import { languages } from '../i18n/index.js';
|
||||
|
||||
const debug = createDebug('app:main:menu');
|
||||
|
||||
const show_force_language_menu = dev || git?.branch?.match(/^(i18n$|trans-)/);
|
||||
|
||||
export default class MenuApp {
|
||||
tray: Tray;
|
||||
|
||||
|
|
@ -121,7 +123,7 @@ export default class MenuApp {
|
|||
menu.append(new MenuItem({type: 'separator'}));
|
||||
menu.append(new MenuItem({label: t('show_main_window')!, click: () => this.app.showMainWindow()}));
|
||||
menu.append(new MenuItem({label: t('preferences')!, click: () => this.app.showPreferencesWindow()}));
|
||||
if (dev) menu.append(new MenuItem({label: 'Language', submenu: Menu.buildFromTemplate([
|
||||
if (show_force_language_menu) menu.append(new MenuItem({label: 'Language', submenu: Menu.buildFromTemplate([
|
||||
...this.app.i18n.options.supportedLngs || ['cimode'],
|
||||
].map(l => new MenuItem({
|
||||
label: languages[l as keyof typeof languages]?.name ?? l,
|
||||
|
|
@ -139,9 +141,9 @@ export default class MenuApp {
|
|||
}
|
||||
|
||||
addNsoAccount = (item: MenuItem, window: BrowserWindow | undefined, event: KeyboardEvent) =>
|
||||
askAddNsoAccount(this.app.store.storage, !event.shiftKey);
|
||||
askAddNsoAccount(this.app, !event.shiftKey);
|
||||
addPctlAccount = (item: MenuItem, window: BrowserWindow | undefined, event: KeyboardEvent) =>
|
||||
askAddPctlAccount(this.app.store.storage, !event.shiftKey);
|
||||
askAddPctlAccount(this.app, !event.shiftKey);
|
||||
|
||||
protected webservices = new Map</** language */ string, WebService[]>();
|
||||
|
||||
|
|
@ -300,18 +302,8 @@ export default class MenuApp {
|
|||
}
|
||||
|
||||
showAddFriendWindow(user: string) {
|
||||
createWindow(WindowType.ADD_FRIEND, {
|
||||
createModalWindow(WindowType.ADD_FRIEND, {
|
||||
user,
|
||||
}, {
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { dialog, Notification } from './electron.js';
|
||||
import createDebug from 'debug';
|
||||
import { i18n } from 'i18next';
|
||||
import { App } from './index.js';
|
||||
import { tryGetNativeImageFromUrl } from './util.js';
|
||||
import { DiscordPresenceConfiguration, DiscordPresenceExternalMonitorsConfiguration, DiscordPresenceSource } from '../common/types.js';
|
||||
import { CurrentUser, Friend, Game, CoralErrorResponse } from '../../api/coral-types.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../../common/presence.js';
|
||||
import { NotificationManager } from '../../common/notify.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { LoopResult } from '../../util/loop.js';
|
||||
import { tryGetNativeImageFromUrl } from './util.js';
|
||||
import { App } from './index.js';
|
||||
import { DiscordPresenceConfiguration, DiscordPresenceExternalMonitorsConfiguration, DiscordPresenceSource } from '../common/types.js';
|
||||
import { DiscordPresence, DiscordPresencePlayTime, ErrorResult } from '../../discord/types.js';
|
||||
import { DiscordRpcClient } from '../../discord/rpc.js';
|
||||
import SplatNet3Monitor, { getConfigFromAppConfig as getSplatNet3MonitorConfigFromAppConfig } from '../../discord/monitor/splatoon3.js';
|
||||
|
|
@ -45,6 +45,7 @@ export class PresenceMonitorManager {
|
|||
i.presence_user = null;
|
||||
i.user_notifications = false;
|
||||
i.friend_notifications = false;
|
||||
i.discord_preconnect = true;
|
||||
|
||||
i.discord.onUpdateActivity = (presence: DiscordPresence | null) => {
|
||||
this.app.store.emit('update-discord-presence', presence ? {...presence, config: undefined} : null);
|
||||
|
|
@ -91,6 +92,7 @@ export class PresenceMonitorManager {
|
|||
const i = new EmbeddedProxyPresenceMonitor(presence_url);
|
||||
|
||||
i.notifications = this.notifications;
|
||||
i.discord_preconnect = true;
|
||||
|
||||
i.discord.onUpdateActivity = (presence: DiscordPresence | null) => {
|
||||
this.app.store.emit('update-discord-presence', presence ? {...presence, config: undefined} : null);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { app, BrowserWindow, dialog, MessageBoxOptions, Notification, session, shell } from './electron.js';
|
||||
import process from 'node:process';
|
||||
import * as crypto from 'node:crypto';
|
||||
import createDebug from 'debug';
|
||||
import * as persist from 'node-persist';
|
||||
import { app, BrowserWindow, dialog, MessageBoxOptions, Notification, session, shell } from './electron.js';
|
||||
import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { App, protocol_registration_options } from './index.js';
|
||||
import { createModalWindow, createWindow } from './windows.js';
|
||||
import { tryGetNativeImageFromUrl } from './util.js';
|
||||
import { WindowType } from '../common/types.js';
|
||||
import { getNintendoAccountSessionToken, NintendoAccountAuthError, NintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { ZNCA_CLIENT_ID } from '../../api/coral.js';
|
||||
import { ZNMA_CLIENT_ID } from '../../api/moon.js';
|
||||
import { getToken, SavedToken } from '../../common/auth/coral.js';
|
||||
import { getPctlToken, SavedMoonToken } from '../../common/auth/moon.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { Jwt } from '../../util/jwt.js';
|
||||
import { tryGetNativeImageFromUrl } from './util.js';
|
||||
import { ZNCA_API_USE_URL } from '../../common/constants.js';
|
||||
import { createWindow } from './windows.js';
|
||||
import { WindowType } from '../common/types.js';
|
||||
import { protocol_registration_options } from './index.js';
|
||||
import { ZNCA_API_USE_TEXT, ZNCA_API_USE_URL } from '../../common/constants.js';
|
||||
|
||||
const debug = createDebug('app:main:na-auth');
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ html {
|
|||
|
||||
let i = 0;
|
||||
|
||||
export function createAuthWindow() {
|
||||
export function createAuthWindow(app: App) {
|
||||
const browser_session = session.defaultSession;
|
||||
|
||||
const window = new BrowserWindow({
|
||||
|
|
@ -63,7 +64,7 @@ export function createAuthWindow() {
|
|||
resizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
title: 'Nintendo Account',
|
||||
title: app.i18n.t('na_auth:window.title') ?? 'Nintendo Account',
|
||||
webPreferences: {
|
||||
session: browser_session,
|
||||
scrollBounce: true,
|
||||
|
|
@ -100,16 +101,22 @@ export class AuthoriseCancelError extends AuthoriseError {
|
|||
}
|
||||
}
|
||||
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window: false):
|
||||
Promise<NintendoAccountSessionTokenCode & {window: BrowserWindow}>
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window: true):
|
||||
Promise<NintendoAccountSessionTokenCode & {window?: never}>
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window?: boolean):
|
||||
Promise<NintendoAccountSessionTokenCode & {window?: BrowserWindow}>
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window = true) {
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
app: App, client_id: string, scope: string | string[], close_window: false,
|
||||
): Promise<NintendoAccountSessionTokenCode & {window: BrowserWindow}>
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
app: App, client_id: string, scope: string | string[], close_window: true,
|
||||
): Promise<NintendoAccountSessionTokenCode & {window?: never}>
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
app: App, client_id: string, scope: string | string[], close_window?: boolean,
|
||||
): Promise<NintendoAccountSessionTokenCode & {window?: BrowserWindow}>
|
||||
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
app: App, client_id: string, scope: string | string[], close_window = true,
|
||||
) {
|
||||
return new Promise<NintendoAccountSessionTokenCode>((rs, rj) => {
|
||||
const {url: authoriseurl, state, verifier, challenge} = getAuthUrl(client_id, scope);
|
||||
const window = createAuthWindow();
|
||||
const window = createAuthWindow(app);
|
||||
|
||||
const handleAuthUrl = (url: URL) => {
|
||||
const authorisedparams = new URLSearchParams(url.hash.substr(1));
|
||||
|
|
@ -312,19 +319,9 @@ function askUserForRedirectUri(
|
|||
authoriseurl: string, client_id: string,
|
||||
handleAuthUrl: (url: URL) => void, rj: (reason: any) => void
|
||||
) {
|
||||
const window = createWindow(WindowType.ADD_ACCOUNT_MANUAL_PROMPT, {
|
||||
const window = createModalWindow(WindowType.ADD_ACCOUNT_MANUAL_PROMPT, {
|
||||
authoriseurl,
|
||||
client_id,
|
||||
}, {
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
});
|
||||
|
||||
window.webContents.on('will-navigate', (event, url_string) => {
|
||||
|
|
@ -354,9 +351,9 @@ const NSO_SCOPE = [
|
|||
'user.screenName',
|
||||
];
|
||||
|
||||
export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_browser = true) {
|
||||
export async function addNsoAccount(app: App, use_in_app_browser = true) {
|
||||
const {code, verifier, window} = use_in_app_browser ?
|
||||
await getSessionTokenCodeByInAppBrowser(ZNCA_CLIENT_ID, NSO_SCOPE, false) :
|
||||
await getSessionTokenCodeByInAppBrowser(app, ZNCA_CLIENT_ID, NSO_SCOPE, false) :
|
||||
await getSessionTokenCodeByDefaultBrowser(ZNCA_CLIENT_ID, NSO_SCOPE, false);
|
||||
|
||||
window?.setFocusable(false);
|
||||
|
|
@ -365,101 +362,131 @@ export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_br
|
|||
try {
|
||||
const [jwt, sig] = Jwt.decode(code);
|
||||
|
||||
const nsotoken = await storage.getItem('NintendoAccountToken.' + jwt.payload.sub) as string | undefined;
|
||||
const nsotoken = await app.store.storage.getItem('NintendoAccountToken.' + jwt.payload.sub) as string | undefined;
|
||||
|
||||
if (nsotoken) {
|
||||
const data = await storage.getItem('NsoToken.' + nsotoken) as SavedToken | undefined;
|
||||
debug('Already authenticated', jwt.payload);
|
||||
|
||||
debug('Already authenticated', data);
|
||||
try {
|
||||
const {nso, data} = await getToken(app.store.storage, nsotoken, process.env.ZNC_PROXY_URL, false);
|
||||
|
||||
new Notification({
|
||||
title: 'Nintendo Switch Online',
|
||||
body: 'Already signed in as ' + data?.nsoAccount.user.name + ' (' + data?.user.nickname + ')',
|
||||
icon: await tryGetNativeImageFromUrl(data!.nsoAccount.user.imageUri),
|
||||
}).show();
|
||||
new Notification({
|
||||
title: app.i18n.t('na_auth:notification_coral.title') ?? 'Nintendo Switch Online',
|
||||
body: app.i18n.t('na_auth:notification_coral.body_existing', {
|
||||
name: data.nsoAccount.user.name,
|
||||
na_name: data.user.nickname,
|
||||
na_username: data.user.screenName,
|
||||
}) ?? 'Already signed in as ' + data.nsoAccount.user.name + ' (Nintendo Account ' +
|
||||
data.user.nickname + ' / ' + data.user.screenName + ')',
|
||||
icon: await tryGetNativeImageFromUrl(data.nsoAccount.user.imageUri),
|
||||
}).show();
|
||||
|
||||
return getToken(storage, nsotoken, process.env.ZNC_PROXY_URL, false);
|
||||
return {nso, data};
|
||||
} catch (err) {
|
||||
if (err instanceof ErrorResponse && err.response.url.startsWith('https://accounts.nintendo.com/')) {
|
||||
const data: NintendoAccountAuthError = err.data;
|
||||
|
||||
if (data.error === 'invalid_grant') {
|
||||
// The session token has expired/was revoked
|
||||
return authenticateCoralSessionToken(app, code, verifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await checkZncaApiUseAllowed(storage, window);
|
||||
await checkZncaApiUseAllowed(app, window);
|
||||
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID);
|
||||
|
||||
debug('session token', token);
|
||||
|
||||
const {nso, data} = await getToken(storage, token.session_token, process.env.ZNC_PROXY_URL, false);
|
||||
|
||||
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
new Notification({
|
||||
title: 'Nintendo Switch Online',
|
||||
body: 'Authenticated as ' + data.nsoAccount.user.name + ' (NSO ' + data.user.nickname + ')',
|
||||
icon: await tryGetNativeImageFromUrl(data.nsoAccount.user.imageUri),
|
||||
}).show();
|
||||
|
||||
return {nso, data};
|
||||
return authenticateCoralSessionToken(app, code, verifier);
|
||||
} finally {
|
||||
window?.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function askAddNsoAccount(storage: persist.LocalStorage, iab = true) {
|
||||
async function authenticateCoralSessionToken(
|
||||
app: App,
|
||||
code: string, verifier: string,
|
||||
reauthenticate = false,
|
||||
) {
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID);
|
||||
|
||||
debug('session token', token);
|
||||
|
||||
const {nso, data} = await getToken(app.store.storage, token.session_token, process.env.ZNC_PROXY_URL, false);
|
||||
|
||||
const users = new Set(await app.store.storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await app.store.storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
new Notification({
|
||||
title: app.i18n.t('na_auth:notification_coral.title') ?? 'Nintendo Switch Online',
|
||||
body: app.i18n.t('na_auth:notification_coral.body_' + (reauthenticate ? 're' : '') + 'authenticated', {
|
||||
name: data.nsoAccount.user.name,
|
||||
na_name: data.user.nickname,
|
||||
na_username: data.user.screenName,
|
||||
}) ?? (reauthenticate ?
|
||||
'Reauthenticated to ' + data.nsoAccount.user.name + ' (Nintendo Account ' + data.user.nickname + ' / ' +
|
||||
data.user.screenName + ')' :
|
||||
'Authenticated as ' + data.nsoAccount.user.name + ' (Nintendo Account ' + data.user.nickname + ' / ' +
|
||||
data.user.screenName + ')'),
|
||||
icon: await tryGetNativeImageFromUrl(data.nsoAccount.user.imageUri),
|
||||
}).show();
|
||||
|
||||
return {nso, data};
|
||||
}
|
||||
|
||||
export async function askAddNsoAccount(app: App, iab = true) {
|
||||
try {
|
||||
return await addNsoAccount(storage, iab);
|
||||
return await addNsoAccount(app, iab);
|
||||
} catch (err: any) {
|
||||
if (err instanceof AuthoriseError && err.code === 'access_denied') return;
|
||||
|
||||
dialog.showErrorBox('Error adding account', err.stack || err.message);
|
||||
dialog.showErrorBox(app.i18n.t('na_auth:error.title') ?? 'Error adding account',
|
||||
err.stack || err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkZncaApiUseAllowed(storage: persist.LocalStorage, window?: BrowserWindow, force = false) {
|
||||
async function checkZncaApiUseAllowed(app: App, window?: BrowserWindow, force = false) {
|
||||
if (!force) {
|
||||
if (await storage.getItem('ZncaApiConsent')) {
|
||||
if (await app.store.storage.getItem('ZncaApiConsent')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.ZNC_PROXY_URL) {
|
||||
debug('Skipping znca API consent; znc proxy URL set');
|
||||
await storage.setItem('ZncaApiConsent', true);
|
||||
await app.store.storage.setItem('ZncaApiConsent', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const ids: string[] | undefined = await storage.getItem('NintendoAccountIds');
|
||||
const ids: string[] | undefined = await app.store.storage.getItem('NintendoAccountIds');
|
||||
|
||||
for (const id of ids ?? []) {
|
||||
const nsotoken: string | undefined = await storage.getItem('NintendoAccountToken.' + id);
|
||||
const nsotoken: string | undefined = await app.store.storage.getItem('NintendoAccountToken.' + id);
|
||||
if (!nsotoken) continue;
|
||||
|
||||
debug('Skipping znca API consent; Nintendo Switch Online account already linked');
|
||||
await storage.setItem('ZncaApiConsent', true);
|
||||
await app.store.storage.setItem('ZncaApiConsent', true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (await askZncaApiUseAllowed(window)) {
|
||||
await storage.setItem('ZncaApiConsent', true);
|
||||
if (await askZncaApiUseAllowed(app, window)) {
|
||||
await app.store.storage.setItem('ZncaApiConsent', true);
|
||||
} else {
|
||||
throw new Error('Cannot continue without third-party APIs allowed');
|
||||
}
|
||||
}
|
||||
|
||||
const ZNCA_API_USE_TEXT = `To access the Nintendo Switch Online app API, nxapi must send some data to third-party APIs. This is required to generate some data to make Nintendo think you\'re using the real Nintendo Switch Online app.
|
||||
|
||||
By default, this uses the imink API, but another service can be used by setting an environment variable. The default API may change without notice if you do not force use of a specific service.
|
||||
|
||||
The data sent includes:
|
||||
|
||||
- When authenticating to the Nintendo Switch Online app: a Nintendo Account ID token, containing your Nintendo Account ID and country, which is valid for 15 minutes
|
||||
- When authenticating to game-specific services: a Coral (Nintendo Switch Online app) ID token, containing your Coral user ID, Nintendo Switch Online membership status, and Nintendo Account child restriction status, which is valid for 2 hours`;
|
||||
|
||||
async function askZncaApiUseAllowed(window?: BrowserWindow): Promise<boolean> {
|
||||
async function askZncaApiUseAllowed(app?: App, window?: BrowserWindow): Promise<boolean> {
|
||||
const options: MessageBoxOptions = {
|
||||
message: 'Third-party API usage',
|
||||
detail: ZNCA_API_USE_TEXT,
|
||||
buttons: ['OK', 'Cancel', 'More information'],
|
||||
message: app?.i18n.t('na_auth:znca_api_use.title') ?? 'Third-party API usage',
|
||||
detail: app?.i18n.t('na_auth:znca_api_use.text') ?? ZNCA_API_USE_TEXT,
|
||||
buttons: [
|
||||
app?.i18n.t('na_auth:znca_api_use.ok') ?? 'OK',
|
||||
app?.i18n.t('na_auth:znca_api_use.cancel') ?? 'Cancel',
|
||||
app?.i18n.t('na_auth:znca_api_use.more_information') ?? 'More information',
|
||||
],
|
||||
cancelId: 1,
|
||||
};
|
||||
|
||||
|
|
@ -471,7 +498,7 @@ async function askZncaApiUseAllowed(window?: BrowserWindow): Promise<boolean> {
|
|||
|
||||
if (result.response === 2) {
|
||||
shell.openExternal(ZNCA_API_USE_URL);
|
||||
return askZncaApiUseAllowed(window);
|
||||
return askZncaApiUseAllowed(app, window);
|
||||
}
|
||||
|
||||
return result.response === 0;
|
||||
|
|
@ -493,9 +520,9 @@ const MOON_SCOPE = [
|
|||
'moonMonthlySummary',
|
||||
];
|
||||
|
||||
export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_browser = true) {
|
||||
export async function addPctlAccount(app: App, use_in_app_browser = true) {
|
||||
const {code, verifier, window} = use_in_app_browser ?
|
||||
await getSessionTokenCodeByInAppBrowser(ZNMA_CLIENT_ID, MOON_SCOPE, false) :
|
||||
await getSessionTokenCodeByInAppBrowser(app, ZNMA_CLIENT_ID, MOON_SCOPE, false) :
|
||||
await getSessionTokenCodeByDefaultBrowser(ZNMA_CLIENT_ID, MOON_SCOPE, false);
|
||||
|
||||
window?.setFocusable(false);
|
||||
|
|
@ -504,48 +531,78 @@ export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_b
|
|||
try {
|
||||
const [jwt, sig] = Jwt.decode(code);
|
||||
|
||||
const moontoken = await storage.getItem('NintendoAccountToken-pctl.' + jwt.payload.sub) as string | undefined;
|
||||
const moontoken = await app.store.storage.getItem('NintendoAccountToken-pctl.' + jwt.payload.sub) as string | undefined;
|
||||
|
||||
if (moontoken) {
|
||||
const data = await storage.getItem('MoonToken.' + moontoken) as SavedMoonToken | undefined;
|
||||
debug('Already authenticated', jwt.payload);
|
||||
|
||||
debug('Already authenticated', data);
|
||||
try {
|
||||
const {moon, data} = await getPctlToken(app.store.storage, moontoken, false);
|
||||
|
||||
new Notification({
|
||||
title: 'Nintendo Switch Parental Controls',
|
||||
body: 'Already signed in as ' + data?.user.nickname,
|
||||
}).show();
|
||||
new Notification({
|
||||
title: app.i18n.t('na_auth:notification_moon.title') ?? 'Nintendo Switch Parental Controls',
|
||||
body: app.i18n.t('na_auth:notification_moon.body_existing', {
|
||||
na_name: data.user.nickname,
|
||||
na_username: data.user.screenName,
|
||||
}) ?? 'Already signed in as ' + data.user.nickname + ' (' + data.user.screenName + ')',
|
||||
}).show();
|
||||
|
||||
return getPctlToken(storage, moontoken, false);
|
||||
return {moon, data};
|
||||
} catch (err) {
|
||||
if (err instanceof ErrorResponse && err.response.url.startsWith('https://accounts.nintendo.com/')) {
|
||||
const data: NintendoAccountAuthError = err.data;
|
||||
|
||||
if (data.error === 'invalid_grant') {
|
||||
// The session token has expired/was revoked
|
||||
return authenticateMoonSessionToken(app, code, verifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNMA_CLIENT_ID);
|
||||
|
||||
debug('session token', token);
|
||||
|
||||
const {moon, data} = await getPctlToken(storage, token.session_token, false);
|
||||
|
||||
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
new Notification({
|
||||
title: 'Nintendo Switch Parental Controls',
|
||||
body: 'Authenticated as ' + data.user.nickname,
|
||||
}).show();
|
||||
|
||||
return {moon, data};
|
||||
return authenticateMoonSessionToken(app, code, verifier);
|
||||
} finally {
|
||||
window?.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function askAddPctlAccount(storage: persist.LocalStorage, iab = true) {
|
||||
async function authenticateMoonSessionToken(
|
||||
app: App,
|
||||
code: string, verifier: string,
|
||||
reauthenticate = false,
|
||||
) {
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNMA_CLIENT_ID);
|
||||
|
||||
debug('session token', token);
|
||||
|
||||
const {moon, data} = await getPctlToken(app.store.storage, token.session_token, false);
|
||||
|
||||
const users = new Set(await app.store.storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await app.store.storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
new Notification({
|
||||
title: app.i18n.t('na_auth:notification_moon.title') ?? 'Nintendo Switch Parental Controls',
|
||||
body: app.i18n.t('na_auth:notification_moon.body_' + (reauthenticate ? 're' : '') + 'authenticated', {
|
||||
na_name: data.user.nickname,
|
||||
na_username: data.user.screenName,
|
||||
}) ?? (reauthenticate ?
|
||||
'Reauthenticated to ' + data.user.nickname + ' (' + data.user.screenName + ')' :
|
||||
'Authenticated as ' + data.user.nickname + ' (' + data.user.screenName + ')'),
|
||||
}).show();
|
||||
|
||||
return {moon, data};
|
||||
}
|
||||
|
||||
export async function askAddPctlAccount(app: App, iab = true) {
|
||||
try {
|
||||
return await addPctlAccount(storage, iab);
|
||||
return await addPctlAccount(app, iab);
|
||||
} catch (err: any) {
|
||||
if (err instanceof AuthoriseError && err.code === 'access_denied') return;
|
||||
|
||||
dialog.showErrorBox('Error adding account', err.stack || err.message);
|
||||
dialog.showErrorBox(app.i18n.t('na_auth:error.title') ?? 'Error adding account',
|
||||
err.stack || err.message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { app, BrowserWindow, clipboard, dialog, IpcMainInvokeEvent, nativeImage, nativeTheme, Notification, ShareMenu, shell, WebContents } from './electron.js';
|
||||
import * as path from 'node:path';
|
||||
import { constants } from 'node:fs';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import * as util from 'node:util';
|
||||
import createDebug from 'debug';
|
||||
import { app, BrowserWindow, clipboard, dialog, IpcMainInvokeEvent, nativeImage, nativeTheme, Notification, ShareMenu, shell, WebContents } from './electron.js';
|
||||
import fetch from 'node-fetch';
|
||||
import CoralApi from '../../api/coral.js';
|
||||
import { CurrentUser, WebService, WebServiceToken } from '../../api/coral-types.js';
|
||||
import { App, Store } from './index.js';
|
||||
import type { DownloadImagesRequest, NativeShareRequest, NativeShareUrlRequest, QrCodeReaderCameraOptions, QrCodeReaderCheckinOptions, QrCodeReaderCheckinResult, QrCodeReaderPhotoLibraryOptions, SendMessageOptions } from '../preload-webservice/znca-js-api.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
import { createWebServiceWindow } from './windows.js';
|
||||
import { askUserForUri } from './util.js';
|
||||
import type { DownloadImagesRequest, NativeShareRequest, NativeShareUrlRequest, QrCodeReaderCameraOptions, QrCodeReaderCheckinOptions, QrCodeReaderCheckinResult, QrCodeReaderPhotoLibraryOptions, SendMessageOptions } from '../preload-webservice/znca-js-api.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import CoralApi from '../../api/coral.js';
|
||||
import { CurrentUser, WebService, WebServiceToken } from '../../api/coral-types.js';
|
||||
import { NintendoAccountUser } from '../../api/na.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
|
||||
const debug = createDebug('app:main:webservices');
|
||||
|
||||
|
|
@ -68,6 +68,8 @@ export default async function openWebService(
|
|||
|
||||
if (!isWebServiceUrlAllowed(webservice, url)) {
|
||||
debug('Web service attempted to navigate to a URL not allowed by it\'s `whiteList`', webservice, url);
|
||||
debug('open', url);
|
||||
shell.openExternal(url);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const windows = new WeakMap<WebContents, WindowConfiguration>();
|
|||
|
||||
export function createWindow<T extends WindowType>(
|
||||
type: T, props: WindowConfiguration<T>['props'],
|
||||
options?: BrowserWindowConstructorOptions
|
||||
options?: BrowserWindowConstructorOptions,
|
||||
) {
|
||||
// Create the browser window
|
||||
const window = new BrowserWindow({
|
||||
|
|
@ -51,6 +51,62 @@ export function getWindowConfiguration(webcontents: WebContents): WindowConfigur
|
|||
return data;
|
||||
}
|
||||
|
||||
const modal_window_width = new WeakMap<BrowserWindow, number>();
|
||||
const modal_window_shown = new WeakSet<BrowserWindow>();
|
||||
|
||||
export function createModalWindow<T extends WindowType>(
|
||||
type: T, props: WindowConfiguration<T>['props'],
|
||||
parent?: BrowserWindow | WebContents,
|
||||
options?: BrowserWindowConstructorOptions,
|
||||
) {
|
||||
if (parent && !(parent instanceof BrowserWindow)) {
|
||||
parent = BrowserWindow.fromWebContents(parent) ?? undefined;
|
||||
}
|
||||
|
||||
const window = createWindow(type, props, {
|
||||
parent,
|
||||
modal: !!parent,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 300,
|
||||
minWidth: 450,
|
||||
maxWidth: 700,
|
||||
minHeight: 300,
|
||||
maxHeight: 300,
|
||||
|
||||
...options,
|
||||
});
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Use a fixed window width on Windows due to a bug getting/setting window size
|
||||
window.setResizable(false);
|
||||
modal_window_width.set(window, options?.width ?? 560);
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
export function setWindowHeight(window: BrowserWindow, height: number) {
|
||||
const [curWidth, curHeight] = window.getSize();
|
||||
const [curContentWidth, curContentHeight] = window.getContentSize();
|
||||
const [minWidth, minHeight] = window.getMinimumSize();
|
||||
const [maxWidth, maxHeight] = window.getMaximumSize();
|
||||
|
||||
if (height !== curContentHeight && curHeight === minHeight && curHeight === maxHeight) {
|
||||
window.setMinimumSize(minWidth, height + (curHeight - curContentHeight));
|
||||
window.setMaximumSize(maxWidth, height + (curHeight - curContentHeight));
|
||||
}
|
||||
|
||||
window.setContentSize(modal_window_width.get(window) ?? curContentWidth, height);
|
||||
|
||||
if (!modal_window_shown.has(window)) {
|
||||
window.show();
|
||||
modal_window_shown.add(window);
|
||||
}
|
||||
}
|
||||
|
||||
const BACKGROUND_COLOUR_MAIN_LIGHT = process.platform === 'win32' ? '#ffffff' : '#ececec';
|
||||
const BACKGROUND_COLOUR_MAIN_DARK = process.platform === 'win32' ? '#000000' : '#252424';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import createDebug from 'debug';
|
||||
import { join } from 'node:path';
|
||||
import { init as initDebug } from './util/debug.js';
|
||||
import { paths } from './util/product.js';
|
||||
|
||||
//
|
||||
// cli entrypoint for Rollup bundle
|
||||
// cli entrypoint
|
||||
//
|
||||
|
||||
createDebug.log = console.warn.bind(console);
|
||||
if (process.env.NXAPI_DEBUG_FILE !== '0') {
|
||||
await initDebug(join(paths.log, 'cli'));
|
||||
}
|
||||
|
||||
import('./cli.js').then(cli => cli.main.call(null));
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import process from 'node:process';
|
||||
import createDebug from 'debug';
|
||||
import Yargs from 'yargs';
|
||||
import * as commands from './cli/index.js';
|
||||
import { checkUpdates } from './common/update.js';
|
||||
import createDebug from './util/debug.js';
|
||||
import { dev } from './util/product.js';
|
||||
import { paths } from './util/storage.js';
|
||||
import { YargsArguments } from './util/yargs.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import process from 'node:process';
|
||||
import createDebug from 'debug';
|
||||
import createDebug from '../util/debug.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import process from 'node:process';
|
|||
import { createRequire } from 'node:module';
|
||||
import * as path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
|
||||
import { dir } from '../util/product.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import process from 'node:process';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { Argv, YargsArguments } from '../util/yargs.js';
|
||||
import * as commands from './nooklink/index.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken, getWebServiceToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nooklink.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getWebServiceToken } from '../../common/auth/nooklink.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import process from 'node:process';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { Argv, YargsArguments } from '../util/yargs.js';
|
||||
import * as commands from './nso/index.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import { PresenceState } from '../../api/coral-types.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { hrduration } from '../../util/misc.js';
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import * as net from 'node:net';
|
||||
import * as os from 'node:os';
|
||||
import createDebug from 'debug';
|
||||
import * as persist from 'node-persist';
|
||||
import express, { Request, RequestHandler, Response } from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import { Announcement, CoralStatus, CurrentUser, Friend, FriendCodeUrl, FriendCodeUser, Presence } from '../../api/coral-types.js';
|
||||
import CoralApi from '../../api/coral.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import CoralApi from '../../api/coral.js';
|
||||
import { Announcement, CoralStatus, CurrentUser, Friend, FriendCodeUrl, FriendCodeUser, Presence } from '../../api/coral-types.js';
|
||||
import { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/znc-proxy.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
import { NotificationManager, PresenceEvent, ZncNotifications } from '../../common/notify.js';
|
||||
import { product } from '../../util/product.js';
|
||||
import { parseListenAddress } from '../../util/net.js';
|
||||
import { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/znc-proxy.js';
|
||||
import { addCliFeatureUserAgent } from '../../util/useragent.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import Users, { CoralUser } from '../../common/users.js';
|
||||
import { EventStreamResponse, HttpServer, ResponseError } from '../util/http-server.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
import { NotificationManager, PresenceEvent, ZncNotifications } from '../../common/notify.js';
|
||||
import Users, { CoralUser } from '../../common/users.js';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
|
|
@ -755,10 +755,7 @@ class Server extends HttpServer {
|
|||
this.resetAuthTimeout(na_session_token, () => user.data.user.id);
|
||||
}
|
||||
} catch (err) {
|
||||
stream.sendEvent('error', {
|
||||
error: (err as Error).name,
|
||||
error_message: (err as Error).message,
|
||||
});
|
||||
stream.sendErrorEvent(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as path from 'node:path';
|
||||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import { PresencePermissions } from '../../api/coral-types.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken, Login } from '../../common/auth/coral.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import createDebug from 'debug';
|
||||
import fetch from 'node-fetch';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
import { AuthPolicy, AuthToken } from '../../api/znc-proxy.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { Argv } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getUserAgent } from '../../util/useragent.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { Argv, YargsArguments } from '../util/yargs.js';
|
||||
import * as commands from './pctl/index.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { hrduration } from '../../util/misc.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
import * as net from 'node:net';
|
||||
import * as os from 'node:os';
|
||||
import createDebug from 'debug';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import express, { Request, Response } from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import * as persist from 'node-persist';
|
||||
import { BankaraMatchMode, BankaraMatchSetting_schedule, CoopSetting_schedule, DetailVotingStatusResult, FestMatchSetting_schedule, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, FriendListResult, FriendOnlineState, Friend_friendList, GraphQLSuccessResponse, LeagueMatchSetting_schedule, RegularMatchSetting_schedule, StageScheduleResult, VsMode, XMatchSetting_schedule } from 'splatnet3-types/splatnet3';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { BankaraMatchMode, BankaraMatchSetting_schedule, CoopRule, CoopSetting_schedule, DetailFestRecordDetailResult, DetailVotingStatusResult, FestMatchSetting_schedule, FestRecordResult, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, FriendListResult, FriendOnlineState, Friend_friendList, GraphQLSuccessResponse, KnownRequestId, LeagueMatchSetting_schedule, RegularMatchSetting_schedule, StageScheduleResult, VsMode, XMatchSetting_schedule } from 'splatnet3-types/splatnet3';
|
||||
import StageScheduleQuery_730cd98 from 'splatnet3-types/graphql/730cd98e84f1030d3e9ac86b6f1aae13';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
|
||||
import { initStorage } from '../util/storage.js';
|
||||
import { addCliFeatureUserAgent, getUserAgent } from '../util/useragent.js';
|
||||
import { parseListenAddress } from '../util/net.js';
|
||||
import { product, version } from '../util/product.js';
|
||||
import Users, { CoralUser } from '../common/users.js';
|
||||
import { Friend } from '../api/coral-types.js';
|
||||
import SplatNet3Api, { PersistedQueryResult, RequestIdSymbol } from '../api/splatnet3.js';
|
||||
import { ErrorResponse, ResponseSymbol } from '../api/util.js';
|
||||
import { getBulletToken, SavedBulletToken } from '../common/auth/splatnet3.js';
|
||||
import SplatNet3Api from '../api/splatnet3.js';
|
||||
import { ErrorResponse } from '../api/util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { initStorage } from '../util/storage.js';
|
||||
import { addCliFeatureUserAgent, getUserAgent } from '../util/useragent.js';
|
||||
import { parseListenAddress } from '../util/net.js';
|
||||
import { EventStreamResponse, HttpServer, ResponseError } from './util/http-server.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
|
||||
import { getTitleIdFromEcUrl } from '../util/misc.js';
|
||||
import StageScheduleQuery_730cd98 from 'splatnet3-types/graphql/730cd98e84f1030d3e9ac86b6f1aae13';
|
||||
|
||||
const debug = createDebug('cli:presence-server');
|
||||
const debugSplatnet3Proxy = createDebug('cli:presence-server:splatnet3-proxy');
|
||||
|
|
@ -47,6 +50,21 @@ interface TitleResult {
|
|||
since: string;
|
||||
}
|
||||
|
||||
interface FestVotingStatusRecord {
|
||||
result: Exclude<DetailVotingStatusResult['fest'], null>;
|
||||
query: KnownRequestId;
|
||||
app_version: string;
|
||||
be_version: string | null;
|
||||
|
||||
friends: {
|
||||
result: FriendListResult['friends'];
|
||||
query: KnownRequestId;
|
||||
be_version: string | null;
|
||||
};
|
||||
|
||||
fest: StageScheduleResult['currentFest'] | DetailFestRecordDetailResult['fest'] | null;
|
||||
}
|
||||
|
||||
export const command = 'presence-server';
|
||||
export const desc = 'Starts a HTTP server to fetch presence data from Coral and SplatNet 3';
|
||||
|
||||
|
|
@ -78,6 +96,14 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
describe: 'SplatNet 3 proxy URL',
|
||||
type: 'string',
|
||||
default: process.env.NXAPI_PRESENCE_SERVER_SPLATNET3_PROXY_URL,
|
||||
}).option('splatnet3-fest-votes', {
|
||||
describe: 'Record Splatoon 3 fest vote history',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('splatnet3-record-fest-votes', {
|
||||
describe: 'Record Splatoon 3 fest vote history',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('update-interval', {
|
||||
describe: 'Max. update interval in seconds',
|
||||
type: 'number',
|
||||
|
|
@ -91,6 +117,8 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
const ResourceUrlMapSymbol = Symbol('ResourceUrls');
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
addCliFeatureUserAgent('presence-server');
|
||||
|
||||
|
|
@ -113,10 +141,23 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
SplatNet3ApiUser.create(storage, token, argv.zncProxyUrl);
|
||||
}) : null;
|
||||
|
||||
const server = new Server(storage, coral_users, splatnet3_users, user_naids);
|
||||
const image_proxy_path = {
|
||||
baas: path.join(argv.dataPath, 'presence-server-resources', 'baas'),
|
||||
atum: path.join(argv.dataPath, 'presence-server-resources', 'atum'),
|
||||
splatnet3: path.join(argv.dataPath, 'presence-server-resources', 'splatnet3'),
|
||||
};
|
||||
|
||||
const server = new Server(storage, coral_users, splatnet3_users, user_naids, image_proxy_path);
|
||||
|
||||
server.allow_all_users = argv.allowAllUsers;
|
||||
server.enable_splatnet3_proxy = argv.splatnet3Proxy;
|
||||
server.record_fest_votes = argv.splatnet3FestVotes || argv.splatnet3RecordFestVotes ? {
|
||||
path: path.join(argv.dataPath, 'presence-server'),
|
||||
read: argv.splatnet3FestVotes,
|
||||
write: argv.splatnet3RecordFestVotes,
|
||||
} : null;
|
||||
server.update_interval = argv.updateInterval * 1000;
|
||||
|
||||
const app = server.app;
|
||||
|
||||
for (const address of argv.listen) {
|
||||
|
|
@ -127,13 +168,62 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
console.log('Listening on %s, port %d', address.address, address.port);
|
||||
});
|
||||
}
|
||||
|
||||
if (argv.splatnet3RecordFestVotes) {
|
||||
const update_interval_fest_voting_status_record = 60 * 60 * 1000; // 60 minutes
|
||||
|
||||
const recordFestVotes = async (is_force_early = false) => {
|
||||
const users = await Promise.all(user_naids.map(id => server.getSplatNet3User(id)));
|
||||
|
||||
debug('Checking for new fest votes to record', is_force_early);
|
||||
|
||||
let fest_ending: StageScheduleResult['currentFest'] | DetailFestRecordDetailResult['fest'] | null = null;
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
const fest = await user.getCurrentFest();
|
||||
|
||||
if (is_force_early) user.updated.fest_vote_status = null;
|
||||
|
||||
// Fetching current fest vote data will record any new data
|
||||
await user.getCurrentFestVotes();
|
||||
|
||||
if (fest && (!fest_ending ||
|
||||
new Date(fest_ending.endTime).getTime() > new Date(fest.endTime).getTime()
|
||||
)) {
|
||||
fest_ending = fest;
|
||||
}
|
||||
} catch (err) {
|
||||
debug('Error fetching current fest voting status for recording');
|
||||
}
|
||||
}
|
||||
|
||||
const time_until_fest_ends_ms = fest_ending ? new Date(fest_ending.endTime).getTime() - Date.now() : null;
|
||||
const update_interval = time_until_fest_ends_ms && time_until_fest_ends_ms > 60 * 1000 ?
|
||||
Math.min(time_until_fest_ends_ms - 60 * 1000, update_interval_fest_voting_status_record) :
|
||||
update_interval_fest_voting_status_record;
|
||||
|
||||
setTimeout(() => recordFestVotes(update_interval !== update_interval_fest_voting_status_record),
|
||||
update_interval);
|
||||
};
|
||||
|
||||
recordFestVotes();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SplatNet3User {
|
||||
created_at = Date.now();
|
||||
expires_at = Infinity;
|
||||
|
||||
record_fest_votes: {
|
||||
path: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
} | null = null;
|
||||
|
||||
schedules: GraphQLSuccessResponse<StageScheduleResult> | null = null;
|
||||
fest_records: GraphQLSuccessResponse<FestRecordResult> | null = null;
|
||||
current_fest: StageScheduleResult['currentFest'] | DetailFestRecordDetailResult['fest'] | null = null;
|
||||
fest_vote_status: GraphQLSuccessResponse<DetailVotingStatusResult> | null = null;
|
||||
|
||||
promise = new Map<string, Promise<void>>();
|
||||
|
|
@ -141,6 +231,8 @@ abstract class SplatNet3User {
|
|||
updated = {
|
||||
friends: Date.now(),
|
||||
schedules: null as number | null,
|
||||
fest_records: null as number | null,
|
||||
current_fest: null as number | null,
|
||||
fest_vote_status: null as number | null,
|
||||
};
|
||||
update_interval = 10 * 1000; // 10 seconds
|
||||
|
|
@ -199,15 +291,55 @@ abstract class SplatNet3User {
|
|||
|
||||
abstract getSchedulesData(): Promise<GraphQLSuccessResponse<StageScheduleResult>>;
|
||||
|
||||
async getCurrentFest(): Promise<StageScheduleResult['currentFest'] | DetailFestRecordDetailResult['fest'] | null> {
|
||||
let update_interval = this.update_interval_schedules;
|
||||
|
||||
if (this.schedules && this.schedules.data.currentFest) {
|
||||
const tricolour_open = new Date(this.schedules.data.currentFest.midtermTime).getTime() <= Date.now();
|
||||
const should_refresh_fest = tricolour_open &&
|
||||
![FestState.SECOND_HALF, FestState.CLOSED].includes(this.schedules.data.currentFest.state as FestState);
|
||||
|
||||
if (should_refresh_fest) update_interval = this.update_interval;
|
||||
}
|
||||
|
||||
await this.update('current_fest', async () => {
|
||||
this.current_fest = await this.getCurrentFestData();
|
||||
}, update_interval);
|
||||
|
||||
return this.current_fest;
|
||||
}
|
||||
|
||||
abstract getCurrentFestData(): Promise<StageScheduleResult['currentFest'] | DetailFestRecordDetailResult['fest'] | null>;
|
||||
|
||||
async getCurrentFestVotes(): Promise<DetailVotingStatusResult['fest'] | null> {
|
||||
await this.update('fest_vote_status', async () => {
|
||||
this.fest_vote_status = await this.getCurrentFestVotingStatusData();
|
||||
const fest_vote_status = await this.getCurrentFestVotingStatusData();
|
||||
|
||||
if (fest_vote_status) this.tryRecordFestVotes(fest_vote_status);
|
||||
|
||||
this.fest_vote_status = fest_vote_status;
|
||||
}, this.update_interval_fest_voting_status ?? this.update_interval);
|
||||
|
||||
return this.fest_vote_status?.data.fest ?? null;
|
||||
}
|
||||
|
||||
abstract getCurrentFestVotingStatusData(): Promise<GraphQLSuccessResponse<DetailVotingStatusResult> | null>;
|
||||
|
||||
async tryRecordFestVotes(fest_vote_status: GraphQLSuccessResponse<DetailVotingStatusResult>) {
|
||||
if (this.record_fest_votes?.write && fest_vote_status.data.fest &&
|
||||
JSON.stringify(fest_vote_status?.data) !== JSON.stringify(this.fest_vote_status?.data)
|
||||
) {
|
||||
try {
|
||||
await this.recordFestVotes(fest_vote_status as PersistedQueryResult<DetailVotingStatusResult>);
|
||||
} catch (err) {
|
||||
debug('Error recording updated fest vote data', fest_vote_status.data.fest.id, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async recordFestVotes(fest_vote_status: PersistedQueryResult<DetailVotingStatusResult>) {
|
||||
throw new Error('Cannot record fest vote status when using SplatNet 3 API proxy');
|
||||
}
|
||||
}
|
||||
|
||||
class SplatNet3ApiUser extends SplatNet3User {
|
||||
|
|
@ -227,10 +359,36 @@ class SplatNet3ApiUser extends SplatNet3User {
|
|||
return this.splatnet.getSchedules();
|
||||
}
|
||||
|
||||
async getCurrentFestVotingStatusData() {
|
||||
async getCurrentFestData() {
|
||||
const schedules = await this.getSchedules();
|
||||
return !schedules.currentFest || new Date(schedules.currentFest.endTime).getTime() <= Date.now() ? null :
|
||||
await this.getFestVotingStatusData(schedules.currentFest.id);
|
||||
|
||||
if (schedules.currentFest) {
|
||||
return new Date(schedules.currentFest.endTime).getTime() <= Date.now() ? null : schedules.currentFest;
|
||||
}
|
||||
|
||||
await this.update('fest_records', async () => {
|
||||
this.fest_records = await this.splatnet.getFestRecords();
|
||||
}, this.update_interval_schedules);
|
||||
|
||||
const current_or_upcoming_fest = this.fest_records!.data.festRecords.nodes.find(fest =>
|
||||
new Date(fest.endTime).getTime() >= Date.now());
|
||||
if (!current_or_upcoming_fest) return null;
|
||||
|
||||
const fest_detail = await this.getFestDetailData(current_or_upcoming_fest.id);
|
||||
|
||||
return fest_detail.data.fest;
|
||||
}
|
||||
|
||||
async getFestDetailData(id: string) {
|
||||
return this.current_fest?.id === id ?
|
||||
await this.splatnet.getFestDetailRefetch(id) :
|
||||
await this.splatnet.getFestDetail(id);
|
||||
}
|
||||
|
||||
async getCurrentFestVotingStatusData() {
|
||||
const fest = await this.getCurrentFest();
|
||||
return !fest || new Date(fest.endTime).getTime() <= Date.now() ? null :
|
||||
await this.getFestVotingStatusData(fest.id);
|
||||
}
|
||||
|
||||
async getFestVotingStatusData(id: string) {
|
||||
|
|
@ -239,6 +397,37 @@ class SplatNet3ApiUser extends SplatNet3User {
|
|||
await this.splatnet.getFestVotingStatus(id);
|
||||
}
|
||||
|
||||
async recordFestVotes(result: PersistedQueryResult<DetailVotingStatusResult>) {
|
||||
if (!result.data.fest) return;
|
||||
|
||||
const id_str = Buffer.from(result.data.fest.id, 'base64').toString() || result.data.fest.id;
|
||||
const match = id_str.match(/^Fest-([A-Z]{2}):(([A-Z]+)-(\d+))$/);
|
||||
const id = match ? match[1] + '-' + match[2] : id_str;
|
||||
|
||||
debug('Recording updated fest vote data', id);
|
||||
|
||||
await this.getFriends();
|
||||
const friends = this.friends as PersistedQueryResult<FriendListResult>;
|
||||
|
||||
const record: FestVotingStatusRecord = {
|
||||
result: result.data.fest,
|
||||
query: result[RequestIdSymbol],
|
||||
app_version: this.splatnet.version,
|
||||
be_version: result[ResponseSymbol].headers.get('x-be-version'),
|
||||
|
||||
friends: {
|
||||
result: friends.data.friends,
|
||||
query: friends[RequestIdSymbol],
|
||||
be_version: friends[ResponseSymbol].headers.get('x-be-version'),
|
||||
},
|
||||
|
||||
fest: await this.getCurrentFest(),
|
||||
};
|
||||
|
||||
await mkdirp(path.join(this.record_fest_votes!.path, 'splatnet3-fest-votes-' + id));
|
||||
await fs.writeFile(path.join(this.record_fest_votes!.path, 'splatnet3-fest-votes-' + id, Date.now() + '.json'), JSON.stringify(record, null, 4) + '\n');
|
||||
}
|
||||
|
||||
static async create(storage: persist.LocalStorage, token: string, znc_proxy_url?: string) {
|
||||
const {splatnet, data} = await getBulletToken(storage, token, znc_proxy_url, true);
|
||||
|
||||
|
|
@ -276,6 +465,10 @@ class SplatNet3ProxyUser extends SplatNet3User {
|
|||
return this.fetch('/schedules');
|
||||
}
|
||||
|
||||
async getCurrentFestData() {
|
||||
return this.fetch('/fest/current');
|
||||
}
|
||||
|
||||
async getCurrentFestVotingStatusData() {
|
||||
return this.fetch('/fest/current/voting-status');
|
||||
}
|
||||
|
|
@ -310,6 +503,16 @@ class Server extends HttpServer {
|
|||
allow_all_users = false;
|
||||
enable_splatnet3_proxy = false;
|
||||
|
||||
record_fest_votes: {
|
||||
path: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
} | null = null;
|
||||
|
||||
readonly image_proxy_path_baas: string | null = null;
|
||||
readonly image_proxy_path_atum: string | null = null;
|
||||
readonly image_proxy_path_splatnet3: string | null = null;
|
||||
|
||||
update_interval = 30 * 1000;
|
||||
/** Interval coral friends data should be updated if the requested user isn't friends with the authenticated user */
|
||||
update_interval_unknown_friends = 10 * 60 * 1000; // 10 minutes
|
||||
|
|
@ -317,12 +520,14 @@ class Server extends HttpServer {
|
|||
app: express.Express;
|
||||
|
||||
titles = new Map</** NSA ID */ string, [TitleResult | null, /** updated */ number]>();
|
||||
readonly promise_image = new Map<string, Promise<string>>();
|
||||
|
||||
constructor(
|
||||
readonly storage: persist.LocalStorage,
|
||||
readonly coral_users: Users<CoralUser>,
|
||||
readonly splatnet3_users: Users<SplatNet3User> | null,
|
||||
readonly user_ids: string[],
|
||||
image_proxy_path?: {baas?: string; atum?: string; splatnet3?: string;},
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -346,9 +551,20 @@ class Server extends HttpServer {
|
|||
this.handleAllUsersRequest(req, res)));
|
||||
app.get('/api/presence/:user', this.createApiRequestHandler((req, res) =>
|
||||
this.handlePresenceRequest(req, res, req.params.user)));
|
||||
app.get('/api/presence/:user/splatoon3-fest-votes', this.createApiRequestHandler((req, res) =>
|
||||
this.handleUserFestVotingStatusHistoryRequest(req, res, req.params.user)));
|
||||
app.get('/api/presence/:user/events', this.createApiRequestHandler((req, res) =>
|
||||
this.handlePresenceStreamRequest(req, res, req.params.user)));
|
||||
|
||||
if (image_proxy_path?.baas) {
|
||||
this.image_proxy_path_baas = image_proxy_path.baas;
|
||||
app.use('/api/presence/resources/baas', express.static(this.image_proxy_path_baas, {redirect: false}));
|
||||
}
|
||||
if (image_proxy_path?.atum) {
|
||||
this.image_proxy_path_atum = image_proxy_path.atum;
|
||||
app.use('/api/presence/resources/atum', express.static(this.image_proxy_path_atum, {redirect: false}));
|
||||
}
|
||||
|
||||
app.use('/api/splatnet3-presence', (req, res, next) => {
|
||||
console.log('[%s] [splatnet3 proxy] %s %s HTTP/%s from %s, port %d%s, %s',
|
||||
new Date(), req.method, req.url, req.httpVersion,
|
||||
|
|
@ -367,12 +583,52 @@ class Server extends HttpServer {
|
|||
this.handleSplatNet3ProxyFriends(req, res)));
|
||||
app.get('/api/splatnet3-presence/schedules', this.createApiRequestHandler((req, res) =>
|
||||
this.handleSplatNet3ProxySchedules(req, res)));
|
||||
app.get('/api/splatnet3-presence/fest/current', this.createApiRequestHandler((req, res) =>
|
||||
this.handleSplatNet3ProxyCurrentFest(req, res)));
|
||||
app.get('/api/splatnet3-presence/fest/current/voting-status', this.createApiRequestHandler((req, res) =>
|
||||
this.handleSplatNet3ProxyCurrentFestVotingStatus(req, res)));
|
||||
|
||||
app.use('/api/splatnet3', (req, res, next) => {
|
||||
console.log('[%s] [splatnet3] %s %s HTTP/%s from %s, port %d%s, %s',
|
||||
new Date(), req.method, req.url, req.httpVersion,
|
||||
req.socket.remoteAddress, req.socket.remotePort,
|
||||
req.headers['x-forwarded-for'] ? ' (' + req.headers['x-forwarded-for'] + ')' : '',
|
||||
req.headers['user-agent']);
|
||||
|
||||
res.setHeader('Server', product + ' presence-server splatnet3-proxy');
|
||||
res.setHeader('X-Server', product + ' presence-server splatnet3-proxy');
|
||||
res.setHeader('X-Served-By', os.hostname());
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
if (image_proxy_path?.splatnet3) {
|
||||
this.image_proxy_path_splatnet3 = image_proxy_path.splatnet3;
|
||||
app.use('/api/splatnet3/resources', express.static(this.image_proxy_path_splatnet3, {redirect: false}));
|
||||
}
|
||||
}
|
||||
|
||||
protected encodeJsonForResponse(data: unknown, space?: number) {
|
||||
return JSON.stringify(data, replacer, space);
|
||||
return JSON.stringify(data, (key: string, value: unknown) => replacer(key, value, data), space);
|
||||
}
|
||||
|
||||
async getCoralUser(naid: string) {
|
||||
const token = await this.storage.getItem('NintendoAccountToken.' + naid);
|
||||
const user = await this.coral_users.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
return user;
|
||||
}
|
||||
|
||||
async getSplatNet3User(naid: string) {
|
||||
const token = await this.storage.getItem('NintendoAccountToken.' + naid);
|
||||
return this.getSplatNet3UserBySessionToken(token);
|
||||
}
|
||||
|
||||
async getSplatNet3UserBySessionToken(token: string) {
|
||||
const user = await this.splatnet3_users!.get(token);
|
||||
user.record_fest_votes = this.record_fest_votes;
|
||||
user.update_interval = this.update_interval;
|
||||
return user;
|
||||
}
|
||||
|
||||
async handleAllUsersRequest(req: Request, res: Response) {
|
||||
|
|
@ -384,12 +640,7 @@ class Server extends HttpServer {
|
|||
|
||||
const result: AllUsersResult[] = [];
|
||||
|
||||
const users = await Promise.all(this.user_ids.map(async id => {
|
||||
const token = await this.storage.getItem('NintendoAccountToken.' + id);
|
||||
const user = await this.coral_users.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
return user;
|
||||
}));
|
||||
const users = await Promise.all(this.user_ids.map(id => this.getCoralUser(id)));
|
||||
|
||||
for (const user of users) {
|
||||
const friends = await user.getFriends();
|
||||
|
|
@ -415,12 +666,7 @@ class Server extends HttpServer {
|
|||
}
|
||||
|
||||
if (this.splatnet3_users && include_splatnet3) {
|
||||
const users = await Promise.all(this.user_ids.map(async id => {
|
||||
const token = await this.storage.getItem('NintendoAccountToken.' + id);
|
||||
const user = await this.splatnet3_users!.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
return user;
|
||||
}));
|
||||
const users = await Promise.all(this.user_ids.map(id => this.getSplatNet3User(id)));
|
||||
|
||||
for (const user of users) {
|
||||
const friends = await user.getFriends();
|
||||
|
|
@ -473,7 +719,9 @@ class Server extends HttpServer {
|
|||
|
||||
result.sort((a, b) => b.presence.updatedAt - a.presence.updatedAt);
|
||||
|
||||
return {result};
|
||||
const images = await this.downloadImages(result, this.getResourceBaseUrls(req));
|
||||
|
||||
return {result, [ResourceUrlMapSymbol]: images};
|
||||
}
|
||||
|
||||
async handlePresenceRequest(req: Request, res: Response | null, presence_user_nsaid: string, is_stream = false) {
|
||||
|
|
@ -523,14 +771,14 @@ class Server extends HttpServer {
|
|||
};
|
||||
|
||||
if (this.splatnet3_users && include_splatnet3) {
|
||||
const token = await this.storage.getItem('NintendoAccountToken.' + user_naid);
|
||||
const user = await this.splatnet3_users!.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
const user = await this.getSplatNet3User(user_naid);
|
||||
|
||||
await this.handleSplatoon3Presence(friend, user, response);
|
||||
}
|
||||
|
||||
return response;
|
||||
const images = await this.downloadImages(response, this.getResourceBaseUrls(req));
|
||||
|
||||
return {...response, [ResourceUrlMapSymbol]: images};
|
||||
}
|
||||
|
||||
getTitleResult(friend: Friend, updated: number, req: Request) {
|
||||
|
|
@ -546,7 +794,7 @@ class Server extends HttpServer {
|
|||
name: game.name,
|
||||
image_url: game.imageUri,
|
||||
url: 'https://fancy.org.uk/api/nxapi/title/' + encodeURIComponent(id) + '/redirect?source=' +
|
||||
encodeURIComponent('nxapi-' + version + '-presenceserver-' + req.headers.host),
|
||||
encodeURIComponent('nxapi-' + version + '-presenceserver/' + req.headers.host),
|
||||
since: new Date(Math.min(Date.now(), friend.presence.updatedAt * 1000)).toISOString(),
|
||||
} : null;
|
||||
|
||||
|
|
@ -576,38 +824,13 @@ class Server extends HttpServer {
|
|||
response.splatoon3 = friend;
|
||||
|
||||
if (fest_vote_status) {
|
||||
const schedules = await user.getSchedules();
|
||||
const fest = await user.getCurrentFest();
|
||||
|
||||
for (const team of fest_vote_status.teams) {
|
||||
const schedule_team = schedules.currentFest?.teams.find(t => t.id === team.id);
|
||||
if (!schedule_team || !team.votes || !team.preVotes) continue; // Shouldn't ever happen
|
||||
const fest_team = this.getFestTeamVotingStatus(fest_vote_status, fest, friend);
|
||||
|
||||
for (const player of team.votes.nodes) {
|
||||
if (player.userIcon.url !== friend.userIcon.url) continue;
|
||||
|
||||
response.splatoon3_fest_team = {
|
||||
...createFestScheduleTeam(schedule_team, FestVoteState.VOTED),
|
||||
...createFestVoteTeam(team, FestVoteState.VOTED),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.splatoon3_fest_team) break;
|
||||
|
||||
for (const player of team.preVotes.nodes) {
|
||||
if (player.userIcon.url !== friend.userIcon.url) continue;
|
||||
|
||||
response.splatoon3_fest_team = {
|
||||
...createFestScheduleTeam(schedule_team, FestVoteState.PRE_VOTED),
|
||||
...createFestVoteTeam(team, FestVoteState.PRE_VOTED),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.splatoon3_fest_team) break;
|
||||
}
|
||||
|
||||
if (!response.splatoon3_fest_team && fest_vote_status.undecidedVotes) {
|
||||
if (fest_team) {
|
||||
response.splatoon3_fest_team = fest_team;
|
||||
} else if (fest_vote_status.undecidedVotes) {
|
||||
response.splatoon3_fest_team = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -635,8 +858,9 @@ class Server extends HttpServer {
|
|||
friend.onlineState === FriendOnlineState.COOP_MODE_FIGHTING
|
||||
) {
|
||||
const schedules = await user.getSchedules();
|
||||
const coop_schedules = friend.coopRule === 'BIG_RUN' ?
|
||||
schedules.coopGroupingSchedule.bigRunSchedules :
|
||||
const coop_schedules =
|
||||
friend.coopRule === CoopRule.BIG_RUN ? schedules.coopGroupingSchedule.bigRunSchedules :
|
||||
friend.coopRule === CoopRule.TEAM_CONTEST ? schedules.coopGroupingSchedule.teamContestSchedules :
|
||||
schedules.coopGroupingSchedule.regularSchedules;
|
||||
const coop_setting = getSchedule(coop_schedules)?.setting;
|
||||
|
||||
|
|
@ -644,6 +868,37 @@ class Server extends HttpServer {
|
|||
}
|
||||
}
|
||||
|
||||
getFestTeamVotingStatus(
|
||||
fest_vote_status: Exclude<DetailVotingStatusResult['fest'], null>,
|
||||
fest: StageScheduleResult['currentFest'] | DetailFestRecordDetailResult['fest'] | null,
|
||||
friend: Friend_friendList,
|
||||
) {
|
||||
for (const team of fest_vote_status.teams) {
|
||||
const schedule_or_detail_team = fest?.teams.find(t => t.id === team.id);
|
||||
if (!schedule_or_detail_team || !team.votes || !team.preVotes) continue;
|
||||
|
||||
for (const player of team.votes.nodes) {
|
||||
if (player.userIcon.url !== friend.userIcon.url) continue;
|
||||
|
||||
return {
|
||||
...createFestScheduleTeam(schedule_or_detail_team, FestVoteState.VOTED),
|
||||
...createFestVoteTeam(team, FestVoteState.VOTED),
|
||||
};
|
||||
}
|
||||
|
||||
for (const player of team.preVotes.nodes) {
|
||||
if (player.userIcon.url !== friend.userIcon.url) continue;
|
||||
|
||||
return {
|
||||
...createFestScheduleTeam(schedule_or_detail_team, FestVoteState.PRE_VOTED),
|
||||
...createFestVoteTeam(team, FestVoteState.PRE_VOTED),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getSettingForVsMode(schedules: StageScheduleResult, vs_mode: Pick<VsMode, 'id' | 'mode'>) {
|
||||
if (vs_mode.mode === 'REGULAR') {
|
||||
return getSchedule(schedules.regularSchedules)?.regularMatchSetting;
|
||||
|
|
@ -669,6 +924,111 @@ class Server extends HttpServer {
|
|||
return null;
|
||||
}
|
||||
|
||||
async handleUserFestVotingStatusHistoryRequest(req: Request, res: Response, presence_user_nsaid: string) {
|
||||
if (!this.record_fest_votes?.read) {
|
||||
throw new ResponseError(404, 'not_found', 'Not recording fest voting status history');
|
||||
}
|
||||
|
||||
// Attempt to fetch the user's current presence to make sure they are
|
||||
// still friends with the presence server user
|
||||
await this.handlePresenceRequest(req, null, presence_user_nsaid);
|
||||
|
||||
const TimestampSymbol = Symbol('Timestamp');
|
||||
const VoteKeySymbol = Symbol('VoteKey');
|
||||
|
||||
const response: {
|
||||
result: {
|
||||
id: string;
|
||||
fest_id: string;
|
||||
fest_team_id: string;
|
||||
fest_team: FestTeam_votingStatus;
|
||||
updated_at: string;
|
||||
[TimestampSymbol]: number;
|
||||
[VoteKeySymbol]: string;
|
||||
}[];
|
||||
} = {
|
||||
result: [],
|
||||
};
|
||||
|
||||
const latest = new Map<string, [timestamp: Date, data: FestTeam_votingStatus]>();
|
||||
const all = req.query['include-all'] === '1';
|
||||
|
||||
for await (const dirent of await fs.opendir(this.record_fest_votes.path)) {
|
||||
if (!dirent.isDirectory() || !dirent.name.startsWith('splatnet3-fest-votes-')) continue;
|
||||
|
||||
const id = dirent.name.substr(21);
|
||||
const fest_votes_dir = path.join(this.record_fest_votes.path, dirent.name);
|
||||
|
||||
for await (const dirent of await fs.opendir(fest_votes_dir)) {
|
||||
const match = dirent.name.match(/^(\d+)\.json$/);
|
||||
if (!dirent.isFile() || !match) continue;
|
||||
|
||||
const timestamp = new Date(parseInt(match[1]));
|
||||
const is_latest = (latest.get(id)?.[0].getTime() ?? 0) <= timestamp.getTime();
|
||||
|
||||
if (!all && !is_latest) continue;
|
||||
|
||||
try {
|
||||
const data: FestVotingStatusRecord =
|
||||
JSON.parse(await fs.readFile(path.join(fest_votes_dir, dirent.name), 'utf-8'));
|
||||
|
||||
const friend = data.friends.result.nodes.find(f => Buffer.from(f.id, 'base64').toString()
|
||||
.match(/^Friend-([0-9a-f]{16})$/)?.[1] === presence_user_nsaid);
|
||||
if (!friend) continue;
|
||||
|
||||
const fest_team = this.getFestTeamVotingStatus(data.result, data.fest, friend);
|
||||
if (!fest_team) continue;
|
||||
|
||||
const fest_id = data.fest ?
|
||||
Buffer.from(data.fest.id, 'base64').toString()
|
||||
.match(/^Fest-([A-Z]{2}):(([A-Z]+)-(\d+))$/)?.[2] || data.fest.id :
|
||||
null;
|
||||
if (!fest_id) continue;
|
||||
|
||||
const fest_team_id =
|
||||
Buffer.from(fest_team.id, 'base64').toString()
|
||||
.match(/^FestTeam-([A-Z]{2}):((([A-Z]+)-(\d+)):([A-Za-z]+))$/)?.[2] || fest_team.id;
|
||||
|
||||
if (is_latest) latest.set(id, [timestamp, fest_team]);
|
||||
|
||||
if (!all) {
|
||||
let index;
|
||||
while ((index = response.result.findIndex(r => r.id === id)) >= 0) {
|
||||
response.result.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
response.result.push({
|
||||
id,
|
||||
fest_id,
|
||||
fest_team_id,
|
||||
fest_team,
|
||||
updated_at: timestamp.toISOString(),
|
||||
[TimestampSymbol]: timestamp.getTime(),
|
||||
[VoteKeySymbol]: fest_id + '/' + fest_team_id + '/' + fest_team.myVoteState,
|
||||
});
|
||||
} catch (err) {
|
||||
debug('Error reading fest voting status records', id, match[1], err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.result.length) throw new ResponseError(404, 'not_found', 'No fest voting status history for this user');
|
||||
|
||||
response.result.sort((a, b) => a[TimestampSymbol] - b[TimestampSymbol]);
|
||||
|
||||
response.result = response.result.filter((result, index, results) => {
|
||||
const prev_result = results[index - 1];
|
||||
return !prev_result || result[VoteKeySymbol] !== prev_result[VoteKeySymbol];
|
||||
});
|
||||
|
||||
response.result.reverse();
|
||||
|
||||
const images = await this.downloadImages(response.result, this.getResourceBaseUrls(req));
|
||||
|
||||
return {...response, [ResourceUrlMapSymbol]: images};
|
||||
}
|
||||
|
||||
presence_streams = new Set<EventStreamResponse>();
|
||||
|
||||
async handlePresenceStreamRequest(req: Request, res: Response, presence_user_nsaid: string) {
|
||||
|
|
@ -704,7 +1064,8 @@ class Server extends HttpServer {
|
|||
for (const [key, value] of Object.entries(result) as
|
||||
[keyof typeof result, typeof result[keyof typeof result]][]
|
||||
) {
|
||||
stream.sendEvent(key, value);
|
||||
if (typeof key !== 'string') continue;
|
||||
stream.sendEvent(key, {...value, [ResourceUrlMapSymbol]: result[ResourceUrlMapSymbol]});
|
||||
}
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.update_interval));
|
||||
|
|
@ -721,9 +1082,9 @@ class Server extends HttpServer {
|
|||
for (const [key, value] of Object.entries(result) as
|
||||
[keyof typeof result, typeof result[keyof typeof result]][]
|
||||
) {
|
||||
if (typeof key !== 'string') continue;
|
||||
if (JSON.stringify(value) === JSON.stringify(last_result[key])) continue;
|
||||
|
||||
stream.sendEvent(key, value);
|
||||
stream.sendEvent(key, {...value, [ResourceUrlMapSymbol]: result[ResourceUrlMapSymbol]});
|
||||
}
|
||||
|
||||
last_result = result;
|
||||
|
|
@ -735,8 +1096,9 @@ class Server extends HttpServer {
|
|||
|
||||
if (retry_after && /^\d+$/.test(retry_after)) {
|
||||
stream.sendEvent(null, 'debug: timestamp ' + new Date().toISOString(), {
|
||||
error: err,
|
||||
error: 'unknown_error',
|
||||
error_message: (err as Error).message,
|
||||
...err,
|
||||
});
|
||||
|
||||
await new Promise(rs => setTimeout(rs, parseInt(retry_after) * 1000));
|
||||
|
|
@ -745,17 +1107,7 @@ class Server extends HttpServer {
|
|||
}
|
||||
}
|
||||
|
||||
if (err instanceof ResponseError) {
|
||||
stream.sendEvent('error', {
|
||||
error: err.code,
|
||||
error_message: err.message,
|
||||
});
|
||||
} else {
|
||||
stream.sendEvent('error', {
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
});
|
||||
}
|
||||
stream.sendErrorEvent(err);
|
||||
|
||||
debug('Error in event stream %d', stream.id, err);
|
||||
|
||||
|
|
@ -772,8 +1124,7 @@ class Server extends HttpServer {
|
|||
req.headers.authorization.substr(3) : null;
|
||||
if (!token) throw new ResponseError(401, 'unauthorised');
|
||||
|
||||
const user = await this.splatnet3_users!.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
const user = await this.getSplatNet3UserBySessionToken(token);
|
||||
|
||||
await user.getFriends();
|
||||
return {result: user.friends};
|
||||
|
|
@ -786,13 +1137,25 @@ class Server extends HttpServer {
|
|||
req.headers.authorization.substr(3) : null;
|
||||
if (!token) throw new ResponseError(401, 'unauthorised');
|
||||
|
||||
const user = await this.splatnet3_users!.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
const user = await this.getSplatNet3UserBySessionToken(token);
|
||||
|
||||
await user.getSchedules();
|
||||
return {result: user.schedules!};
|
||||
}
|
||||
|
||||
async handleSplatNet3ProxyCurrentFest(req: Request, res: Response) {
|
||||
if (!this.enable_splatnet3_proxy) throw new ResponseError(403, 'forbidden');
|
||||
|
||||
const token = req.headers.authorization?.substr(0, 3) === 'na ' ?
|
||||
req.headers.authorization.substr(3) : null;
|
||||
if (!token) throw new ResponseError(401, 'unauthorised');
|
||||
|
||||
const user = await this.getSplatNet3UserBySessionToken(token);
|
||||
|
||||
await user.getCurrentFest();
|
||||
return {result: user.current_fest};
|
||||
}
|
||||
|
||||
async handleSplatNet3ProxyCurrentFestVotingStatus(req: Request, res: Response) {
|
||||
if (!this.enable_splatnet3_proxy) throw new ResponseError(403, 'forbidden');
|
||||
|
||||
|
|
@ -800,12 +1163,106 @@ class Server extends HttpServer {
|
|||
req.headers.authorization.substr(3) : null;
|
||||
if (!token) throw new ResponseError(401, 'unauthorised');
|
||||
|
||||
const user = await this.splatnet3_users!.get(token);
|
||||
user.update_interval = this.update_interval;
|
||||
const user = await this.getSplatNet3UserBySessionToken(token);
|
||||
|
||||
await user.getCurrentFestVotes();
|
||||
return {result: user.fest_vote_status};
|
||||
}
|
||||
|
||||
async downloadImages(data: unknown, base_url: {
|
||||
baas: string | null;
|
||||
atum: string | null;
|
||||
splatnet3: string | null;
|
||||
}): Promise<Record<string, string>> {
|
||||
const image_urls: [url: string, dir: string, base_url: string][] = [];
|
||||
|
||||
// Use JSON.stringify to iterate over everything in the response
|
||||
JSON.stringify(data, (key: string, value: unknown) => {
|
||||
if (this.image_proxy_path_baas && base_url.baas) {
|
||||
if (typeof value === 'string' &&
|
||||
value.startsWith('https://cdn-image-e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com/')
|
||||
) {
|
||||
image_urls.push([value, this.image_proxy_path_baas, base_url.baas]);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.image_proxy_path_atum && base_url.atum) {
|
||||
if (typeof value === 'string' &&
|
||||
value.startsWith('https://atum-img-lp1.cdn.nintendo.net/')
|
||||
) {
|
||||
image_urls.push([value, this.image_proxy_path_atum, base_url.atum]);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.image_proxy_path_splatnet3 && base_url.splatnet3) {
|
||||
if (typeof value === 'object' && value && 'url' in value && typeof value.url === 'string') {
|
||||
if (value.url.toLowerCase().startsWith('https://api.lp1.av5ja.srv.nintendo.net/')) {
|
||||
image_urls.push([value.url, this.image_proxy_path_splatnet3, base_url.splatnet3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const url_map: Record<string, string> = {};
|
||||
|
||||
await Promise.all(image_urls.map(async ([url, dir, base_url]) => {
|
||||
url_map[url] = new URL(await this.downloadImage(url, dir), base_url).toString();
|
||||
}));
|
||||
|
||||
return url_map;
|
||||
}
|
||||
|
||||
getResourceBaseUrls(req: Request) {
|
||||
const base_url = process.env.BASE_URL ??
|
||||
(req.headers['x-forwarded-proto'] === 'https' ? 'https://' : 'http://') +
|
||||
req.headers.host;
|
||||
|
||||
return {
|
||||
baas: this.image_proxy_path_baas ? base_url + '/api/presence/resources/baas/' : null,
|
||||
atum: this.image_proxy_path_atum ? base_url + '/api/presence/resources/atum/' : null,
|
||||
splatnet3: this.image_proxy_path_splatnet3 ? base_url + '/api/splatnet3/resources/' : null,
|
||||
};
|
||||
}
|
||||
|
||||
downloadImage(url: string, dir: string) {
|
||||
const pathname = new URL(url).pathname;
|
||||
const name = pathname.substr(1).toLowerCase()
|
||||
.replace(/^resources\//g, '')
|
||||
.replace(/(\/|^)\.\.(\/|$)/g, '$1...$2') +
|
||||
(path.extname(pathname) ? '' : '.jpeg');
|
||||
|
||||
const promise = this.promise_image.get(dir + '/' + name) ?? Promise.resolve().then(async () => {
|
||||
try {
|
||||
await fs.stat(path.join(dir, name));
|
||||
return name;
|
||||
} catch (err) {}
|
||||
|
||||
debug('Fetching image %s', name);
|
||||
const response = await fetch(url);
|
||||
const data = new Uint8Array(await response.arrayBuffer());
|
||||
|
||||
if (!response.ok) throw new ErrorResponse('Unable to download resource ' + name, response, data.toString());
|
||||
|
||||
await mkdirp(path.dirname(path.join(dir, name)));
|
||||
await fs.writeFile(path.join(dir, name), data);
|
||||
|
||||
debug('Downloaded image %s', name);
|
||||
|
||||
return name;
|
||||
}).then(result => {
|
||||
this.promise_image.delete(dir + '/' + name);
|
||||
return result;
|
||||
}).catch(err => {
|
||||
this.promise_image.delete(dir + '/' + name);
|
||||
throw err;
|
||||
});
|
||||
|
||||
this.promise_image.set(dir + '/' + name, promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
function createScheduleFest(
|
||||
|
|
@ -834,7 +1291,7 @@ function createFestVoteTeam(
|
|||
id: team.id,
|
||||
teamName: team.teamName,
|
||||
image: {
|
||||
url: getSplatoon3inkUrl(team.image.url),
|
||||
url: team.image.url,
|
||||
},
|
||||
color: team.color,
|
||||
votes: {nodes: []},
|
||||
|
|
@ -842,11 +1299,19 @@ function createFestVoteTeam(
|
|||
};
|
||||
}
|
||||
|
||||
function replacer(key: string, value: any) {
|
||||
if ((key === 'image' || key.endsWith('Image')) && value && typeof value === 'object' && 'url' in value) {
|
||||
function replacer(key: string, value: any, data: unknown) {
|
||||
const url_map = data && typeof data === 'object' && ResourceUrlMapSymbol in data &&
|
||||
data[ResourceUrlMapSymbol] && typeof data[ResourceUrlMapSymbol] === 'object' ?
|
||||
data[ResourceUrlMapSymbol] as Partial<Record<string, string>> : null;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return url_map?.[value] ?? value;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value && 'url' in value && typeof value.url === 'string') {
|
||||
return {
|
||||
...value,
|
||||
url: getSplatoon3inkUrl(value.url),
|
||||
url: url_map?.[value.url] ?? value.url,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import process from 'node:process';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { Argv, YargsArguments } from '../util/yargs.js';
|
||||
import * as commands from './splatnet2/index.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as path from 'node:path';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as path from 'node:path';
|
||||
import createDebug from 'debug';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getIksmToken } from '../../common/auth/splatnet2.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { getAllSeasons } from '../../api/splatnet2-xrank.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import process from 'node:process';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { Argv, YargsArguments } from '../util/yargs.js';
|
||||
import * as commands from './splatnet3/index.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import createDebug from 'debug';
|
||||
import { Judgement } from 'splatnet3-types/splatnet3';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import fetch from 'node-fetch';
|
||||
import { PhotoAlbumResult } from 'splatnet3-types/splatnet3';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { FestState, Fest_detail, RequestId } from 'splatnet3-types/splatnet3';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { BankaraBattleHistoriesRefetchResult, CoopHistoryResult, LatestBattleHistoriesRefetchResult, LatestBattleHistoriesResult, PrivateBattleHistoriesRefetchResult, RefetchableCoopHistory_CoopResultResult, RegularBattleHistoriesRefetchResult, RequestId, XBattleHistoriesRefetchResult } from 'splatnet3-types/splatnet3';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import createDebug from 'debug';
|
||||
import { FestState } from 'splatnet3-types/splatnet3';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import createDebug from 'debug';
|
||||
import { FriendOnlineState, Friend_friendList } from 'splatnet3-types/splatnet3';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as path from 'node:path';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import Table from '../util/table.js';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
|
|
@ -40,6 +40,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
bullet_token: data.bullet_token.bulletToken,
|
||||
expires_at: data.expires_at,
|
||||
language: data.bullet_token.lang,
|
||||
country: data.country,
|
||||
version: data.version,
|
||||
queries: data.queries,
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user