diff --git a/README.md b/README.md index 365318c..95ebbb4 100644 --- a/README.md +++ b/README.md @@ -11,39 +11,41 @@ Handles all BOSS (Background Online Storage Service) related tasks for the Prete Configurations are loaded through environment variables. `.env` files are supported. -| Environment variable | Description | Default | -| ------------------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------- | -| `PN_BOSS_CONFIG_HTTP_PORT` | The HTTP port the server listens on | None | -| `PN_BOSS_CONFIG_LOG_FORMAT` | What logging format to use, possible options: `pretty` or `json` | `pretty` | -| `PN_BOSS_CONFIG_LOG_LEVEL` | What log level to use | `info` | -| `PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY` | The BOSS WiiU AES key, needs to be dumped from a console | None | -| `PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | None | -| `PN_BOSS_CONFIG_BOSS_3DS_AES_KEY` | The BOSS 3DS AES key, needs to be dumped from a console | None | -| `PN_BOSS_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | None | -| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_ADDRESS` | Address for the GRPC server to listen on | None | -| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_PORT` | Port for the GRPC server to listen on | None | -| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_API_KEY` | API key that services will use to connect to the BOSS GRPC server | None | -| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_ADDRESS` | Address of the account GRPC server | None | -| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_PORT` | Port of the account GRPC server | None | -| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_API_KEY` | API key of the account GRPC server | None | -| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_ADDRESS` | Address of the friends GRPC server | None | -| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_PORT` | Port of the friends GRPC server | None | -| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_API_KEY` | API key of the friends GRPC server | None | -| `PN_BOSS_CONFIG_S3_ENDPOINT` | S3 server endpoint | None | -| `PN_BOSS_CONFIG_S3_REGION` | S3 server region | None | -| `PN_BOSS_CONFIG_S3_BUCKET` | S3 server bucket | None | -| `PN_BOSS_CONFIG_S3_ACCESS_KEY` | S3 access key | None | -| `PN_BOSS_CONFIG_S3_ACCESS_SECRET` | S3 access key secret | None | -| `PN_BOSS_CONFIG_CDN_DISK_PATH` | Storage path for the CDN, use as alternative for S3 | None | -| `PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED` | Should Streetpass Relay be enabled? | `false` | -| `PN_BOSS_CONFIG_STREETPASS_RELAY_CLEAN_OLD_DATA` | Should old Streetpass Relay data be automatically cleaned up? | `false` | -| `PN_BOSS_CONFIG_DOMAINS_NPDI` | What domain should the NPDI component use? | `npdi.cdn.pretendo.cc` | -| `PN_BOSS_CONFIG_DOMAINS_NPDL` | What domain should the NPDL component use? | `npdl.cdn.pretendo.cc` | -| `PN_BOSS_CONFIG_DOMAINS_NPFL` | What domain should the NPFL component use? | `npfl.c.app.pretendo.cc` | -| `PN_BOSS_CONFIG_DOMAINS_NPPL` | What domain should the NPPL component use? | `nppl.app.pretendo.cc,nppl.c.app.pretendo.cc` | -| `PN_BOSS_CONFIG_DOMAINS_NPTS` | What domain should the NPTS component use? | `npts.app.pretendo.cc` | -| `PN_BOSS_CONFIG_DOMAINS_SPR` | What domain should the SPR component use? | `service.spr.app.pretendo.cc` | - +| Environment variable | Description | Default | +|-----------------------------------------------------|---------------------------------------------------------------------|-----------------------------------------------| +| `PN_BOSS_CONFIG_HTTP_PORT` | The HTTP port the server listens on | None | +| `PN_BOSS_CONFIG_LOG_FORMAT` | What logging format to use, possible options: `pretty` or `json` | `pretty` | +| `PN_BOSS_CONFIG_LOG_LEVEL` | What log level to use | `info` | +| `PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY` | The BOSS WiiU AES key, needs to be dumped from a console | None | +| `PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | None | +| `PN_BOSS_CONFIG_BOSS_3DS_AES_KEY` | The BOSS 3DS AES key, needs to be dumped from a console | None | +| `PN_BOSS_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | None | +| `PN_BOSS_CONFIG_GRPC_MAX_RECEIVE_MESSAGE_LENGTH_MB` | The maximum size, in megabytes, a message sent to the server can be | 4 | +| `PN_BOSS_CONFIG_GRPC_MAX_SEND_MESSAGE_LENGTH_MB` | The maximum size, in megabytes, a message sent to the client can be | 4 | +| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_ADDRESS` | Address for the GRPC server to listen on | None | +| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_PORT` | Port for the GRPC server to listen on | None | +| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_API_KEY` | API key that services will use to connect to the BOSS GRPC server | None | +| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_ADDRESS` | Address of the account GRPC server | None | +| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_PORT` | Port of the account GRPC server | None | +| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_API_KEY` | API key of the account GRPC server | None | +| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_ADDRESS` | Address of the friends GRPC server | None | +| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_PORT` | Port of the friends GRPC server | None | +| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_API_KEY` | API key of the friends GRPC server | None | +| `PN_BOSS_CONFIG_S3_ENDPOINT` | S3 server endpoint | None | +| `PN_BOSS_CONFIG_S3_REGION` | S3 server region | None | +| `PN_BOSS_CONFIG_S3_BUCKET` | S3 server bucket | None | +| `PN_BOSS_CONFIG_S3_ACCESS_KEY` | S3 access key | None | +| `PN_BOSS_CONFIG_S3_ACCESS_SECRET` | S3 access key secret | None | +| `PN_BOSS_CONFIG_CDN_DISK_PATH` | Storage path for the CDN, use as alternative for S3 | None | +| `PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED` | Should Streetpass Relay be enabled? | `false` | +| `PN_BOSS_CONFIG_STREETPASS_RELAY_CLEAN_OLD_DATA` | Should old Streetpass Relay data be automatically cleaned up? | `false` | +| `PN_BOSS_CONFIG_DOMAINS_NPDI` | What domain should the NPDI component use? | `npdi.cdn.pretendo.cc` | +| `PN_BOSS_CONFIG_DOMAINS_NPDL` | What domain should the NPDL component use? | `npdl.cdn.pretendo.cc` | +| `PN_BOSS_CONFIG_DOMAINS_NPFL` | What domain should the NPFL component use? | `npfl.c.app.pretendo.cc` | +| `PN_BOSS_CONFIG_DOMAINS_NPPL` | What domain should the NPPL component use? | `nppl.app.pretendo.cc,nppl.c.app.pretendo.cc` | +| `PN_BOSS_CONFIG_DOMAINS_NPTS` | What domain should the NPTS component use? | `npts.app.pretendo.cc` | +| `PN_BOSS_CONFIG_DOMAINS_SPR` | What domain should the SPR component use? | `service.spr.app.pretendo.cc` | + ## S3 server The S3 server is optional, you can set `PN_BOSS_CONFIG_CDN_DISK_PATH` if you want to use a local folder as CDN source instead. @@ -61,7 +63,7 @@ npm run build Configurations are loaded through environment variables. `.env` files are supported. | Environment variable | Description | | -| --------------------------- | ------------------------------------------------------------------------------------------- | -------- | +|-----------------------------|---------------------------------------------------------------------------------------------|----------| | `PN_BOSS_CLI_GRPC_HOST` | The Host that the BOSS GRPC server is on. Example: `localhost:5678` | Required | | `PN_BOSS_CLI_GRPC_APIKEY` | Master API key of the BOSS GRPC server. | Required | | `PN_BOSS_CLI_WIIU_AES_KEY` | The BOSS WiiU AES key, needs to be dumped from a console | Optional | diff --git a/package-lock.json b/package-lock.json index 6c229dc..beddf7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "license": "AGPL-3.0-only", "dependencies": { "@aws-sdk/client-s3": "^3.723.0", - "@pretendonetwork/boss-crypto": "^1.0.0", - "@pretendonetwork/grpc": "^1.0.6", - "@typegoose/auto-increment": "^4.13.0", + "@pretendonetwork/boss-crypto": "^1.2.2", + "@pretendonetwork/grpc": "^2.3.5", + "@typegoose/auto-increment": "^4.13.1", "commander": "^14.0.0", "cron": "^4.3.3", "dotenv": "^16.4.7", @@ -358,6 +358,7 @@ "version": "3.723.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.723.0.tgz", "integrity": "sha512-9IH90m4bnHogBctVna2FnXaIGVORncfdxcqeEIovOxjIJJyHDmEAtA7B91dAM4sruddTbVzOYnqfPVst3odCbA==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -410,6 +411,7 @@ "version": "3.723.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.723.0.tgz", "integrity": "sha512-YyN8x4MI/jMb4LpHsLf+VYqvbColMK8aZeGWVk2fTFsmt8lpTYGaGC1yybSwGX42mZ4W8ucu8SAYSbUraJZEjA==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -917,6 +919,12 @@ "node": ">=18.0.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz", + "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -2022,6 +2030,12 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2033,9 +2047,10 @@ } }, "node_modules/@pretendonetwork/boss-crypto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pretendonetwork/boss-crypto/-/boss-crypto-1.0.0.tgz", - "integrity": "sha512-ybd3sB356v5Azxj99R62+7kytgAzfUYuXRJbdOznGL6infgCJ056TjTadN4V48m7t+3f6sPOUgo9YWUFNxlLLg==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@pretendonetwork/boss-crypto/-/boss-crypto-1.2.2.tgz", + "integrity": "sha512-sGlMiXGIThWfbs85xdWuJdgmW6YV6aQ8znh3vWWwG08BwpzzqgflluFYKhy6P0rpiMUJQuDAWK1IcVIsjD1lhw==", + "license": "LGPL-3.0-only" }, "node_modules/@pretendonetwork/eslint-config": { "version": "0.1.1", @@ -2057,12 +2072,14 @@ } }, "node_modules/@pretendonetwork/grpc": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-1.0.6.tgz", - "integrity": "sha512-kTK4lO8AdrQ5GOvYdJ7sqvIP3ubn5TGqGGqjVpgCTSiVBvBmlnz3fQkoDHmYw2WeA0CNtUx2dROG3Juiy5t7BQ==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.3.5.tgz", + "integrity": "sha512-FU0uvhZr8gFgiIi+gBtD6+5or34bHcd9X+ZVpRdc7IiupW5V+uxiXigJKX4Vd46QC412y+BEGofCjM1bBlTJhg==", + "license": "AGPL-3.0-only", "dependencies": { - "long": "^5.2.1", - "protobufjs": "^7.2.3" + "@bufbuild/protobuf": "^2.2.2", + "nice-grpc-common": "^2.0.2", + "typescript": "^5.7.2" } }, "node_modules/@protobufjs/aspromise": { @@ -3146,9 +3163,9 @@ } }, "node_modules/@typegoose/auto-increment": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@typegoose/auto-increment/-/auto-increment-4.13.0.tgz", - "integrity": "sha512-saOwqB66duV+rntkME/027A8opjgzmV3pBY8+zoJ4mGSc3FVGad6CSr56x4oqd15p39XtWH1UNZaS5Bzp6O6Ow==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@typegoose/auto-increment/-/auto-increment-4.13.1.tgz", + "integrity": "sha512-Dv0jlgBo4GkAApZmSxNGhD6eF3LP+1veQGvmNG2/tCNJvCVdTBUlC8ZNdUdTEzQlRRVq5p53qJH18irc5sX2jA==", "license": "MIT", "dependencies": { "loglevel": "^1.9.2", @@ -3374,6 +3391,7 @@ "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", @@ -3987,6 +4005,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5030,6 +5049,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -5097,6 +5117,7 @@ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5307,6 +5328,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5807,15 +5829,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -7388,6 +7401,7 @@ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.1.tgz", "integrity": "sha512-K0RfrUXXufqNRZZjvAGdyjydB91SnbWxlwFYi5t7zN2DxVWFD3c6puia0/7xfBwZm6RCpYOVdYFlRFpoDWiC+w==", "license": "MIT", + "peer": true, "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", @@ -7818,13 +7832,13 @@ } }, "node_modules/pino": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.1.tgz", - "integrity": "sha512-40SszWPOPwGhUIJ3zj0PsbMNV1bfg8nw5Qp/tP2FE2p3EuycmhDeYimKOMBAu6rtxcSw2QpjJsuK5A6v+en8Yw==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", "license": "MIT", "dependencies": { + "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", @@ -9133,6 +9147,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9467,7 +9482,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9564,6 +9579,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -10170,6 +10186,7 @@ "version": "3.723.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.723.0.tgz", "integrity": "sha512-9IH90m4bnHogBctVna2FnXaIGVORncfdxcqeEIovOxjIJJyHDmEAtA7B91dAM4sruddTbVzOYnqfPVst3odCbA==", + "peer": true, "requires": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -10216,6 +10233,7 @@ "version": "3.723.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.723.0.tgz", "integrity": "sha512-YyN8x4MI/jMb4LpHsLf+VYqvbColMK8aZeGWVk2fTFsmt8lpTYGaGC1yybSwGX42mZ4W8ucu8SAYSbUraJZEjA==", + "peer": true, "requires": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -10622,6 +10640,11 @@ "tslib": "^2.6.2" } }, + "@bufbuild/protobuf": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz", + "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==" + }, "@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -11263,6 +11286,11 @@ "@noble/hashes": "^1.1.5" } }, + "@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -11271,9 +11299,9 @@ "optional": true }, "@pretendonetwork/boss-crypto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pretendonetwork/boss-crypto/-/boss-crypto-1.0.0.tgz", - "integrity": "sha512-ybd3sB356v5Azxj99R62+7kytgAzfUYuXRJbdOznGL6infgCJ056TjTadN4V48m7t+3f6sPOUgo9YWUFNxlLLg==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@pretendonetwork/boss-crypto/-/boss-crypto-1.2.2.tgz", + "integrity": "sha512-sGlMiXGIThWfbs85xdWuJdgmW6YV6aQ8znh3vWWwG08BwpzzqgflluFYKhy6P0rpiMUJQuDAWK1IcVIsjD1lhw==" }, "@pretendonetwork/eslint-config": { "version": "0.1.1", @@ -11292,12 +11320,13 @@ } }, "@pretendonetwork/grpc": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-1.0.6.tgz", - "integrity": "sha512-kTK4lO8AdrQ5GOvYdJ7sqvIP3ubn5TGqGGqjVpgCTSiVBvBmlnz3fQkoDHmYw2WeA0CNtUx2dROG3Juiy5t7BQ==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.3.5.tgz", + "integrity": "sha512-FU0uvhZr8gFgiIi+gBtD6+5or34bHcd9X+ZVpRdc7IiupW5V+uxiXigJKX4Vd46QC412y+BEGofCjM1bBlTJhg==", "requires": { - "long": "^5.2.1", - "protobufjs": "^7.2.3" + "@bufbuild/protobuf": "^2.2.2", + "nice-grpc-common": "^2.0.2", + "typescript": "^5.7.2" } }, "@protobufjs/aspromise": { @@ -12063,9 +12092,9 @@ } }, "@typegoose/auto-increment": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@typegoose/auto-increment/-/auto-increment-4.13.0.tgz", - "integrity": "sha512-saOwqB66duV+rntkME/027A8opjgzmV3pBY8+zoJ4mGSc3FVGad6CSr56x4oqd15p39XtWH1UNZaS5Bzp6O6Ow==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@typegoose/auto-increment/-/auto-increment-4.13.1.tgz", + "integrity": "sha512-Dv0jlgBo4GkAApZmSxNGhD6eF3LP+1veQGvmNG2/tCNJvCVdTBUlC8ZNdUdTEzQlRRVq5p53qJH18irc5sX2jA==", "requires": { "loglevel": "^1.9.2", "tslib": "^2.8.1" @@ -12261,6 +12290,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", @@ -12601,7 +12631,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -13325,6 +13356,7 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, + "peer": true, "requires": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", @@ -13375,6 +13407,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -13556,6 +13589,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "peer": true, "requires": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -13877,11 +13911,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" - }, "fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -14894,6 +14923,7 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.1.tgz", "integrity": "sha512-K0RfrUXXufqNRZZjvAGdyjydB91SnbWxlwFYi5t7zN2DxVWFD3c6puia0/7xfBwZm6RCpYOVdYFlRFpoDWiC+w==", + "peer": true, "requires": { "bson": "^6.10.4", "kareem": "2.6.3", @@ -15191,12 +15221,12 @@ "dev": true }, "pino": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.1.tgz", - "integrity": "sha512-40SszWPOPwGhUIJ3zj0PsbMNV1bfg8nw5Qp/tP2FE2p3EuycmhDeYimKOMBAu6rtxcSw2QpjJsuK5A6v+en8Yw==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", "requires": { + "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", @@ -16075,7 +16105,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true + "dev": true, + "peer": true } } }, @@ -16296,7 +16327,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true + "peer": true }, "typescript-eslint": { "version": "8.39.1", @@ -16353,6 +16384,7 @@ "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, + "peer": true, "requires": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", diff --git a/package.json b/package.json index 58cabfd..441bd4a 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.723.0", - "@pretendonetwork/boss-crypto": "^1.0.0", - "@pretendonetwork/grpc": "^1.0.6", - "@typegoose/auto-increment": "^4.13.0", + "@pretendonetwork/boss-crypto": "^1.2.2", + "@pretendonetwork/grpc": "^2.3.5", + "@typegoose/auto-increment": "^4.13.1", "commander": "^14.0.0", "cron": "^4.3.3", "dotenv": "^16.4.7", diff --git a/src/cli/apps.cmd.ts b/src/cli/apps.cmd.ts index 365ce72..0c61434 100644 --- a/src/cli/apps.cmd.ts +++ b/src/cli/apps.cmd.ts @@ -20,6 +20,11 @@ const listCmd = new Command('ls') if (key === 'name') { return prettyTrunc(value, 20); } + + if (key === 'titleId') { + return value.toString(16).toLowerCase().padStart(16, '0'); + } + return value; } }); @@ -41,7 +46,7 @@ const viewCmd = new Command('view') const obj = { appId: app.bossAppId, name: app.name, - titleId: app.titleId, + titleId: app.titleId.toString(16).toLowerCase().padStart(16, '0'), titleRegion: app.titleRegion, knownTasks: app.tasks }; diff --git a/src/cli/cli.ts b/src/cli/cli.ts index b2a3ec9..c950529 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,6 +1,7 @@ import { program as baseProgram } from 'commander'; import { taskCmd } from './tasks.cmd'; import { fileCmd } from './files.cmd'; +import { file3DSCmd } from './files-3ds.cmd'; import { appCmd } from './apps.cmd'; import { importCmd } from './import.cmd'; @@ -11,7 +12,8 @@ const program = baseProgram .addCommand(appCmd) .addCommand(taskCmd) .addCommand(importCmd) - .addCommand(fileCmd); + .addCommand(fileCmd) + .addCommand(file3DSCmd); program.parseAsync(process.argv) .catch(console.error) diff --git a/src/cli/files-3ds.cmd.ts b/src/cli/files-3ds.cmd.ts new file mode 100644 index 0000000..efcb52b --- /dev/null +++ b/src/cli/files-3ds.cmd.ts @@ -0,0 +1,205 @@ +import fs from 'node:fs/promises'; +import { pipeline } from 'node:stream/promises'; +import { Readable } from 'node:stream'; +import { request } from 'undici'; +import { Command } from 'commander'; +import { decrypt3DS } from '@pretendonetwork/boss-crypto'; +import { PlatformType } from '@pretendonetwork/grpc/boss/v2/platform_type'; +import { commandHandler, getCliContext } from './utils'; +import { logOutputList, logOutputObject } from './output'; + +const listCmd = new Command('ls') + .description('List all task files in BOSS') + .argument('', 'BOSS app to search in') + .argument('', 'Task to search in') + .option('-c, --country [country]', 'Country to filter with') + .option('-l, --language [language]', 'Language to filter with') + .option('-a, --any', 'Shows any file regardless of country and language requirements') + .action(commandHandler<[string, string]>(async (cmd): Promise => { + const [appId, taskId] = cmd.args; + const opts = cmd.opts<{ country?: string; language?: string; any: boolean }>(); + const ctx = getCliContext(); + const { files } = await ctx.grpc.listFilesCTR({ + bossAppId: appId, + taskId: taskId, + country: opts.country, + language: opts.language, + any: opts.any + }); + logOutputList(files, { + format: cmd.format, + onlyIncludeKeys: ['dataId', 'name', 'size'], + mapping: { + dataId: 'Data ID', + name: 'Name', + size: 'Size (bytes)' + } + }); + })); + +const viewCmd = new Command('view') + .description('Look up a specific task file') + .argument('', 'BOSS app that contains the task') + .argument('', 'Task that contains the task file') + .argument('', 'Task file ID to lookup', BigInt) + .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { + const [appId, taskId, dataId] = cmd.args; + const ctx = getCliContext(); + const { files } = await ctx.grpc.listFilesCTR({ + bossAppId: appId, + taskId: taskId, + any: true + }); + const file = files.find(v => v.dataId === dataId); + if (!file) { + console.log(`Could not find task file with data ID ${dataId} in task ${taskId}`); + return; + } + logOutputObject({ + dataId: file.dataId, + name: file.name, + size: file.size, + hash: file.hash, + supportedCountries: file.supportedCountries, + supportedLanguages: file.supportedLanguages, + attributes: file.attributes, + creatorPid: file.creatorPid, + payloadContents: file.payloadContents, + flags: file.flags, + createdAt: new Date(Number(file.createdTimestamp)), + updatedAt: new Date(Number(file.updatedTimestamp)) + }, { + format: cmd.format + }); + })); + +const downloadCmd = new Command('download') + .description('Download a task file') + .argument('', 'BOSS app that contains the task') + .argument('', 'Task that contains the task file') + .argument('', 'Task file ID to lookup', BigInt) + .option('-d, --decrypt', 'Decrypt the file before return') + .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { + const [appId, taskId, dataId] = cmd.args; + const ctx = getCliContext(); + const { files } = await ctx.grpc.listFilesCTR({ + bossAppId: appId, + taskId: taskId, + any: true + }); + const file = files.find(v => v.dataId === dataId); + if (!file) { + console.error(`Could not find task file with data ID ${dataId} in task ${taskId}`); + process.exit(1); + } + + const npdl = ctx.getNpdlUrl(); + const country = file.supportedCountries.length > 0 ? '/' + file.supportedCountries[0] : ''; + const language = file.supportedLanguages.length > 0 ? '/' + file.supportedLanguages[0] : ''; + const { body, statusCode } = await request(`${npdl.url}/p01/nsa/${file.bossAppId}/${file.taskId}${country}${language}/${file.name}`, { + headers: { + Host: npdl.host + } + }); + if (statusCode > 299) { + console.error(`Failed to download: invalid status code (${statusCode})`); + process.exit(1); + } + + const chunks: Buffer[] = []; + for await (const chunk of body) { + chunks.push(Buffer.from(chunk)); + } + + let buffer: Buffer = Buffer.concat(chunks); + + if (cmd.opts().decrypt) { + const keys = ctx.get3DSKeys(); + const decrypted = decrypt3DS(buffer, keys.aesKey); + // TODO - Handle multiple payloads + buffer = decrypted.payload_contents[0]?.content ?? Buffer.alloc(0); + } + + await pipeline(Readable.from(buffer), process.stdout); + })); + +const createCmd = new Command('create') + .description('Create a new task file') + .argument('', 'BOSS app to store the task file in') + .argument('', 'Task to store the task file in') + .requiredOption('--name ', 'Name of the task file') + .requiredOption('--title-id ', 'Target title ID of the payload') + .requiredOption('--content-datatype ', 'Content datatype of the payload') + .requiredOption('--ns-data-id ', 'NS Data ID of the payload') + .requiredOption('--version ', 'Version of the payload') + .requiredOption('--file ', 'Path of the file to upload') + .option('--country ', 'Countries for this task file') + .option('--lang ', 'Languages for this task file') + .option('--attribute1 [attribute1]', 'Attribute 1 for this task file') + .option('--attribute2 [attribute2]', 'Attribute 2 for this task file') + .option('--attribute3 [attribute3]', 'Attribute 3 for this task file') + .option('--desc [desc]', 'Description for this task file') + .option('-m, --mark-arrived-privileged', 'Only notify of new content to privileged titles') + .option('-n, --no-payload', 'Make this task file have no payload contents') + .action(commandHandler<[string, string]>(async (cmd): Promise => { + const [appId, taskId] = cmd.args; + // TODO - Handle multiple payload contents + const opts = cmd.opts<{ name: string; titleId: string; contentDatatype: string; nsDataId: string; version: string; file: string; country: string[]; lang: string[]; attribute1?: string; attribute2?: string; attribute3?: string; desc?: string; markArrivedPrivileged: boolean; payload: boolean }>(); + const fileBuf = opts.payload ? await fs.readFile(opts.file) : Buffer.alloc(0); + const ctx = getCliContext(); + const { file } = await ctx.grpc.uploadFileCTR({ + taskId: taskId, + bossAppId: appId, + supportedCountries: opts.country, + supportedLanguages: opts.lang, + attributes: { + attribute1: opts.attribute1, + attribute2: opts.attribute2, + attribute3: opts.attribute3, + description: opts.desc + }, + name: opts.name, + payloadContents: opts.payload + ? [{ + titleId: BigInt(parseInt(opts.titleId, 16)), + contentDatatype: Number(opts.contentDatatype), + nsDataId: Number(opts.nsDataId), + version: Number(opts.version), + content: fileBuf + }] + : [], + flags: { + markArrivedPrivileged: opts.markArrivedPrivileged + } + }); + if (!file) { + console.log(`Failed to create file!`); + return; + } + console.log(`Created file with ID ${file.dataId}`); + })); + +const deleteCmd = new Command('delete') + .description('Delete a task file') + .argument('', 'BOSS app that contains the task') + .argument('', 'Task that contains the task file') + .argument('', 'Task file ID to delete', BigInt) + .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- I want to use destructuring + const [appId, _taskId, dataId] = cmd.args; + const ctx = getCliContext(); + await ctx.grpc.deleteFile({ + bossAppId: appId, + dataId: dataId, + platformType: PlatformType.PLATFORM_TYPE_CTR + }); + console.log(`Deleted task file with ID ${dataId}`); + })); + +export const file3DSCmd = new Command('file-3ds') + .description('Manage all the 3DS task files in BOSS') + .addCommand(listCmd) + .addCommand(createCmd) + .addCommand(deleteCmd) + .addCommand(downloadCmd) + .addCommand(viewCmd); diff --git a/src/cli/files.cmd.ts b/src/cli/files.cmd.ts index 5e51b0f..6f6ea23 100644 --- a/src/cli/files.cmd.ts +++ b/src/cli/files.cmd.ts @@ -4,6 +4,7 @@ import { Readable } from 'node:stream'; import { request } from 'undici'; import { Command } from 'commander'; import { decryptWiiU } from '@pretendonetwork/boss-crypto'; +import { PlatformType } from '@pretendonetwork/grpc/boss/v2/platform_type'; import { commandHandler, getCliContext } from './utils'; import { logOutputList, logOutputObject } from './output'; @@ -11,12 +12,19 @@ const listCmd = new Command('ls') .description('List all task files in BOSS') .argument('', 'BOSS app to search in') .argument('', 'Task to search in') + .option('-c, --country [country]', 'Country to filter with') + .option('-l, --language [language]', 'Language to filter with') + .option('-a, --any', 'Shows any file regardless of country and language requirements') .action(commandHandler<[string, string]>(async (cmd): Promise => { const [appId, taskId] = cmd.args; + const opts = cmd.opts<{ country?: string; language?: string; any: boolean }>(); const ctx = getCliContext(); - const { files } = await ctx.grpc.listFiles({ + const { files } = await ctx.grpc.listFilesWUP({ bossAppId: appId, - taskId: taskId + taskId: taskId, + country: opts.country, + language: opts.language, + any: opts.any }); logOutputList(files, { format: cmd.format, @@ -38,9 +46,10 @@ const viewCmd = new Command('view') .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { const [appId, taskId, dataId] = cmd.args; const ctx = getCliContext(); - const { files } = await ctx.grpc.listFiles({ + const { files } = await ctx.grpc.listFilesWUP({ bossAppId: appId, - taskId: taskId + taskId: taskId, + any: true }); const file = files.find(v => v.dataId === dataId); if (!file) { @@ -76,9 +85,10 @@ const downloadCmd = new Command('download') .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { const [appId, taskId, dataId] = cmd.args; const ctx = getCliContext(); - const { files } = await ctx.grpc.listFiles({ + const { files } = await ctx.grpc.listFilesWUP({ bossAppId: appId, - taskId: taskId + taskId: taskId, + any: true }); const file = files.find(v => v.dataId === dataId); if (!file) { @@ -122,20 +132,30 @@ const createCmd = new Command('create') .requiredOption('--file ', 'Path of the file to upload') .option('--country ', 'Countries for this task file') .option('--lang ', 'Languages for this task file') + .option('--attribute1 [attribute1]', 'Attribute 1 for this task file') + .option('--attribute2 [attribute2]', 'Attribute 2 for this task file') + .option('--attribute3 [attribute3]', 'Attribute 3 for this task file') + .option('--desc [desc]', 'Description for this task file') .option('--name-as-id', 'Force the name as the data ID') .option('--notify-new ', 'Add entry to NotifyNew') .option('--notify-led', 'Enable NotifyLED') .action(commandHandler<[string, string]>(async (cmd): Promise => { const [appId, taskId] = cmd.args; - const opts = cmd.opts<{ name: string; country: string[]; notifyNew: string[]; notifyLed: boolean; lang: string[]; nameAsId?: boolean; type: string; file: string }>(); + const opts = cmd.opts<{ name: string; country: string[]; notifyNew: string[]; notifyLed: boolean; lang: string[]; attribute1?: string; attribute2?: string; attribute3?: string; desc?: string; nameAsId?: boolean; type: string; file: string }>(); const fileBuf = await fs.readFile(opts.file); const ctx = getCliContext(); - const { file } = await ctx.grpc.uploadFile({ + const { file } = await ctx.grpc.uploadFileWUP({ taskId: taskId, bossAppId: appId, name: opts.name, supportedCountries: opts.country, supportedLanguages: opts.lang, + attributes: { + attribute1: opts.attribute1, + attribute2: opts.attribute2, + attribute3: opts.attribute3, + description: opts.desc + }, type: opts.type, nameEqualsDataId: opts.nameAsId ?? false, data: fileBuf, @@ -160,13 +180,14 @@ const deleteCmd = new Command('delete') const ctx = getCliContext(); await ctx.grpc.deleteFile({ bossAppId: appId, - dataId: dataId + dataId: dataId, + platformType: PlatformType.PLATFORM_TYPE_WUP }); console.log(`Deleted task file with ID ${dataId}`); })); export const fileCmd = new Command('file') - .description('Manage all the task files in BOSS') + .description('Manage all the Wii U task files in BOSS') .addCommand(listCmd) .addCommand(createCmd) .addCommand(deleteCmd) diff --git a/src/cli/seed.cmd.ts b/src/cli/seed.cmd.ts index 4b1d929..967e0d9 100644 --- a/src/cli/seed.cmd.ts +++ b/src/cli/seed.cmd.ts @@ -3,6 +3,7 @@ import fs from 'fs/promises'; import { Command } from 'commander'; import { xml2js } from 'xml-js'; import { decryptWiiU } from '@pretendonetwork/boss-crypto'; +import { PlatformType } from '@pretendonetwork/grpc/boss/v2/platform_type'; import { getCliContext } from './utils'; import { seedFolder } from './root'; import type { CliContext } from './utils'; @@ -31,7 +32,7 @@ export async function uploadFileIfChanged(ops: UploadFileOptions): Promise fileContents = decryptedContents.content; } - const allExistingTaskFiles = await ops.ctx.grpc.listFiles({ + const allExistingTaskFiles = await ops.ctx.grpc.listFilesWUP({ bossAppId: ops.bossAppId, taskId: ops.taskId }); @@ -40,11 +41,12 @@ export async function uploadFileIfChanged(ops: UploadFileOptions): Promise console.warn(`${ops.dataId}: File already uploaded, reuploading`); await ops.ctx.grpc.deleteFile({ bossAppId: ops.bossAppId, - dataId: BigInt(ops.dataId) + dataId: BigInt(ops.dataId), + platformType: PlatformType.PLATFORM_TYPE_WUP }); } - await ops.ctx.grpc.uploadFile({ + await ops.ctx.grpc.uploadFileWUP({ bossAppId: ops.bossAppId, taskId: ops.taskId, name: ops.fileXml.Filename._text, @@ -78,20 +80,11 @@ export async function processTasksheet(ctx: CliContext, taskFiles: string[], fil await ctx.grpc.registerTask({ bossAppId: bossAppId, id: taskName, - titleId: xmlContents.TaskSheet.TitleId._text, - country: 'This value isnt used' + titleId: BigInt(parseInt(xmlContents.TaskSheet.TitleId._text, 16)), + status: xmlContents.TaskSheet.ServiceStatus._text }); console.log(`${filename}: Created task`); } - await ctx.grpc.updateTask({ - bossAppId: bossAppId, - id: taskName, - updateData: { - titleId: xmlContents.TaskSheet.TitleId._text, - status: xmlContents.TaskSheet.ServiceStatus._text - } - }); - console.log(`${filename}: Updated title ID and status`); let xmlFiles = xmlContents.TaskSheet.Files?.File ?? []; if (!Array.isArray(xmlFiles)) { diff --git a/src/cli/tasks.cmd.ts b/src/cli/tasks.cmd.ts index ef41894..da4c115 100644 --- a/src/cli/tasks.cmd.ts +++ b/src/cli/tasks.cmd.ts @@ -38,7 +38,7 @@ const viewCmd = new Command('view') taskId: task.id, inGameId: task.inGameId, description: task.description, - titleId: task.titleId, + titleId: task.titleId.toString(16).toLowerCase().padStart(16, '0'), bossAppId: task.bossAppId, creatorPid: task.creatorPid, status: task.status, @@ -54,17 +54,20 @@ const createCmd = new Command('create') .argument('', 'BOSS app to store the task in') .requiredOption('--id ', 'Id of the task') .requiredOption('--title-id ', 'Title ID for the task') + .option('--status [status]', 'Initial status of the task') + .option('--interval [interval]', 'Interval of the task') .option('--desc [desc]', 'Description of the task') .action(commandHandler<[string]>(async (cmd): Promise => { const [appId] = cmd.args; const ctx = getCliContext(); - const opts = cmd.opts<{ id: string; titleId: string; desc?: string }>(); + const opts = cmd.opts<{ id: string; titleId: string; status?: string; interval?: string; desc?: string }>(); const { task } = await ctx.grpc.registerTask({ bossAppId: appId, id: opts.id, - titleId: opts.titleId, - description: opts.desc ?? '', - country: 'This value isnt used' + titleId: BigInt(parseInt(opts.titleId, 16)), + status: opts.status ?? 'open', + interval: Number(opts.interval ?? 0), + description: opts.desc ?? '' }); if (!task) { console.log(`Failed to create task!`); diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 6d7ab63..5091855 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -1,20 +1,27 @@ -import { BOSSDefinition } from '@pretendonetwork/grpc/boss/boss_service'; +import { BossServiceDefinition } from '@pretendonetwork/grpc/boss/v2/boss_service'; import { createChannel, createClient, Metadata } from 'nice-grpc'; import dotenv from 'dotenv'; -import type { BOSSClient } from '@pretendonetwork/grpc/boss/boss_service'; +import type { BossServiceClient } from '@pretendonetwork/grpc/boss/v2/boss_service'; import type { Command } from 'commander'; import type { FormatOption } from './output'; export type WiiUKeys = { aesKey: string; hmacKey: string }; +export type CtrKeys = { aesKey: string }; export type NpdiUrl = { host: string; url: string; }; +export type NpdlUrl = { + host: string; + url: string; +}; export type CliContext = { - grpc: BOSSClient; + grpc: BossServiceClient; getNpdiUrl: () => NpdiUrl; + getNpdlUrl: () => NpdlUrl; getWiiUKeys: () => WiiUKeys; + get3DSKeys: () => CtrKeys; }; export function getCliContext(): CliContext { @@ -30,7 +37,7 @@ export function getCliContext(): CliContext { } const channel = createChannel(grpcHost); - const client: BOSSClient = createClient(BOSSDefinition, channel, { + const client: BossServiceClient = createClient(BossServiceDefinition, channel, { '*': { metadata: new Metadata({ 'X-API-Key': grpcKey @@ -49,6 +56,15 @@ export function getCliContext(): CliContext { host: npdiHost }; }, + getNpdlUrl(): NpdiUrl { + const npdlUrl = process.env.PN_BOSS_CLI_NPDL_URL ?? 'https://npdl.cdn.pretendo.cc'; + const npdlHost = process.env.PN_BOSS_CLI_NPDL_HOST ?? new URL(npdlUrl).host; + + return { + url: npdlUrl, + host: npdlHost + }; + }, getWiiUKeys(): WiiUKeys { const aesKey = process.env.PN_BOSS_CLI_WIIU_AES_KEY ?? ''; const hmacKey = process.env.PN_BOSS_CLI_WIIU_HMAC_KEY ?? ''; @@ -63,6 +79,16 @@ export function getCliContext(): CliContext { aesKey, hmacKey }; + }, + get3DSKeys(): CtrKeys { + const aesKey = process.env.PN_BOSS_CLI_3DS_AES_KEY ?? ''; + + if (!aesKey) { + throw new Error('Missing env variable PN_BOSS_CLI_3DS_AES_KEY - needed for decryption'); + } + return { + aesKey + }; } }; } diff --git a/src/config-manager.ts b/src/config-manager.ts index c8e5e23..dc7342f 100644 --- a/src/config-manager.ts +++ b/src/config-manager.ts @@ -45,6 +45,8 @@ export const config = { } }, grpc: { + max_receive_message_length: Number(process.env.PN_BOSS_CONFIG_GRPC_MAX_RECEIVE_MESSAGE_LENGTH_MB?.trim() || '4'), + max_send_message_length: Number(process.env.PN_BOSS_CONFIG_GRPC_MAX_SEND_MESSAGE_LENGTH_MB?.trim() || '4'), boss: { address: process.env.PN_BOSS_CONFIG_GRPC_BOSS_SERVER_ADDRESS?.trim() || '', port: Number(process.env.PN_BOSS_CONFIG_GRPC_BOSS_SERVER_PORT?.trim() || ''), diff --git a/src/database.ts b/src/database.ts index 0c41186..fa8762e 100644 --- a/src/database.ts +++ b/src/database.ts @@ -2,12 +2,14 @@ import mongoose from 'mongoose'; import { CECData } from '@/models/cec-data'; import { CECSlot } from '@/models/cec-slot'; import { Task } from '@/models/task'; -import { File } from '@/models/file'; +import { FileCTR } from '@/models/file-ctr'; +import { FileWUP } from '@/models/file-wup'; import { config } from '@/config-manager'; import type { HydratedCECDataDocument } from '@/types/mongoose/cec-data'; import type { HydratedCECSlotDocument, ICECSlot } from '@/types/mongoose/cec-slot'; import type { HydratedTaskDocument, ITask } from '@/types/mongoose/task'; -import type { HydratedFileDocument, IFile } from '@/types/mongoose/file'; +import type { HydratedFileCTRDocument, IFileCTR } from '@/types/mongoose/file-ctr'; +import type { HydratedFileWUPDocument, IFileWUP } from '@/types/mongoose/file-wup'; const connection_string: string = config.mongoose.connection_string; @@ -52,10 +54,10 @@ export function getTask(bossAppID: string, taskID: string): Promise { +export function getCTRTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string, any: boolean = false): Promise { verifyConnected(); - const filter: mongoose.FilterQuery = { + const filter: mongoose.FilterQuery = { task_id: taskID.slice(0, 7), boss_app_id: bossAppID, $and: [] @@ -72,6 +74,10 @@ export function getTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: s { supported_countries: country } ] }); + } else if (!any) { + filter.$and?.push({ + supported_countries: { $eq: [] } + }); } if (language) { @@ -81,19 +87,23 @@ export function getTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: s { supported_languages: language } ] }); + } else if (!any) { + filter.$and?.push({ + supported_languages: { $eq: [] } + }); } if (filter.$and?.length === 0) { delete filter.$and; } - return File.find(filter); + return FileCTR.find(filter); } -export function getTaskFilesWithAttributes(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string, attribute1?: string, attribute2?: string, attribute3?: string): Promise { +export function getWUPTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string, any: boolean = false): Promise { verifyConnected(); - const filter: mongoose.FilterQuery = { + const filter: mongoose.FilterQuery = { task_id: taskID.slice(0, 7), boss_app_id: bossAppID, $and: [] @@ -110,6 +120,56 @@ export function getTaskFilesWithAttributes(allowDeleted: boolean, bossAppID: str { supported_countries: country } ] }); + } else if (!any) { + filter.$and?.push({ + supported_countries: { $eq: [] } + }); + } + + if (language) { + filter.$and?.push({ + $or: [ + { supported_languages: { $eq: [] } }, + { supported_languages: language } + ] + }); + } else if (!any) { + filter.$and?.push({ + supported_languages: { $eq: [] } + }); + } + + if (filter.$and?.length === 0) { + delete filter.$and; + } + + return FileWUP.find(filter); +} + +export function getCTRTaskFilesWithAttributes(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string, attribute1?: string, attribute2?: string, attribute3?: string): Promise { + verifyConnected(); + + const filter: mongoose.FilterQuery = { + task_id: taskID.slice(0, 7), + boss_app_id: bossAppID, + $and: [] + }; + + if (!allowDeleted) { + filter.deleted = false; + } + + if (country) { + filter.$and?.push({ + $or: [ + { supported_countries: { $eq: [] } }, + { supported_countries: country } + ] + }); + } else { + filter.$and?.push({ + supported_countries: { $eq: [] } + }); } if (language) { @@ -119,31 +179,35 @@ export function getTaskFilesWithAttributes(allowDeleted: boolean, bossAppID: str { supported_languages: language } ] }); + } else { + filter.$and?.push({ + supported_languages: { $eq: [] } + }); } if (attribute1) { - filter.attribute1 = attribute1; + filter.attributes.attribute1 = attribute1; } if (attribute2) { - filter.attribute2 = attribute2; + filter.attributes.attribute2 = attribute2; } if (attribute3) { - filter.attribute3 = attribute3; + filter.attributes.attribute3 = attribute3; } if (filter.$and?.length === 0) { delete filter.$and; } - return File.find(filter); + return FileCTR.find(filter); } -export function getTaskFile(bossAppID: string, taskID: string, name: string, country?: string, language?: string): Promise { +export function getCTRTaskFile(bossAppID: string, taskID: string, name: string, country?: string, language?: string): Promise { verifyConnected(); - const filter: mongoose.FilterQuery = { + const filter: mongoose.FilterQuery = { deleted: false, boss_app_id: bossAppID, task_id: taskID.slice(0, 7), @@ -158,6 +222,10 @@ export function getTaskFile(bossAppID: string, taskID: string, name: string, cou { supported_countries: country } ] }); + } else { + filter.$and?.push({ + supported_countries: { $eq: [] } + }); } if (language) { @@ -167,19 +235,68 @@ export function getTaskFile(bossAppID: string, taskID: string, name: string, cou { supported_languages: language } ] }); + } else { + filter.$and?.push({ + supported_languages: { $eq: [] } + }); } - if (filter.$and?.length === 0) { - delete filter.$and; + return FileCTR.findOne(filter); +} + +export function getWUPTaskFile(bossAppID: string, taskID: string, name: string, country?: string, language?: string): Promise { + verifyConnected(); + + const filter: mongoose.FilterQuery = { + deleted: false, + boss_app_id: bossAppID, + task_id: taskID.slice(0, 7), + name: name, + $and: [] + }; + + if (country) { + filter.$and?.push({ + $or: [ + { supported_countries: { $eq: [] } }, + { supported_countries: country } + ] + }); + } else { + filter.$and?.push({ + supported_countries: { $eq: [] } + }); + } + + if (language) { + filter.$and?.push({ + $or: [ + { supported_languages: { $eq: [] } }, + { supported_languages: language } + ] + }); + } else { + filter.$and?.push({ + supported_languages: { $eq: [] } + }); } - return File.findOne(filter); + return FileWUP.findOne(filter); +} + +export function getCTRTaskFileBySerialNumber(serialNumber: bigint): Promise { + verifyConnected(); + + return FileCTR.findOne({ + deleted: false, + serial_number: serialNumber + }); } -export function getTaskFileByDataID(dataID: bigint): Promise { +export function getWUPTaskFileByDataID(dataID: bigint): Promise { verifyConnected(); - return File.findOne({ + return FileWUP.findOne({ deleted: false, data_id: Number(dataID) }); diff --git a/src/models/file-ctr.ts b/src/models/file-ctr.ts new file mode 100644 index 0000000..1b3fa18 --- /dev/null +++ b/src/models/file-ctr.ts @@ -0,0 +1,48 @@ +import mongoose from 'mongoose'; +import { AutoIncrementID } from '@typegoose/auto-increment'; +import type { IFileCTR, IFileCTRMethods, FileCTRModel } from '@/types/mongoose/file-ctr'; + +const FileCTRSchema = new mongoose.Schema({ + deleted: { + type: Boolean, + default: false + }, + creator_pid: Number, + hash: String, + file_key: String, + size: BigInt, + task_id: String, + boss_app_id: String, + supported_countries: [String], + supported_languages: [String], + attributes: { + attribute1: String, + attribute2: String, + attribute3: String, + description: String + }, + name: String, + serial_number: BigInt, // * This is effectively the predecessor of the Wii U DataID + payload_contents: [{ + title_id: BigInt, + content_datatype: Number, + ns_data_id: Number, // * Should payload contents be put in their own collection with their own autoincrementing IDs? + version: Number, + size: Number + }], + flags: { + mark_arrived_privileged: Boolean + }, + created: BigInt, + updated: BigInt +}, { id: false }); + +FileCTRSchema.plugin(AutoIncrementID, { + startAt: 50000, // * Start very high to avoid conflicts with Nintendo Data IDs + field: 'serial_number' +}); + +FileCTRSchema.index({ task_id: 1, boss_app_id: 1 }); +FileCTRSchema.index({ task_id: 1, boss_app_id: 1, name: 1 }); + +export const FileCTR = mongoose.model('FileCTR', FileCTRSchema, 'files-ctr'); diff --git a/src/models/file-wup.ts b/src/models/file-wup.ts new file mode 100644 index 0000000..1494ba9 --- /dev/null +++ b/src/models/file-wup.ts @@ -0,0 +1,43 @@ +import mongoose from 'mongoose'; +import { AutoIncrementID } from '@typegoose/auto-increment'; +import type { IFileWUP, IFileWUPMethods, FileWUPModel } from '@/types/mongoose/file-wup'; + +const FileWUPSchema = new mongoose.Schema({ + deleted: { + type: Boolean, + default: false + }, + file_key: String, + data_id: BigInt, + task_id: String, + boss_app_id: String, + supported_countries: [String], + supported_languages: [String], + attributes: { + attribute1: String, + attribute2: String, + attribute3: String, + description: String + }, + creator_pid: Number, + name: String, + type: String, + hash: String, + size: BigInt, + notify_on_new: [String], + notify_led: Boolean, + condition_played: BigInt, + auto_delete: Boolean, // * We don't know what this does, but it exists on WUP tasks. So track it + created: BigInt, + updated: BigInt +}, { id: false }); + +FileWUPSchema.plugin(AutoIncrementID, { + startAt: 50000, // * Start very high to avoid conflicts with Nintendo Data IDs + field: 'data_id' +}); + +FileWUPSchema.index({ task_id: 1, boss_app_id: 1 }); +FileWUPSchema.index({ task_id: 1, boss_app_id: 1, name: 1 }); + +export const FileWUP = mongoose.model('FileWUP', FileWUPSchema, 'files-wup'); diff --git a/src/models/file.ts b/src/models/file.ts deleted file mode 100644 index d918cb8..0000000 --- a/src/models/file.ts +++ /dev/null @@ -1,39 +0,0 @@ -import mongoose from 'mongoose'; -import { AutoIncrementID } from '@typegoose/auto-increment'; -import type { IFile, IFileMethods, FileModel } from '@/types/mongoose/file'; - -const FileSchema = new mongoose.Schema({ - deleted: { - type: Boolean, - default: false - }, - file_key: String, - data_id: Number, // TODO - Wait until https://github.com/typegoose/auto-increment/pull/21 is merged and then change this to BigInt - task_id: String, - boss_app_id: String, - supported_countries: [String], - supported_languages: [String], - password: String, - attribute1: String, - attribute2: String, - attribute3: String, - creator_pid: Number, - name: String, - type: String, - hash: String, - size: BigInt, - notify_on_new: [String], - notify_led: Boolean, - created: BigInt, - updated: BigInt -}, { id: false }); - -FileSchema.plugin(AutoIncrementID, { - startAt: 50000, // * Start very high to avoid conflicts with Nintendo Data IDs - field: 'data_id' -}); - -FileSchema.index({ task_id: 1, boss_app_id: 1 }); -FileSchema.index({ task_id: 1, boss_app_id: 1, name: 1 }); - -export const File = mongoose.model('File', FileSchema); diff --git a/src/models/task.ts b/src/models/task.ts index cb374b1..21f4c72 100644 --- a/src/models/task.ts +++ b/src/models/task.ts @@ -15,6 +15,7 @@ const TaskSchema = new mongoose.Schema({ required: true, enum: ['open', 'close'] }, + interval: Number, title_id: String, description: String, created: BigInt, diff --git a/src/services/grpc/boss/implementation.ts b/src/services/grpc/boss/implementation.ts deleted file mode 100644 index c674db7..0000000 --- a/src/services/grpc/boss/implementation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { listKnownBOSSApps } from '@/services/grpc/boss/list-known-boss-apps'; -import { listTasks } from '@/services/grpc/boss/list-tasks'; -import { registerTask } from '@/services/grpc/boss/register-task'; -import { updateTask } from '@/services/grpc/boss/update-task'; -import { deleteTask } from '@/services/grpc/boss/delete-task'; -import { listFiles } from '@/services/grpc/boss/list-files'; -import { uploadFile } from '@/services/grpc/boss/upload-file'; -import { updateFileMetadata } from '@/services/grpc/boss/update-file-metadata'; -import { deleteFile } from '@/services/grpc/boss/delete-file'; -import type { BOSSServiceImplementation } from '@pretendonetwork/grpc/boss/boss_service'; - -export const implementation: BOSSServiceImplementation = { - listKnownBOSSApps, - listTasks, - registerTask, - updateTask, - deleteTask, - listFiles, - uploadFile, - updateFileMetadata, - deleteFile -}; diff --git a/src/services/grpc/boss/delete-file.ts b/src/services/grpc/boss/v1/delete-file.ts similarity index 74% rename from src/services/grpc/boss/delete-file.ts rename to src/services/grpc/boss/v1/delete-file.ts index 518acb5..0947aad 100644 --- a/src/services/grpc/boss/delete-file.ts +++ b/src/services/grpc/boss/v1/delete-file.ts @@ -1,10 +1,10 @@ import { Status, ServerError } from 'nice-grpc'; -import { getTaskFileByDataID } from '@/database'; -import { hasPermission } from '@/services/grpc/boss/middleware/authentication-middleware'; -import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware'; +import { getWUPTaskFileByDataID } from '@/database'; +import { hasPermission } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; import type { CallContext } from 'nice-grpc'; import type { DeleteFileRequest } from '@pretendonetwork/grpc/boss/delete_file'; -import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; export async function deleteFile(request: DeleteFileRequest, context: CallContext & AuthenticationCallContextExt): Promise { if (!hasPermission(context, 'deleteBossFiles')) { @@ -18,7 +18,7 @@ export async function deleteFile(request: DeleteFileRequest, context: CallContex throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file data ID'); } - const file = await getTaskFileByDataID(dataID); + const file = await getWUPTaskFileByDataID(dataID); if (!file || file.boss_app_id !== bossAppID) { throw new ServerError(Status.INVALID_ARGUMENT, `File ${dataID} not found for BOSS app ${bossAppID}`); diff --git a/src/services/grpc/boss/delete-task.ts b/src/services/grpc/boss/v1/delete-task.ts similarity index 83% rename from src/services/grpc/boss/delete-task.ts rename to src/services/grpc/boss/v1/delete-task.ts index 8df48f9..4d850e3 100644 --- a/src/services/grpc/boss/delete-task.ts +++ b/src/services/grpc/boss/v1/delete-task.ts @@ -1,10 +1,10 @@ import { Status, ServerError } from 'nice-grpc'; import { getTask } from '@/database'; -import { hasPermission } from '@/services/grpc/boss/middleware/authentication-middleware'; -import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware'; +import { hasPermission } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; import type { CallContext } from 'nice-grpc'; import type { DeleteTaskRequest } from '@pretendonetwork/grpc/boss/delete_task'; -import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; export async function deleteTask(request: DeleteTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise { if (!hasPermission(context, 'deleteBossTasks')) { diff --git a/src/services/grpc/boss/v1/implementation.ts b/src/services/grpc/boss/v1/implementation.ts new file mode 100644 index 0000000..fa09885 --- /dev/null +++ b/src/services/grpc/boss/v1/implementation.ts @@ -0,0 +1,22 @@ +import { listKnownBOSSApps } from '@/services/grpc/boss/v1/list-known-boss-apps'; +import { listTasks } from '@/services/grpc/boss/v1/list-tasks'; +import { registerTask } from '@/services/grpc/boss/v1/register-task'; +import { updateTask } from '@/services/grpc/boss/v1/update-task'; +import { deleteTask } from '@/services/grpc/boss/v1/delete-task'; +import { listFiles } from '@/services/grpc/boss/v1/list-files'; +import { uploadFile } from '@/services/grpc/boss/v1/upload-file'; +import { updateFileMetadata } from '@/services/grpc/boss/v1/update-file-metadata'; +import { deleteFile } from '@/services/grpc/boss/v1/delete-file'; +import type { BOSSServiceImplementation } from '@pretendonetwork/grpc/boss/boss_service'; + +export const bossServiceImplementationV1: BOSSServiceImplementation = { + listKnownBOSSApps, + listTasks, + registerTask, + updateTask, + deleteTask, + listFiles, + uploadFile, + updateFileMetadata, + deleteFile +}; diff --git a/src/services/grpc/boss/list-files.ts b/src/services/grpc/boss/v1/list-files.ts similarity index 85% rename from src/services/grpc/boss/list-files.ts rename to src/services/grpc/boss/v1/list-files.ts index 112caa9..e8a9ba7 100644 --- a/src/services/grpc/boss/list-files.ts +++ b/src/services/grpc/boss/v1/list-files.ts @@ -1,6 +1,6 @@ import { Status, ServerError } from 'nice-grpc'; import { isValidCountryCode, isValidLanguage } from '@/util'; -import { getTaskFiles } from '@/database'; +import { getWUPTaskFiles } from '@/database'; import type { ListFilesRequest, ListFilesResponse } from '@pretendonetwork/grpc/boss/list_files'; const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/; @@ -35,7 +35,7 @@ export async function listFiles(request: ListFilesRequest): Promise ({ @@ -45,10 +45,10 @@ export async function listFiles(request: ListFilesRequest): Promise { if (!hasPermission(context, 'updateBossFiles')) { @@ -23,7 +23,7 @@ export async function updateFileMetadata(request: UpdateFileMetadataRequest, con throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file update data'); } - const file = await getTaskFileByDataID(dataID); + const file = await getWUPTaskFileByDataID(dataID); if (!file || file.deleted) { throw new ServerError(Status.INVALID_ARGUMENT, `File ${dataID} not found`); @@ -43,10 +43,10 @@ export async function updateFileMetadata(request: UpdateFileMetadataRequest, con file.boss_app_id = updateData.bossAppId; file.supported_countries = updateData.supportedCountries; file.supported_languages = updateData.supportedLanguages; - file.password = updateData.password; - file.attribute1 = updateData.attribute1; - file.attribute2 = updateData.attribute2; - file.attribute3 = updateData.attribute3; + file.attributes.description = updateData.password; + file.attributes.attribute1 = updateData.attribute1; + file.attributes.attribute2 = updateData.attribute2; + file.attributes.attribute3 = updateData.attribute3; file.name = updateData.name; file.type = updateData.type; file.notify_on_new = updateData.notifyOnNew; diff --git a/src/services/grpc/boss/update-task.ts b/src/services/grpc/boss/v1/update-task.ts similarity index 88% rename from src/services/grpc/boss/update-task.ts rename to src/services/grpc/boss/v1/update-task.ts index f18a2a1..f8c3c41 100644 --- a/src/services/grpc/boss/update-task.ts +++ b/src/services/grpc/boss/v1/update-task.ts @@ -1,10 +1,10 @@ import { Status, ServerError } from 'nice-grpc'; import { getTask } from '@/database'; -import { hasPermission } from '@/services/grpc/boss/middleware/authentication-middleware'; -import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware'; +import { hasPermission } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; import type { CallContext } from 'nice-grpc'; import type { UpdateTaskRequest } from '@pretendonetwork/grpc/boss/update_task'; -import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; export async function updateTask(request: UpdateTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise { if (!hasPermission(context, 'updateBossTasks')) { @@ -41,6 +41,7 @@ export async function updateTask(request: UpdateTaskRequest, context: CallContex task.id = updateData.id.slice(0, 7); task.in_game_id = updateData.id; } + task.boss_app_id = updateData.bossAppId ? updateData.bossAppId : task.boss_app_id; task.title_id = updateData.titleId ? updateData.titleId : task.title_id; task.status = updateData.status ? updateData.status : task.status; diff --git a/src/services/grpc/boss/upload-file.ts b/src/services/grpc/boss/v1/upload-file.ts similarity index 88% rename from src/services/grpc/boss/upload-file.ts rename to src/services/grpc/boss/v1/upload-file.ts index 95d6dd2..a83e6b8 100644 --- a/src/services/grpc/boss/upload-file.ts +++ b/src/services/grpc/boss/v1/upload-file.ts @@ -1,12 +1,12 @@ import { Status, ServerError } from 'nice-grpc'; import { encryptWiiU } from '@pretendonetwork/boss-crypto'; import { isValidCountryCode, isValidFileNotifyCondition, isValidFileType, isValidLanguage, md5 } from '@/util'; -import { getTask, getTaskFile } from '@/database'; -import { File } from '@/models/file'; +import { getTask, getWUPTaskFile } from '@/database'; +import { FileWUP } from '@/models/file-wup'; import { config } from '@/config-manager'; import { uploadCDNFile } from '@/cdn'; -import { hasPermission } from '@/services/grpc/boss/middleware/authentication-middleware'; -import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware'; +import { hasPermission } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; import type { CallContext } from 'nice-grpc'; import type { UploadFileRequest, UploadFileResponse } from '@pretendonetwork/grpc/boss/upload_file'; @@ -113,7 +113,7 @@ export async function uploadFile(request: UploadFileRequest, context: CallContex throw new ServerError(Status.ABORTED, message); } - let file = await getTaskFile(bossAppID, taskID, name); + let file = await getWUPTaskFile(bossAppID, taskID, name); if (file) { file.deleted = true; @@ -122,12 +122,18 @@ export async function uploadFile(request: UploadFileRequest, context: CallContex await file.save(); } - file = await File.create({ + file = await FileWUP.create({ task_id: taskID.slice(0, 7), boss_app_id: bossAppID, file_key: key, supported_countries: supportedCountries, supported_languages: supportedLanguages, + attributes: { + attribute1: request.attribute1, + attribute2: request.attribute2, + attribute3: request.attribute3, + description: request.password + }, creator_pid: context.user?.pid, name: name, type: type, @@ -152,10 +158,10 @@ export async function uploadFile(request: UploadFileRequest, context: CallContex bossAppId: file.boss_app_id, supportedCountries: file.supported_countries, supportedLanguages: file.supported_languages, - password: file.password, - attribute1: file.attribute1, - attribute2: file.attribute2, - attribute3: file.attribute3, + password: file.attributes.description, + attribute1: file.attributes.attribute1, + attribute2: file.attributes.attribute2, + attribute3: file.attributes.attribute3, creatorPid: file.creator_pid, name: file.name, type: file.type, diff --git a/src/services/grpc/boss/v2/delete-file.ts b/src/services/grpc/boss/v2/delete-file.ts new file mode 100644 index 0000000..0a2959e --- /dev/null +++ b/src/services/grpc/boss/v2/delete-file.ts @@ -0,0 +1,44 @@ +import { Status, ServerError } from 'nice-grpc'; +import { PlatformType } from '@pretendonetwork/grpc/boss/v2/platform_type'; +import { getCTRTaskFileBySerialNumber, getWUPTaskFileByDataID } from '@/database'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { DeleteFileRequest } from '@pretendonetwork/grpc/boss/v2/delete_file'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; +import type { HydratedFileCTRDocument } from '@/types/mongoose/file-ctr'; +import type { HydratedFileWUPDocument } from '@/types/mongoose/file-wup'; + +export async function deleteFile(request: DeleteFileRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'deleteBossFiles')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to delete files'); + } + + const dataID = request.dataId; + const bossAppID = request.bossAppId.trim(); + + if (!dataID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file data ID'); + } + + let file: HydratedFileCTRDocument | HydratedFileWUPDocument | null; + + if (request.platformType === PlatformType.PLATFORM_TYPE_CTR) { + file = await getCTRTaskFileBySerialNumber(dataID); + } else if (request.platformType === PlatformType.PLATFORM_TYPE_WUP) { + file = await getWUPTaskFileByDataID(dataID); + } else { + throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid platform type'); + } + + if (!file || file.boss_app_id !== bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, `File ${dataID} not found for BOSS app ${bossAppID}`); + } + + file.deleted = true; + file.updated = BigInt(Date.now()); + + await file.save(); + + return {}; +} diff --git a/src/services/grpc/boss/v2/delete-task.ts b/src/services/grpc/boss/v2/delete-task.ts new file mode 100644 index 0000000..6f5129a --- /dev/null +++ b/src/services/grpc/boss/v2/delete-task.ts @@ -0,0 +1,37 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getTask } from '@/database'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { DeleteTaskRequest } from '@pretendonetwork/grpc/boss/v2/delete_task'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; + +export async function deleteTask(request: DeleteTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'deleteBossTasks')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to delete tasks'); + } + + const taskID = request.id.trim(); + const bossAppID = request.bossAppId.trim(); + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + const task = await getTask(bossAppID, taskID); + + if (!task) { + throw new ServerError(Status.INVALID_ARGUMENT, `Task ${taskID} not found for BOSS app ${bossAppID}`); + } + + task.deleted = true; + task.updated = BigInt(Date.now()); + + await task.save(); + + return {}; +} diff --git a/src/services/grpc/boss/v2/implementation.ts b/src/services/grpc/boss/v2/implementation.ts new file mode 100644 index 0000000..11376a7 --- /dev/null +++ b/src/services/grpc/boss/v2/implementation.ts @@ -0,0 +1,28 @@ +import { listKnownBOSSApps } from '@/services/grpc/boss/v2/list-known-boss-apps'; +import { listTasks } from '@/services/grpc/boss/v2/list-tasks'; +import { registerTask } from '@/services/grpc/boss/v2/register-task'; +import { updateTask } from '@/services/grpc/boss/v2/update-task'; +import { deleteTask } from '@/services/grpc/boss/v2/delete-task'; +import { deleteFile } from '@/services/grpc/boss/v2/delete-file'; +import { listFilesWUP } from '@/services/grpc/boss/v2/list-files-wup'; +import { uploadFileWUP } from '@/services/grpc/boss/v2/upload-file-wup'; +import { listFilesCTR } from '@/services/grpc/boss/v2/list-files-ctr'; +import { uploadFileCTR } from '@/services/grpc/boss/v2/upload-file-ctr'; +import { updateFileMetadataCTR } from '@/services/grpc/boss/v2/update-file-metadata-ctr'; +import { updateFileMetadataWUP } from '@/services/grpc/boss/v2/update-file-metadata-wup'; +import type { BossServiceImplementation } from '@pretendonetwork/grpc/boss/v2/boss_service'; + +export const bossServiceImplementationV2: BossServiceImplementation = { + listKnownBOSSApps, + listTasks, + registerTask, + updateTask, + deleteTask, + deleteFile, + listFilesWUP, + uploadFileWUP, + listFilesCTR, + uploadFileCTR, + updateFileMetadataCTR, + updateFileMetadataWUP +}; diff --git a/src/services/grpc/boss/v2/list-files-ctr.ts b/src/services/grpc/boss/v2/list-files-ctr.ts new file mode 100644 index 0000000..dd03ee7 --- /dev/null +++ b/src/services/grpc/boss/v2/list-files-ctr.ts @@ -0,0 +1,69 @@ +import { Status, ServerError } from 'nice-grpc'; +import { isValidCountryCode, isValidLanguage } from '@/util'; +import { getCTRTaskFiles } from '@/database'; +import type { ListFilesCTRRequest, ListFilesCTRResponse } from '@pretendonetwork/grpc/boss/v2/list_files_ctr'; + +const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/; + +export async function listFilesCTR(request: ListFilesCTRRequest): Promise { + const taskID = request.taskId.trim(); + const bossAppID = request.bossAppId.trim(); + const country = request.country?.trim(); + const language = request.language?.trim(); + const any = request.any; + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + if (bossAppID.length !== 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must be 16 characters'); + } + + if (!BOSS_APP_ID_FILTER_REGEX.test(bossAppID)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must only contain letters and numbers'); + } + + if (country && !isValidCountryCode(country)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${country} is not a valid country`); + } + + if (language && !isValidLanguage(language)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${language} is not a valid language`); + } + + const files = await getCTRTaskFiles(false, bossAppID, taskID, country, language, any); + + return { + files: files.map(file => ({ + deleted: file.deleted, + dataId: file.serial_number, + taskId: file.task_id, + bossAppId: file.boss_app_id, + supportedCountries: file.supported_countries, + supportedLanguages: file.supported_languages, + attributes: file.attributes, + creatorPid: file.creator_pid, + name: file.name, + hash: file.hash, + serialNumber: file.serial_number, + payloadContents: file.payload_contents.map(payloadContentInfo => ({ + titleId: payloadContentInfo.title_id, + contentDatatype: payloadContentInfo.content_datatype, + nsDataId: payloadContentInfo.ns_data_id, + version: payloadContentInfo.version, + size: payloadContentInfo.size + })), + size: file.size, + createdTimestamp: file.created, + updatedTimestamp: file.updated, + flags: { + markArrivedPrivileged: file.flags.mark_arrived_privileged + } + })) + }; +} diff --git a/src/services/grpc/boss/v2/list-files-wup.ts b/src/services/grpc/boss/v2/list-files-wup.ts new file mode 100644 index 0000000..a537561 --- /dev/null +++ b/src/services/grpc/boss/v2/list-files-wup.ts @@ -0,0 +1,63 @@ +import { Status, ServerError } from 'nice-grpc'; +import { isValidCountryCode, isValidLanguage } from '@/util'; +import { getWUPTaskFiles } from '@/database'; +import type { ListFilesWUPRequest, ListFilesWUPResponse } from '@pretendonetwork/grpc/boss/v2/list_files_wup'; + +const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/; + +export async function listFilesWUP(request: ListFilesWUPRequest): Promise { + const taskID = request.taskId.trim(); + const bossAppID = request.bossAppId.trim(); + const country = request.country?.trim(); + const language = request.language?.trim(); + const any = request.any; + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + if (bossAppID.length !== 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must be 16 characters'); + } + + if (!BOSS_APP_ID_FILTER_REGEX.test(bossAppID)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must only contain letters and numbers'); + } + + if (country && !isValidCountryCode(country)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${country} is not a valid country`); + } + + if (language && !isValidLanguage(language)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${language} is not a valid language`); + } + + const files = await getWUPTaskFiles(false, bossAppID, taskID, country, language, any); + + return { + files: files.map(file => ({ + deleted: file.deleted, + dataId: file.data_id, + taskId: file.task_id, + bossAppId: file.boss_app_id, + supportedCountries: file.supported_countries, + supportedLanguages: file.supported_languages, + attributes: file.attributes, + creatorPid: file.creator_pid, + name: file.name, + type: file.type, + hash: file.hash, + size: file.size, + notifyOnNew: file.notify_on_new, + notifyLed: file.notify_led, + conditionPlayed: file.condition_played, + autoDelete: file.auto_delete, + createdTimestamp: file.created, + updatedTimestamp: file.updated + })) + }; +} diff --git a/src/services/grpc/boss/v2/list-known-boss-apps.ts b/src/services/grpc/boss/v2/list-known-boss-apps.ts new file mode 100644 index 0000000..ae1d666 --- /dev/null +++ b/src/services/grpc/boss/v2/list-known-boss-apps.ts @@ -0,0 +1,4586 @@ +import type { ListKnownBOSSAppsResponse } from '@pretendonetwork/grpc/boss/v2/list_known_boss_apps'; + +export async function listKnownBOSSApps(): Promise { + return { + apps: [ + { + bossAppId: 'WJDaV6ePVgrS0TRa', + titleId: 0x0005003010016000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['olvinfo'] + }, + { + bossAppId: 'VFoY6V7u7UUq1EG5', + titleId: 0x0005003010016100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['olvinfo', 'oltopic'] + }, + { + bossAppId: '8MNOVprfNVAJjfCM', + titleId: 0x0005003010016200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['olvinfo'] + }, + { + bossAppId: 'v1cqzWykBKUg0rHQ', + titleId: 0x000500301001900An, + titleRegion: 'JPN', + name: 'Miiverse Post All', + tasks: ['solv'] + }, + { + bossAppId: 'bieC9ACJlisFg5xS', + titleId: 0x000500301001910An, + titleRegion: 'USA', + name: 'Miiverse Post All', + tasks: ['solv'] + }, + { + bossAppId: 'tOaQcoBLtPTgVN3Y', + titleId: 0x000500301001920An, + titleRegion: 'EUR', + name: 'Miiverse Post All', + tasks: ['solv'] + }, + { + bossAppId: 'HX8a16MMNn6i1z0Y', + titleId: 0x000500301001400An, + titleRegion: 'JPN', + name: 'Nintendo eShop', + tasks: ['wood1', 'woodBGM'] + }, + { + bossAppId: '07E3nY6lAwlwrQRo', + titleId: 0x000500301001410An, + titleRegion: 'USA', + name: 'Nintendo eShop', + tasks: ['wood1', 'woodBGM'] + }, + { + bossAppId: '8UsM86l8xgkjFk8z', + titleId: 0x000500301001420An, + titleRegion: 'EUR', + name: 'Nintendo eShop', + tasks: ['wood1', 'woodBGM'] + }, + { + bossAppId: 'IXmFUqR2qenXfF61', + titleId: 0x0005001010066000n, + titleRegion: 'ALL', + name: 'ECO Process', + tasks: ['promo1', 'promo2', 'promo3', 'push'] + }, + { + bossAppId: 'BMQAm5iUVtPsJVsU', + titleId: 0x000500101004D000n, + titleRegion: 'JPN', + name: 'Notifications', + tasks: ['sysmsg1', 'sysmsg2'] + }, + { + bossAppId: 'LRmanFo4Tx3kEGDp', + titleId: 0x000500101004D100n, + titleRegion: 'USA', + name: 'Notifications', + tasks: ['sysmsg1', 'sysmsg2'] + }, + { + bossAppId: 'TZr27FE8wzKiEaTO', + titleId: 0x000500101004D200n, + titleRegion: 'EUR', + name: 'Notifications', + tasks: ['sysmsg1', 'sysmsg2'] + }, + { + bossAppId: 'JnIrm9c4E9JBmxBo', + titleId: 0x0005000010185200n, + titleRegion: 'JPN', + name: 'NewスーパーマリオブラザーズU 無料お試し版 (New SUPER MARIO BROS. U (Trial))', + tasks: ['news'] + }, + { + bossAppId: 'dadlI27Ww8H2d56x', + titleId: 0x0005000010101C00n, + titleRegion: 'JPN', + name: 'NewスーパーマリオブラザーズU (New SUPER MARIO BROS. U)', + tasks: ['news', 'plyrepo'] + }, + { + bossAppId: 'RaPn5saabzliYrpo', + titleId: 0x0005000010101D00n, + titleRegion: 'USA', + name: 'New SUPER MARIO BROS. U', + tasks: ['news', 'plyrepo'] + }, + { + bossAppId: '14VFIK3rY2SP0WRE', + titleId: 0x0005000010101E00n, + titleRegion: 'EUR', + name: 'New SUPER MARIO BROS. U', + tasks: ['news', 'plyrepo'] + }, + { + bossAppId: 'RbEQ44t2AocC4rvu', + titleId: 0x000500001014B700n, + titleRegion: 'USA', + name: 'New SUPER MARIO BROS. U + New SUPER LUIGI U', + tasks: ['news'] + }, + { + bossAppId: '287gv3WZdxo1QRhl', + titleId: 0x000500001014B800n, + titleRegion: 'EUR', + name: 'New SUPER MARIO BROS. U + New SUPER LUIGI U', + tasks: ['news'] + }, + { + bossAppId: 'bb6tOEckvgZ50ciH', + titleId: 0x0005000010162B00n, + titleRegion: 'JPN', + name: 'スプラトゥーン (Splatoon)', + tasks: ['optdat2', 'schdat2', 'schdata', 'optdata'] + }, + { + bossAppId: 'rjVlM7hUXPxmYQJh', + titleId: 0x0005000010176900n, + titleRegion: 'USA', + name: 'Splatoon', + tasks: ['optdat2', 'schdat2', 'schdata', 'optdata2', 'schdata2', 'test', 'preport', 'otpdata2', 'scddata2', 'otpdat2', 'optdata'] + }, + { + bossAppId: 'zvGSM4kOrXpkKnpT', + titleId: 0x0005000010176A00n, + titleRegion: 'EUR', + name: 'Splatoon', + tasks: ['optdat2', 'schdat2', 'schdata', 'optdata'] + }, + { + bossAppId: 'm8KJPtmPweiPuETE', + titleId: 0x000500001012F100n, + titleRegion: 'JPN', + name: 'Wii Sports Club', + tasks: ['sp1_ans', 'sp1_rnk', 'sp1_evt'] + }, + { + bossAppId: 'pO72Hi5uqf5yuNd8', + titleId: 0x0005000010144D00n, + titleRegion: 'USA', + name: 'Wii Sports Club', + tasks: ['sp1_ans', 'sp1_rnk', 'sp1_evt'] + }, + { + bossAppId: '4m8Xme1wKgzwslTJ', + titleId: 0x0005000010144E00n, + titleRegion: 'EUR', + name: 'Wii Sports Club', + tasks: ['sp1_ans', 'sp1_rnk', 'sp1_evt'] + }, + { + bossAppId: 'ESLqtAhxS8KQU4eu', + titleId: 0x000500001018DB00n, + titleRegion: 'JPN', + name: 'Super Mario Maker (スーパーマリオメーカー)', + tasks: ['CHARA'] + }, + { + bossAppId: 'vGwChBW1ExOoHDsm', + titleId: 0x000500001018DC00n, + titleRegion: 'USA', + name: 'Super Mario Maker', + tasks: ['CHARA'] + }, + { + bossAppId: 'IeUc4hQsKKe9rJHB', + titleId: 0x000500001018DD00n, + titleRegion: 'EUR', + name: 'Super Mario Maker', + tasks: ['CHARA'] + }, + { + bossAppId: '4krJA4Gx3jF5nhQf', + titleId: 0x000500001012BE00n, + titleRegion: 'EUR', + name: 'PIKMIN 3', + tasks: ['histgrm'] + }, + { + bossAppId: '9jRZEoWYLc3OG9a8', + titleId: 0x000500001012BD00n, + titleRegion: 'USA', + name: 'PIKMIN 3', + tasks: ['histgrm'] + }, + { + bossAppId: 'VWqUTspR5YtjDjxa', + titleId: 0x000500001012BC00n, + titleRegion: 'JPN', + name: 'ピクミン3 (PIKMIN 3)', + tasks: ['histgrm'] + }, + { + bossAppId: 'Ge1KtMu8tYlf4AUM', + titleId: 0x0005000010192000n, + titleRegion: 'JPN', + name: '太鼓の達人 特盛り! (Taiko no Tatsujin Tokumori!)', + tasks: ['notice1'] + }, + { + bossAppId: 'gycVtTzCouZmukZ6', + titleId: 0x0005000010110E00n, + titleRegion: 'JPN', + name: '大乱闘スマッシュブラザーズ for Wii U (Super Smash Bros. for Wii U)', + tasks: ['NEWS', 'amiibo', 'friend', 'CONQ'] + }, + { + bossAppId: 'o2Ug1pIp9Uhri6Nh', + titleId: 0x0005000010144F00n, + titleRegion: 'USA', + name: 'Super Smash Bros. for Wii U', + tasks: ['amiibo', 'NEWS', 'friend', 'CONQ'] + }, + { + bossAppId: 'n6rAJ1nnfC1Sgcpl', + titleId: 0x0005000010145000n, + titleRegion: 'EUR', + name: 'Super Smash Bros. for Wii U', + tasks: ['amiibo', 'NEWS', 'friend', 'CONQ'] + }, + { + bossAppId: 'CHUN6T1m7Xk4EBg4', + titleId: 0x00050000101DFF00n, + titleRegion: 'JPN', + name: 'プチコンBIG (Petitcom BIG)', + tasks: ['ptcbnws'] + }, + { + bossAppId: 'zyXdCW9jGdi9rjaz', + titleId: 0x0005000010142200n, + titleRegion: 'JPN', + name: 'NewスーパールイージU (New SUPER LUIGI U)', + tasks: ['news'] + }, + { + bossAppId: 'jPHLlJr2fJyTzffp', + titleId: 0x0005000010142300n, + titleRegion: 'USA', + name: 'New SUPER LUIGI U', + tasks: ['news'] + }, + { + bossAppId: 'YsXB6IRGSI56tPxl', + titleId: 0x0005000010142400n, + titleRegion: 'EUR', + name: 'New SUPER LUIGI U', + tasks: ['news'] + }, + { + bossAppId: 'Lbqp9Sg1i0xUzFFa', + titleId: 0x0005000010113800n, + titleRegion: 'EUR', + name: 'Zen Pinball 2', + tasks: ['PTS'] + }, + { + bossAppId: 'DwU7n0FidGrLNiOo', + titleId: 0x000500001014D900n, + titleRegion: 'JPN', + name: 'ぷよぷよテトリス (PUYOPUYOTETRIS)', + tasks: ['boss1', 'boss2', 'boss3'] + }, + { + bossAppId: 'yIUkFmuGVkGP8pDb', + titleId: 0x0005000010132200n, + titleRegion: 'JPN', + name: '太鼓の達人 Wii Uば~じょん! (Taiko no Tatsujin Wii U version!)', + tasks: ['notice1'] + }, + { + bossAppId: 'v4WRObSzD7VU3dcJ', + titleId: 0x00050000101D3000n, + titleRegion: 'JPN', + name: '太鼓の達人 あつめて★ともだち大作戦! (Taiko no Tatsujin Atsumete★ TomodachiDaisakusen!)', + tasks: ['notice1'] + }, + { + bossAppId: '3zDjXIA57bSceyaw', + titleId: 0x00050000101BEC00n, + titleRegion: 'USA', + name: 'Star Fox Guard', + tasks: ['param'] + }, + { + bossAppId: 'NL38jhExI2CQqhWd', + titleId: 0x00050000101CDB00n, + titleRegion: 'JPN', + name: 'Splatoon Pre-Launch Review', + tasks: ['schdata', 'optdata'] + }, + { + bossAppId: 'sE6KwEpQYyg6tdU7', + titleId: 0x00050000101CDC00n, + titleRegion: 'USA', + name: 'Splatoon Pre-Launch Review', + tasks: ['schdata', 'optdata'] + }, + { + bossAppId: 'pTKZ9q5KrCP3gBag', + titleId: 0x00050000101CDD00n, + titleRegion: 'EUR', + name: 'Splatoon Pre-Launch Review', + tasks: ['schdata', 'optdata'] + }, + { + bossAppId: 'CJT88RO008LAnD51', + titleId: 0x0005000010170600n, + titleRegion: 'JPN', + name: '仮面ライダー バトライド・ウォーⅡ プレミアムTV&MOVIEサウンドED. (KAMEN RIDER BATTRIDE WAR Ⅱ PREMIUM TV&MOVIE SOUND ED.)', + tasks: ['PE_GAK', 'PE_ZNG'] + }, + { + bossAppId: 'FyyMFzEByuQJc6sJ', + titleId: 0x0005000010135200n, + titleRegion: 'USA', + name: 'Star Wars Pinball', + tasks: ['PTS'] + }, + { + bossAppId: 'A4yyXWKZZUToFtrt', + titleId: 0x0005000010132A00n, + titleRegion: 'EUR', + name: 'Star Wars Pinball', + tasks: ['PTS'] + }, + { + bossAppId: 'HauaFQ1sPsnQ6rBj', + titleId: 0x0005000010171F00n, + titleRegion: 'USA', + name: 'Pushmo World', + tasks: ['annouce'] + }, + { + bossAppId: 'qDUeFmk0Az71nHyD', + titleId: 0x0005000010110900n, + titleRegion: 'JPN', + name: 'NINJA GAIDEN 3: Razor\'s Edge', + tasks: ['DLCINFO'] + }, + { + bossAppId: 'yVsSPM2E0DEOxroT', + titleId: 0x0005000010110A00n, + titleRegion: 'USA', + name: 'NINJA GAIDEN 3: Razor\'s Edge', + tasks: ['DLCINFO'] + }, + { + bossAppId: 'Xw6OvZkQofQ3O8Bi', + titleId: 0x0005000010110B00n, + titleRegion: 'EUR', + name: 'NINJA GAIDEN 3: Razor\'s Edge', + tasks: ['DLCINFO'] + }, + { + bossAppId: 'LUQX5swEjBUPQ8nR', + titleId: 0x0005000010110200n, + titleRegion: 'USA', + name: 'WARRIORS OROCHI 3 Hyper(NA)', + tasks: ['OR2H000'] + }, + { + bossAppId: 'y4pXrgLe0JGao3No', + titleId: 0x0005000010112B00n, + titleRegion: 'EUR', + name: 'WARRIORS OROCHI 3 Hyper(EU)', + tasks: ['OR2H000'] + }, + { + bossAppId: 'j01mRJ9sNe00MWPC', + titleId: 0x0005000010170700n, + titleRegion: 'JPN', + name: '仮面ライダー バトライド・ウォーⅡ (KAMEN RIDER BATTRIDE WAR Ⅱ)', + tasks: ['CHR_GAK', 'CHR_ZNG'] + }, + { + bossAppId: 'P45xuCJjERf6MNWG', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['movie'] + }, + { + bossAppId: 'PQWAfUmDpVo0u9Fi', + titleId: 0x0005000010111C00n, + titleRegion: 'JPN', + name: 'Romance of the Three Kingdoms 12', + tasks: ['Card'] + }, + { + bossAppId: 'EA9wpEnmZmeX70YS', + titleId: 0x0005000010192200n, + titleRegion: 'JPN', + name: 'KAMEN RIDER SUMMON RIDE!', + tasks: ['ADDCHR0'] + }, + { + bossAppId: 'Iq5CNAngvR9auXFO', + titleId: 0x00050000101BED00n, + titleRegion: 'EUR', + name: 'Star Fox Guard', + tasks: ['param'] + }, + { + bossAppId: 'ZtwtVqJkmGE2LloD', + titleId: 0x0005000010115F00n, + titleRegion: 'USA', + name: 'Zen Pinball 2', + tasks: ['PTS'] + }, + { + bossAppId: 'eAzIbHvwKNHwz85M', + titleId: 0x0005000010116400n, + titleRegion: 'JPN', + name: 'niconico', + tasks: ['news'] + }, + { + bossAppId: 'z4d72slRF5GX0cEr', + titleId: 0x0005000010172000n, + titleRegion: 'EUR', + name: 'Pullblox World', + tasks: ['annouce'] + }, + { + bossAppId: '5iKeqk6fQq3wwfgy', + titleId: 0x000500001010EA00n, + titleRegion: 'JPN', + name: 'WARRIORS OROCHI 3 Hyper(JP)', + tasks: ['OR2H000'] + }, + { + bossAppId: 'rvI5oS5jSZ0aLpeo', + titleId: 0x0005000011000000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['demo1'] + }, + { + bossAppId: 'R5WU9gZtFShZlf6j', + titleId: 0x000500001010ED00n, + titleRegion: 'EUR', + name: 'MARIO KART 8', + tasks: ['movie', 'Histo'] + }, + { + bossAppId: '78QqMzbyBbwEpzVg', + titleId: 0x0005000010111700n, + titleRegion: 'USA', + name: 'Injustice: Gods Among Us', + tasks: ['Tvars'] + }, + { + bossAppId: 'I8IZTXQyDnUnFo77', + titleId: 0x0005000010111A00n, + titleRegion: 'EUR', + name: 'Injustice: Gods Among Us', + tasks: ['Tvars'] + }, + { + bossAppId: 'XcawL2u1CU624gg3', + titleId: 0x0005000010185300n, + titleRegion: 'JPN', + name: 'PIKMIN 3 (Trial)', + tasks: ['histgrm'] + }, + { + bossAppId: 'uNRNThGetHLXasV9', + titleId: 0x00050000101BEB00n, + titleRegion: 'JPN', + name: 'Star Fox Guard', + tasks: ['param'] + }, + { + bossAppId: 'MBOU6MNVQRdTT1QA', + titleId: 0x00050000101DCC00n, + titleRegion: 'JPN', + name: 'Star Fox Guard Special Demo', + tasks: ['param'] + }, + { + bossAppId: 'zBlJpj2pXcFeJYJI', + titleId: 0x00050000101DCE00n, + titleRegion: 'EUR', + name: 'Star Fox Guard: Special Demo Version', + tasks: ['param'] + }, + { + bossAppId: 'dHWbU7brnq9QKaKA', + titleId: 0x00050000101DCD00n, + titleRegion: 'USA', + name: 'Star Fox Guard Special Demo', + tasks: ['param'] + }, + { + bossAppId: 'gjYbE1NbQerS5v6n', + titleId: 0x0005000010149000n, + titleRegion: 'JPN', + name: 'Romance of the Three Kingdoms 12 with Powerup kit', + tasks: ['Card'] + }, + { + bossAppId: '07SSacDlEHc8z0jg', + titleId: 0x0004000000155000n, + titleRegion: 'JPN', + name: 'ディズニー マジックキャッスル マイ・ハッピー・ライフ2', + tasks: ['FGONLYT', 'MC2NWS'] + }, + { + bossAppId: '0hFlOFo7pNTU2dyE', + titleId: 0x00040000001A2D00n, + titleRegion: 'USA', + name: 'Swapdoodle', + tasks: ['RNG_EC1', 'RNG_LS1', 'RNG_MD1', 'RNG_NT1', 'RNG_NT2', 'RNG_GM1', 'RNG_AP1', 'RNG_DFR', 'RNG_U01', 'RNG_U02', 'RNG_U03', 'RNG_U04', 'RNG_U05', 'RNG_U06', 'RNG_U07', 'RNG_UUS'] + }, + { + bossAppId: '10Og6tqFdXrW1Dra', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '18ZHPbhuhUMZmLMS', + titleId: 0x00040000001D6C00n, + titleRegion: 'UNK', + name: 'Yo-kai Watch 3', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '1dNhxKHa1kgzz0gj', + titleId: 0x0004000000174E00n, + titleRegion: 'JPN', + name: 'MEDAROT9 KABUTO Ver.', + tasks: ['MEDA9'] + }, + { + bossAppId: '2eMXT6CAAcQIYWuN', + titleId: 0x000400000016C700n, + titleRegion: 'JPN', + name: 'Yokai Watch Busters Akaneko Dan', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '2s640xAtrZGOWdq0', + titleId: 0x0004000000051700n, + titleRegion: 'USA', + name: 'Swapnote', + tasks: ['JFR_LS2', 'JFR_NT2', 'JFR_NT1', 'JFR_NT3', 'JFR_AP2', 'JFR_GM2', 'JFR_DNT', 'JFR_DLS', 'JFR_DAP', 'JFR_DGM', 'JFR_DFR', 'JFR_U01', 'JFR_U02', 'JFR_U03', 'JFR_U04', 'JFR_U05', 'JFR_U06', 'JFR_U07', 'JFR_U08', 'JFR_U09', 'JFR_U10'] + }, + { + bossAppId: '2zDwgq1t61PlMaPq', + titleId: 0x000400000012DE00n, + titleRegion: 'UNK', + name: 'ファイアーエムブレム if', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: '3ddVFPLZpzu77yvS', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: '3EHaOtNKDsD3Ybk8', + titleId: 0x000400000016C600n, + titleRegion: 'JPN', + name: 'Yokai Watch Busters Shiroinu Tai', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '3isXVXrb2lLqmrW0', + titleId: 0x00040000000CB900n, + titleRegion: 'USA', + name: 'Nintendo 3DS Guide: Louvre (English Version)', + tasks: ['AL8_001'] + }, + { + bossAppId: '3t2IUj3ASUzKKEHK', + titleId: 0x00040000001B4100n, + titleRegion: 'EUR', + name: 'Fire Emblem Echoes: Shadows of Valentia', + tasks: ['TASK00', 'TASK01'] + }, + { + bossAppId: '3vveLadT8H6xKkQH', + titleId: 0x00040000001A2E00n, + titleRegion: 'EUR', + name: 'Swapdoodle', + tasks: ['RNG_EC1', 'RNG_LS1', 'RNG_MD1', 'RNG_NT1', 'RNG_NT2', 'RNG_GM1', 'RNG_AP1', 'RNG_DFR', 'RNG_U01', 'RNG_U02', 'RNG_U03', 'RNG_U04', 'RNG_U05', 'RNG_U06', 'RNG_U07', 'RNG_UUS'] + }, + { + bossAppId: '4LvdQ9tJBCOyHrv4', + titleId: 0x0004000000030700n, + titleRegion: 'EUR', + name: 'MARIO KART 7', + tasks: ['comm', 'ghost', 'ranking'] + }, + { + bossAppId: '4OBVxt1uzhPW4cGR', + titleId: 0x0004000000118100n, + titleRegion: 'JPN', + name: 'DETECTIVE CONAN PHANTOM RHAPSODY', + tasks: ['BKRJ-00', 'BKRJ', 'FGONLYT'] + }, + { + bossAppId: '53R1vYbkfqXlqzns', + titleId: 0x0004000000030600n, + titleRegion: 'JPN', + name: 'マリオカート7', + tasks: ['comm', 'ghost', 'ranking'] + }, + { + bossAppId: '5xq3tXtlGqUd7MbV', + titleId: 0x00040000000EDF00n, + titleRegion: 'USA', + name: 'Super Smash Bros. for Nintendo 3DS', + tasks: ['NEWS', 'amiibo', 'FGONLYT'] + }, + { + bossAppId: '5yLkA3wqcXruNE2N', + titleId: 0x000400000008C300n, + titleRegion: 'USA', + name: 'Tomodachi Life', + tasks: ['tmTaskA', 'tmTaskD'] + }, + { + bossAppId: '6jk78e80CzvtPvim', + titleId: 0x0004000000095000n, + titleRegion: 'JPN', + name: 'Pokemon Mystery Dungeon Magna gate and infinity labyrinth', + tasks: ['PAINF00'] + }, + { + bossAppId: '6SKM87Ll80ONzAvQ', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '7EnMOQ6WfZSmvQgu', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '7knOkHlr5db97LJ6', + titleId: 0x00040000001B4000n, + titleRegion: 'USA', + name: 'Fire Emblem Echoes: Shadows of Valentia', + tasks: ['TASK00', 'TASK01'] + }, + { + bossAppId: '7NdLNTN4iH9wEbJQ', + titleId: 0x00040000000D0900n, + titleRegion: 'EUR', + name: 'Pokémon Art Academy', + tasks: ['FGONLYT', 'pnote'] + }, + { + bossAppId: '7vzbLQCS84rtXY0y', + titleId: 0x0004000000140000n, + titleRegion: 'JPN', + name: 'シアトリズム ドラゴンクエスト', + tasks: ['FGONLYT', 'TDQ01'] + }, + { + bossAppId: '8DeC0vBA5VDzPu8x', + titleId: 0x00040000000A7700n, + titleRegion: 'JPN', + name: 'AKB48+Me', + tasks: ['AKBadd'] + }, + { + bossAppId: '8DViaPfyfH1pGO91', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '8mX4hRmZqkDHn0SE', + titleId: 0x000400000010BB00n, + titleRegion: 'JPN', + name: 'イナズマイレブンGO ギャラクシー スーパーノヴァ', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '9x4m4dJwyBBlUc3g', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'demotask', 'flist', 'test'] + }, + { + bossAppId: 'ac2P6aIvORxnQJwK', + titleId: 0x0004000000147100n, + titleRegion: 'USA', + name: 'Little Battlers eXperience', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'adQnSUvJlXp5igaT', + titleId: 0x00040000001D6A00n, + titleRegion: 'UNK', + name: 'Yo-kai Watch 3', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'aFMv95FiWHC3k7XV', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'AWKlN4KN0rMA7Gqc', + titleId: 0x0004000000113400n, + titleRegion: 'JPN', + name: 'MEDAROT8 KUWAGATA Ver.1.1', + tasks: ['MEDA8'] + }, + { + bossAppId: 'AZcAJ2Stxa9P8h7a', + titleId: 0x0004000000072A00n, + titleRegion: 'JPN', + name: 'Dynasty Warriors VS', + tasks: ['SMVS_EC', 'SMVS_SC', 'SMVS_PR'] + }, + { + bossAppId: 'B6Iqt2r0EGU549NW', + titleId: 0x0004000000030800n, + titleRegion: 'USA', + name: 'MARIO KART 7', + tasks: ['comm', 'ghost', 'ranking'] + }, + { + bossAppId: 'b8RPtQj41Mw5HjoX', + titleId: 0x00040000001D6700n, + titleRegion: 'USA', + name: 'Yo-kai Watch 3', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'bGoDxPNf97rXq9a5', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'BgyAdVTkMfLTzM0k', + titleId: 0x0004000000167600n, + titleRegion: 'KOR', + name: 'YO-KAI WATCH', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'bOMC45IF1taPnYx8', + titleId: 0x0004000000168C00n, + titleRegion: 'UNK', + name: 'Little Battlers eXperience', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'ciTu5gGHcW767kOc', + titleId: 0x0004000000101300n, + titleRegion: 'JPN', + name: 'ヒーローバンク', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'cPYaUEX74KmPR0wF', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'CtfKXACbUPl8s7lk', + titleId: 0x0004001000021900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BGM1', 'BGM2', 'TIGER1'] + }, + { + bossAppId: 'cXo6TOh0AtrCzaCP', + titleId: 0x0004000000051800n, + titleRegion: 'EUR', + name: 'Nintendo Letter Box', + tasks: ['JFR_LS2', 'JFR_AP2', 'JFR_NT1', 'JFR_NT2', 'JFR_NT3', 'JFR_DLS', 'JFR_DNT', 'JFR_DAP', 'JFR_DGM', 'JFR_GM2', 'JFR_DFR', 'JFR_U01', 'JFR_U02', 'JFR_U03', 'JFR_U04', 'JFR_U05', 'JFR_U06', 'JFR_U07', 'JFR_U08', 'JFR_U09', 'JFR_U10'] + }, + { + bossAppId: 'd4495LzgtcxUObka', + titleId: 0x0004000000065A00n, + titleRegion: 'JPN', + name: 'MEDAROT7 KUWAGATA Ver.1.1', + tasks: ['MEDA7'] + }, + { + bossAppId: 'DJBXc6TzoubPYfy6', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['weekly'] + }, + { + bossAppId: 'dpwg7hD86KlFwcbk', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'DUE2YwOq2m9fVsdZ', + titleId: 0x000400000012A800n, + titleRegion: 'JPN', + name: 'hoppechan minnadeodekake! wakuwaku hoppeland!!Ver.1.2', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'DwQNLZT8QlZIxyAJ', + titleId: 0x0004000000107C00n, + titleRegion: 'USA', + name: 'Chibi-Robo! Photo Finder', + tasks: ['mesdat', 'dat'] + }, + { + bossAppId: 'e4rcoYW9QTHc2whz', + titleId: 0x0004000000155100n, + titleRegion: 'JPN', + name: 'Yokai Watch 2 shinuchi', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'E5myNZzoWVHdNUvY', + titleId: 0x0004000000113300n, + titleRegion: 'JPN', + name: 'MEDAROT8 KABUTO Ver.1.1', + tasks: ['MEDA8'] + }, + { + bossAppId: 'eH6oowgSM7n662mw', + titleId: 0x000400000008C400n, + titleRegion: 'EUR', + name: 'Tomodachi Life', + tasks: ['tmTaskA', 'tmTaskD', 'tmTaskU'] + }, + { + bossAppId: 'f5xYhmZvwKo4uv5A', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'fbAzg6nuN4hhQ3pJ', + titleId: 0x00040000001B2900n, + titleRegion: 'EUR', + name: 'YO-KAI WATCH 2: PSYCHIC SPECTERS', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'fFV9HxPJR7NJRTre', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'FNY6HPPbPj2jIErD', + titleId: 0x0004000000072400n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['Amb_eu', 'Amb'] + }, + { + bossAppId: 'fQZi0N3YlWWq07AZ', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['item'] + }, + { + bossAppId: 'fsYqLFLQngg32A1l', + titleId: 0x00040000000D5600n, + titleRegion: 'USA', + name: 'Nintendo 3DS Guide: Louvre (Version française)', + tasks: ['AL8_001'] + }, + { + bossAppId: 'G06t7Q3mkOy95nBw', + titleId: 0x00040000001CEB00n, + titleRegion: 'USA', + name: 'YO-KAI WATCH BLASTERS RED CAT CORPS', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'G4uDyFA9Yc06kV8m', + titleId: 0x00040000000B6E00n, + titleRegion: 'USA', + name: 'Crashmo', + tasks: ['JAU'] + }, + { + bossAppId: 'g6DKUfhQUHKd5gZz', + titleId: 0x00040000001CA800n, + titleRegion: 'EUR', + name: 'LAYTON\'S MYSTERY JOURNEY™ Katrielle and the Millionaires\'...', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'GjTM1Bphz05wTMtO', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'gOlIVAcjDpj6K1Ut', + titleId: 0x0004000000166A00n, + titleRegion: 'JPN', + name: 'Phoenix Wright: Ace Attorney Spirit of Justice', + tasks: ['GS6'] + }, + { + bossAppId: 'GQkLSOsKwIpvr8Yi', + titleId: 0x000400000007AE00n, + titleRegion: 'USA', + name: 'New Super Mario Bros. 2', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'GQqblssCbq6PuCyJ', + titleId: 0x0004000000174F00n, + titleRegion: 'JPN', + name: 'MEDAROT9 KUWAGATA Ver.', + tasks: ['MEDA9'] + }, + { + bossAppId: 'guBwm9TlQvYvncKn', + titleId: 0x000400000011C500n, + titleRegion: 'JPN', + name: 'Pokémon Alpha Sapphire', + tasks: ['horogra', 'FGONLYT'] + }, + { + bossAppId: 'GxCs83sbgwaoL2js', + titleId: 0x00040000001AE600n, + titleRegion: 'JPN', + name: '100% PASUKARU SENSEI Perfect Paint Bombers', + tasks: ['FGONLYT', 'POSTING'] + }, + { + bossAppId: 'H9GUJGE1xWr9VrgG', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'hA0Mq0T7KQofzsFF', + titleId: 0x00040000000CBA00n, + titleRegion: 'KOR', + name: 'Nintendo 3DS Guide: Louvre', + tasks: ['AL8_001'] + }, + { + bossAppId: 'i9omdntnCo1GPdHA', + titleId: 0x00040000001CB400n, + titleRegion: 'UNK', + name: 'LAYTON\'S MYSTERY JOURNEY™ Katrielle and the Millionaires\'...', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'iolEjnbGp2W1ghtO', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'iYhju0xUdEBftgzw', + titleId: 0x0004000000167700n, + titleRegion: 'USA', + name: 'YO-KAI WATCH', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'j0ITmVqVgfUxe0O9', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'FGONLYT', 'news'] + }, + { + bossAppId: 'J3u1c5M8Ff9Y9TyG', + titleId: 0x00040000000F0500n, + titleRegion: 'UNK', + name: '大合奏!バンドブラザーズP しもべツール', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'j5cz6H9mjSt8wvLX', + titleId: 0x0004000000086300n, + titleRegion: 'UNK', + name: 'Animal Crossing New Leaf', + tasks: ['FGONLYT', 'dlvexb', 'news', 'news_p', 'pnews', 'dream', 'dream_p'] + }, + { + bossAppId: 'J6la9Kj8iqTvAPOq', + titleId: 0x0004000000153600n, + titleRegion: 'EUR', + name: 'Nintendo Badge Arcade Ver.1.3.1', + tasks: ['data', 'FGONLYT', 'news'] + }, + { + bossAppId: 'j9wEm4RKrgNuaudD', + titleId: 0x0004000000176E00n, + titleRegion: 'JPN', + name: 'The Legend of Zelda Tri Force Heroes', + tasks: ['Info_00', 'Data', 'Data_00', 'Data_01'] + }, + { + bossAppId: 'JfttU8Wg1iBIbbIs', + titleId: 0x0004000000053700n, + titleRegion: 'JPN', + name: 'エクストルーパーズ', + tasks: ['EXT0100'] + }, + { + bossAppId: 'JIguVGEJJOhAq2te', + titleId: 0x00040000001A2B00n, + titleRegion: 'JPN', + name: 'Fire Emblem Echoes: Shadows of Valentia', + tasks: ['TASK00', 'FGONLYT', 'TASK01'] + }, + { + bossAppId: 'JqhCCnj1t7Zid1SG', + titleId: 0x000400000010BA00n, + titleRegion: 'JPN', + name: 'イナズマイレブンGO ギャラクシー ビッグバン', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'jy95T3iOBKyGbAXl', + titleId: 0x00040000000CF900n, + titleRegion: 'JPN', + name: 'Pokémon Art Academy', + tasks: ['FGONLYT', 'pnote'] + }, + { + bossAppId: 'kb6KM6y6fQx3bOWs', + titleId: 0x00040000000D0A00n, + titleRegion: 'USA', + name: 'Pokémon Art Academy', + tasks: ['FGONLYT', 'pnote'] + }, + { + bossAppId: 'kQGzeGpga1cXwbtf', + titleId: 0x0004000000065B00n, + titleRegion: 'JPN', + name: 'MEDAROT7 KABUTO Ver.1.1', + tasks: ['MEDA7'] + }, + { + bossAppId: 'kSz44ZF73HpIb4O7', + titleId: 0x000400000008B400n, + titleRegion: 'TWN', + name: 'MARIO KART 7', + tasks: ['comm', 'ghost', 'ranking'] + }, + { + bossAppId: 'kTnNBa7mg66E11iV', + titleId: 0x0004000000030A00n, + titleRegion: 'KOR', + name: 'MARIO KART 7', + tasks: ['ghost', 'ranking', 'comm'] + }, + { + bossAppId: 'L1P2RB6s878vioGs', + titleId: 0x0004000000030100n, + titleRegion: 'USA', + name: 'Kid Icarus: Uprising', + tasks: ['SEED'] + }, + { + bossAppId: 'lg9TdjOIyCO7aUR4', + titleId: 0x00040000000CF400n, + titleRegion: 'JPN', + name: 'Yokai Watch', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'lhz3HfuATBPF4EdY', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['ESE_CNF', 'ESE_NWS'] + }, + { + bossAppId: 'Lju5rPwt0QgXXh7Q', + titleId: 0x00040000000CB700n, + titleRegion: 'JPN', + name: 'Nintendo 3DS Guide: Louvre', + tasks: ['AL8_001'] + }, + { + bossAppId: 'llh2zGNci2sULyVX', + titleId: 0x00040000000DCA00n, + titleRegion: 'JPN', + name: 'ダンボール戦機W 超カスタム', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'Lw62Mz00pmT1osSJ', + titleId: 0x00040000000C3C00n, + titleRegion: 'JPN', + name: 'Rodea the Sky Soldier', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'lX2cH6BQVJFyjCfN', + titleId: 0x00040000001D6B00n, + titleRegion: 'UNK', + name: 'Yo-kai Watch 3', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'Lx3bOup9RTIcePKE', + titleId: 0x00040000000FD500n, + titleRegion: 'USA', + name: 'THEATRHYTHM FINAL FANTASY CURTAIN CALL', + tasks: ['DUOINFO'] + }, + { + bossAppId: 'M0EKKJWDaHUDcmRr', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'M7HNbBKOBj4NTTor', + titleId: 0x0004000000124A00n, + titleRegion: 'JPN', + name: 'リアル脱出ゲームxニンテンドー3DS 超破壊計画からの脱出', + tasks: ['FGONLYT', 'info'] + }, + { + bossAppId: 'mb2iVt7j6Wy6yxkb', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'MEXh2UqOKVgJS3h3', + titleId: 0x0004000000169D00n, + titleRegion: 'JPN', + name: 'アイカツ! My No.1 Stage!', + tasks: ['AKT410a'] + }, + { + bossAppId: 'MqPfuz5l3ptcMj23', + titleId: 0x00040000000D5700n, + titleRegion: 'EUR', + name: 'Nintendo 3DS Guide: Louvre (Deutsche Version)', + tasks: ['AL8_001'] + }, + { + bossAppId: 'n1cddgQeTxunOG2K', + titleId: 0x00040000001B4F00n, + titleRegion: 'EUR', + name: 'Miitopia', + tasks: ['Enquete', 'MiiDL'] + }, + { + bossAppId: 'NSWeucPFtbNNCx07', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'NU0xisOCz9UY0IBu', + titleId: 0x0004000000197200n, + titleRegion: 'JPN', + name: 'アイカツスターズ! Myスペシャルアピール', + tasks: ['AKT5PKG'] + }, + { + bossAppId: 'NXIWsA2Gm4EH219s', + titleId: 0x0004000000183100n, + titleRegion: 'KOR', + name: 'Rodea the Sky Soldier', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'OFypMD7hNCrRPthA', + titleId: 0x0004000000179400n, + titleRegion: 'USA', + name: 'Fire Emblem Fates Birthright', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'oGD8mLvDr8YY1L9E', + titleId: 0x00040000001C7900n, + titleRegion: 'UNK', + name: 'LAYTON\'S MYSTERY JOURNEY™ Katrielle and the Millionaires\'...', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'OkqvIoTur40udixa', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'Omwq2nEEfqZ9yo9S', + titleId: 0x00040000001CEC00n, + titleRegion: 'EUR', + name: 'YO-KAI WATCH BLASTERS RED CAT CORPS', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'oPuD6PkiPpx1EFiy', + titleId: 0x00040000001C1800n, + titleRegion: 'JPN', + name: 'The SNACK WORLD TREJARERS', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'oUF988iLJo3cWLji', + titleId: 0x00040000001D6900n, + titleRegion: 'UNK', + name: 'Yo-kai Watch 3', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'OvbmGLZ9senvgV3K', + titleId: 0x0004000000153500n, + titleRegion: 'USA', + name: 'Nintendo Badge Arcade Ver.1.3.1', + tasks: ['data', 'FGONLYT', 'news'] + }, + { + bossAppId: 'p1of6zTyKCnqOITZ', + titleId: 0x000400000009F100n, + titleRegion: 'EUR', + name: 'Fire Emblem: Awakening', + tasks: ['Quartz0', 'Quartz1'] + }, + { + bossAppId: 'p47ZQwc3R4qAmAYD', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['allhp', 'FGONLYT'] + }, + { + bossAppId: 'p5HtkudRvY55R212', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'PKeGb0m4S3Gv5jis', + titleId: 0x0004000000188E00n, + titleRegion: 'KOR', + name: 'Fire Emblem Fates', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'PpjtFCm4bQskyBUy', + titleId: 0x00040000000BA900n, + titleRegion: 'EUR', + name: 'Pokémon Mystery Dungeon Gates to Infinity', + tasks: ['PAINF00'] + }, + { + bossAppId: 'PV3ljR2w0UwrQfWk', + titleId: 0x00040000001B2800n, + titleRegion: 'UNK', + name: 'YO-KAI WATCH 2: PSYCHIC SPECTERS', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'PzpV1tz303wO66AD', + titleId: 0x00040000001CEF00n, + titleRegion: 'USA', + name: 'YO-KAI WATCH BLASTERS WHITE DOG SQUAD', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'pZWhL0tyf4FMCt8r', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'qAM6zmNEtT45AgUa', + titleId: 0x0004000000072300n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['Amb_jp', 'Amb'] + }, + { + bossAppId: 'qCcCCCLDwNMea0nq', + titleId: 0x0004000000176F00n, + titleRegion: 'USA', + name: 'The Legend of Zelda Tri Force Heroes', + tasks: ['info_00', 'Data', 'Data_00', 'Data_01'] + }, + { + bossAppId: 'QDlGiLVY6lFsOhV3', + titleId: 0x0004000000101200n, + titleRegion: 'JPN', + name: 'Puyopuyo Tetris', + tasks: ['boss1', 'boss2', 'boss3', 'FGONLYT'] + }, + { + bossAppId: 'qdy7ZP05GyiwgV7L', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'QfJQIU7IDmkaUuLt', + titleId: 0x0004000000072500n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['Amb_us', 'Amb'] + }, + { + bossAppId: 'qIe6CqkMDj1IaHoD', + titleId: 0x000400000014E000n, + titleRegion: 'JPN', + name: 'アイカツ! 365日のアイドルデイズ', + tasks: ['AKT365I'] + }, + { + bossAppId: 'QOnECUiVKl7wigDk', + titleId: 0x000400000005C300n, + titleRegion: 'JPN', + name: 'スライムもりもりドラゴンクエスト3 大海賊としっぽ団', + tasks: ['Strike1'] + }, + { + bossAppId: 'Qp03w0NGhViaEw4w', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'RA470KStGjDbVn4O', + titleId: 0x000400000012DC00n, + titleRegion: 'UNK', + name: 'ファイアーエムブレム if 白夜王国', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'rdWhyQMMUyKavjb4', + titleId: 0x000400000012DD00n, + titleRegion: 'UNK', + name: 'ファイアーエムブレム if 暗夜王国', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'rFgmDJuaHQO3TQFc', + titleId: 0x0004000000178800n, + titleRegion: 'JPN', + name: 'Miitopia', + tasks: ['Enquete', 'MiiDL'] + }, + { + bossAppId: 'rLNcsjO2p5oT0mqf', + titleId: 0x0004000000169500n, + titleRegion: 'UNK', + name: 'Rodea the Sky Soldier V1.01', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'RrNiKxhX1QFjyHSS', + titleId: 0x0004000000179600n, + titleRegion: 'USA', + name: 'Fire Emblem Fates Conquest', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'rsJRb5kSxvgEazo3', + titleId: 0x0004001000021800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['PANEL', 'MIIDATA', 'ETC', 'PANELLM', 'UPDATE', 'LEGEND'] + }, + { + bossAppId: 'ruVXn8rPH6AV83CV', + titleId: 0x00040000000C0200n, + titleRegion: 'JPN', + name: 'レイトン教授と 超文明Aの遺産', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'RXA1qO1PUOBzCrfe', + titleId: 0x0004000000167800n, + titleRegion: 'EUR', + name: 'YO-KAI WATCH', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'S1JafzaIw7C1Iia5', + titleId: 0x0004000000147200n, + titleRegion: 'EUR', + name: 'Little Battlers eXperience', + tasks: ['FGONLYT'] + }, + { + bossAppId: 's4T2rYW8ByXzSwQH', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 's6jspRUJkQbUMWgK', + titleId: 0x0004000000159500n, + titleRegion: 'USA', + name: 'Devil Survivor 2 Record Breaker', + tasks: ['Ds2ocTk'] + }, + { + bossAppId: 'S7tpGELe3d0jT75J', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'sbPtwI3pQEFTPEYu', + titleId: 0x0004000000168800n, + titleRegion: 'UNK', + name: 'Rodea the Sky Soldier', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'SS47bOArh8LhftQC', + titleId: 0x0004000000078500n, + titleRegion: 'JPN', + name: 'ダンボール戦機 爆ブースト', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'svTk7OJgceQciyt9', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'Sz5wLxuBiuxOV6qO', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'T3WjBdLsN0SbsBpB', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'T6hDbtUyZu9yGbiN', + titleId: 0x000400000017C200n, + titleRegion: 'UNK', + name: 'YO-KAI WATCH', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 't6hwMmI9dyfj3vFa', + titleId: 0x00040000000D5800n, + titleRegion: 'EUR', + name: 'Nintendo 3DS Guide: Louvre (Versione italiana)', + tasks: ['AL8_001'] + }, + { + bossAppId: 't7svvLkl31nkcVv9', + titleId: 0x00040000001B4E00n, + titleRegion: 'USA', + name: 'Miitopia', + tasks: ['Enquete', 'MiiDL'] + }, + { + bossAppId: 'TABYOV7qcJ3wjUlk', + titleId: 0x000400000004B700n, + titleRegion: 'JPN', + name: 'G1 Grand Prix Ver.1.1', + tasks: ['g1_plus'] + }, + { + bossAppId: 'TegobOkDc6fN2cLL', + titleId: 0x0004000000120C00n, + titleRegion: 'KOR', + name: 'Tomodachi Life', + tasks: ['tmTaskA'] + }, + { + bossAppId: 'TkTJ7y1N7b4bvs8m', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'TuKTMEFYh0sLS1O3', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'TVNiiigBTi1WzpnF', + titleId: 0x00040000001C6800n, + titleRegion: 'KOR', + name: 'Fire Emblem Echoes: Shadows of Valentia', + tasks: ['TASK00', 'FGONLYT', 'TASK01'] + }, + { + bossAppId: 'U2ZFOVWD6LH9r4JQ', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'uLrjbAsNhsH7Hc1N', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'Umg3IE2TYUrKEV1D', + titleId: 0x000400000012F800n, + titleRegion: 'JPN', + name: 'Yokai Watch 2 honke', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'uuI82221UKkqmtbp', + titleId: 0x0004003000008F02n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['basho4', 'basho5', 'sysmsg1', 'sysmsg2', 'sysmsg3', 'basho1', 'basho2', 'basho3', 'basho0'] + }, + { + bossAppId: 'UvNyA9yqIVj7LUml', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'uwGWP9VmceGVyTXv', + titleId: 0x00040000000E9A00n, + titleRegion: 'JPN', + name: 'シアトリズム FF カーテンコール', + tasks: ['DUOINFO'] + }, + { + bossAppId: 'V04RWTNqtHWfOaEB', + titleId: 0x00040000000EA900n, + titleRegion: 'USA', + name: 'Disney Magical World', + tasks: ['FGONLYT', 'BNMCASI', 'BNMCASL'] + }, + { + bossAppId: 'V5zESuakCkVaRtYB', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['data', 'news'] + }, + { + bossAppId: 'vCl8UPQ872Q2wzc0', + titleId: 0x000400000008C500n, + titleRegion: 'JPN', + name: 'トモダチコレクション 新生活', + tasks: ['tmTaskA'] + }, + { + bossAppId: 'vl1QWl9Lf3FhiH8r', + titleId: 0x000400000014AD00n, + titleRegion: 'JPN', + name: '大逆転裁判 -成歩堂龍ノ介の冒險-', + tasks: ['DSAIBAN'] + }, + { + bossAppId: 'w4J4AC8GMlfdkX9c', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'w8pqLFi96ZJLL6Co', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'WaOuJFckDnqKr5sK', + titleId: 0x00040000001D6800n, + titleRegion: 'EUR', + name: 'Yo-kai Watch 3', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'wbyZZlvq1j2QNBXj', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'wfHyw6Hj9b1UeCOd', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'WNcE0vrvCCO74nPV', + titleId: 0x000400000016DE00n, + titleRegion: 'USA', + name: 'SmileBASIC Ver.3.6.0', + tasks: ['ptc3nwe'] + }, + { + bossAppId: 'wXBTx8n5TvYbdKqD', + titleId: 0x000400000012F900n, + titleRegion: 'JPN', + name: 'Yokai Watch 2 ganso', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'wZgX791EQIHRicOk', + titleId: 0x00040000001C1900n, + titleRegion: 'JPN', + name: 'LAYTON\'S MYSTERY JOURNEY Katrielle and the Millionaires\'...', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'X0sh6ppe6HabEEcI', + titleId: 0x0004000000177000n, + titleRegion: 'EUR', + name: 'The Legend of Zelda Tri Force Heroes', + tasks: ['Info_00', 'Data', 'Data_00', 'Data_01'] + }, + { + bossAppId: 'X7wyEU7bsKuTwsw0', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'xHdRFJ7H9HIm2Eu5', + titleId: 0x0004000000030000n, + titleRegion: 'JPN', + name: 'Kid Icarus: Uprising', + tasks: ['SEED'] + }, + { + bossAppId: 'XrC4VtwZsJ3ro8sn', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'Y49VUspi8Bd4dqV9', + titleId: 0x00040000000CF500n, + titleRegion: 'JPN', + name: 'DQM2', + tasks: ['FGONLYT', 'WHALE01', 'WHALE02'] + }, + { + bossAppId: 'y67Up0WfC3ljfqic', + titleId: 0x0004000000030200n, + titleRegion: 'EUR', + name: 'Kid Icarus: Uprising', + tasks: ['SEED'] + }, + { + bossAppId: 'YapN7dMun6U6CVPx', + titleId: 0x000400100002CD00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['thmlist', 'thmtop', 'thmnews', 'thmdtls'] + }, + { + bossAppId: 'YbltplPRvEZ9620A', + titleId: 0x000400000007AF00n, + titleRegion: 'EUR', + name: 'New SUPER MARIO BROS. 2', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'YCHNmojskL3NNzNp', + titleId: 0x0004000000095C00n, + titleRegion: 'JPN', + name: 'Disney Magic Castle My Happy Life', + tasks: ['FGONLYT', 'BNMCASI', 'BNMCASL'] + }, + { + bossAppId: 'yj9NpW9dQjUYhN6i', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'ylHLOTvdmBIaUX4H', + titleId: 0x0004000000179800n, + titleRegion: 'UNK', + name: 'Fire Emblem Fates', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'YSMWAkVsyGiCsw36', + titleId: 0x00040000000F5100n, + titleRegion: 'JPN', + name: '三國志', + tasks: ['SGS_APD', 'SGS_INF'] + }, + { + bossAppId: 'ytEcS6DukxLYv42a', + titleId: 0x0004000000072000n, + titleRegion: 'JPN', + name: 'ファイアーエムブレム 覚醒', + tasks: ['Quartz0', 'Quartz1'] + }, + { + bossAppId: 'YzNlkoJpfJCGhJ5J', + titleId: 0x0004000000166E00n, + titleRegion: 'JPN', + name: 'モンハン日記 ぽかぽかアイルー村DX', + tasks: ['AIROUDX'] + }, + { + bossAppId: 'zFRghpEL0RQSrIkZ', + titleId: 0x0004000000144300n, + titleRegion: 'JPN', + name: 'ヒーローバンク2', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'ZtNvntGBgoUcf3hZ', + titleId: 0x00040000001CF000n, + titleRegion: 'EUR', + name: 'YO-KAI WATCH BLASTERS WHITE DOG SQUAD', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'ZweaaGi5WXHt5SO2', + titleId: 0x00040000000A0500n, + titleRegion: 'USA', + name: 'Fire Emblem Awakening', + tasks: ['Quartz0', 'Quartz1'] + }, + { + bossAppId: 'Zy3Cob0dHUBuRjK3', + titleId: 0x00040000000D5900n, + titleRegion: 'USA', + name: 'Nintendo 3DS Guide: Louvre (versión en español)', + tasks: ['AL8_001'] + }, + { + bossAppId: 'zY73NQ2CPef4Rfvu', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'ZzIGzz3gyLOPJUbk', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'RbCdW5sw3xphZ3x7', + titleId: 0x000400000014F100n, + titleRegion: 'USA', + name: 'Animal Crossing: Happy Home Designer', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'kdZ3BEkxoaIt2lLJ', + titleId: 0x00040000000D4400n, + titleRegion: 'JPN', + name: 'Oxford Reading Tree Floppy\'s Phonics vol.3', + tasks: ['ORT03'] + }, + { + bossAppId: '0a2LXft7kMJ39q4g', + titleId: 0x00040000000D4200n, + titleRegion: 'JPN', + name: 'Oxford Reading Tree Floppy\'s Phonics vol.2', + tasks: ['ORT02'] + }, + { + bossAppId: '6cDyiXh2nyoSmHeE', + titleId: 0x00040000000D4300n, + titleRegion: 'JPN', + name: 'Oxford Reading Tree Floppy\'s Phonics vol.1', + tasks: ['ORT01'] + }, + { + bossAppId: 'Tw16Bbw3beUmN6Jd', + titleId: 0x000400000014F000n, + titleRegion: 'JPN', + name: 'Animal Crossing: Happy Home Designer', + tasks: ['FGONLYT'] + }, + { + bossAppId: '7b6ZRfqtEUTwJ6rj', + titleId: 0x00040000000B1900n, + titleRegion: 'EUR', + name: 'Phonics Fun with Biff, Chip and Kipper vol.3', + tasks: ['ORT03'] + }, + { + bossAppId: 'PDhvZpH361L3gmm3', + titleId: 0x00040000000B1800n, + titleRegion: 'EUR', + name: 'Phonics Fun with Biff, Chip and Kipper vol.2', + tasks: ['ORT02'] + }, + { + bossAppId: 'AOdUQg8P4dXbIC4b', + titleId: 0x000400000014F200n, + titleRegion: 'EUR', + name: 'Animal Crossing: Happy Home Designer', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'tqdef27ymLVxfSaD', + titleId: 0x00040000000B1700n, + titleRegion: 'EUR', + name: 'Phonics Fun with Biff, Chip and Kipper vol.1', + tasks: ['ORT01'] + }, + { + bossAppId: 'qEFxWy2HQ5AX7qTX', + titleId: 0x0004000200178801n, + titleRegion: 'UNK', + name: 'Miitopia 体験版', + tasks: ['MiiDL'] + }, + { + bossAppId: 'EnLW1Fe8d0AfR3mE', + titleId: 0x0004000000055900n, + titleRegion: 'EUR', + name: 'Rabbids Rumble', + tasks: ['NEWRABS'] + }, + { + bossAppId: 'lX5BwDIdDsj6RkZg', + titleId: 0x000400000004C300n, + titleRegion: 'USA', + name: 'Imagine® babyz®', + tasks: ['RACHEL'] + }, + { + bossAppId: 'gQ3O9gb9VwvI3is6', + titleId: 0x0004000000182B00n, + titleRegion: 'KOR', + name: 'The Legend of Zelda Tri Force Heroes', + tasks: ['Data', 'Data_00', 'Data_01', 'Info_00'] + }, + { + bossAppId: 'Yh1oGYQ6MHHRacyR', + titleId: 0x0004000000182C00n, + titleRegion: 'TWN', + name: 'The Legend of Zelda Tri Force Heroes', + tasks: ['Data', 'Data_00', 'Data_01', 'Info_00'] + }, + { + bossAppId: 'qn6svwUCotEnBg24', + titleId: 0x00040000001B1700n, + titleRegion: 'JPN', + name: '好きなMiiで見る Miitopia 予告編', + tasks: ['MiiDL', 'Enquete'] + }, + { + bossAppId: 'QlZMDe93rUHQ7svF', + titleId: 0x000400000004E100n, + titleRegion: 'UNK', + name: 'Imagine™ Babies 3D', + tasks: ['RACHEL'] + }, + { + bossAppId: '6WLiMjiIXzGMmZ6n', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['Info', 'Info_00'] + }, + { + bossAppId: 'HpRhWfgZE0SoEiJ6', + titleId: 0x0004001000020B00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['NZOffPg'] + }, + { + bossAppId: 'BC10cFTXc0NHTohu', + titleId: 0x00040000001A2C00n, + titleRegion: 'JPN', + name: 'Swapdoodle', + tasks: ['RNG_EC1', 'RNG_MD1', 'RNG_LS1', 'RNG_NT1', 'RNG_NT2', 'RNG_GM1', 'RNG_DFR', 'RNG_U01', 'RNG_U02', 'RNG_U03', 'RNG_U04', 'RNG_U05', 'RNG_U06', 'RNG_U07', 'RNG_UUS'] + }, + { + bossAppId: 'QwyHOPV4LsvQ2I3U', + titleId: 0x00040000000F4E00n, + titleRegion: 'JPN', + name: 'NEWラブプラス+', + tasks: ['PTASK01', 'FGONLYT', 'PTASK02'] + }, + { + bossAppId: '7RW9z5Cb71Fpt1OE', + titleId: 0x0004001000020900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BGM1', 'TIGER1', 'BGM2'] + }, + { + bossAppId: 'TLmPhQflK0Yn6BeH', + titleId: 0x0004000000117200n, + titleRegion: 'JPN', + name: 'Petit computer 3 Ver.3.6.3', + tasks: ['ptc3nws'] + }, + { + bossAppId: 'pfQzJEaJOiPlLy3t', + titleId: 0x0004001000020800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['MIIDATA', 'ETC', 'PANELLM', 'UPDATE', 'PANEL', 'LEGEND'] + }, + { + bossAppId: '110Rzo2E1vYSfAz6', + titleId: 0x000400100002CC00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['thmtop', 'thmnews', 'thmdtls', 'thmlist'] + }, + { + bossAppId: 'gWr4JXxb2mKTG3lq', + titleId: 0x0004003000008202n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['basho1', 'basho2', 'basho3', 'basho4', 'basho5', 'sysmsg1', 'sysmsg2', 'sysmsg3', 'basho0'] + }, + { + bossAppId: 'WyI1CBPmzfm2nR2f', + titleId: 0x0004000000051600n, + titleRegion: 'JPN', + name: 'Swapnote', + tasks: ['JFR_LS2', 'JFR_NT1', 'JFR_NT2', 'JFR_NT3', 'JFR_AP2', 'JFR_GM2', 'JFR_DNT', 'JFR_DLS', 'JFR_DAP', 'JFR_DGM', 'JFR_DFR', 'JFR_U01', 'JFR_U02', 'JFR_U03', 'JFR_U04', 'JFR_U05', 'JFR_U06', 'JFR_U07', 'JFR_U08', 'JFR_U09', 'JFR_U10'] + }, + { + bossAppId: 'h0VRqB2YEgq39zvO', + titleId: 0x0004000000055D00n, + titleRegion: 'JPN', + name: 'Pokémon X', + tasks: ['horogra', 'FGONLYT'] + }, + { + bossAppId: 'H7btVjYWs7p5dGj6', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BREAK01', 'BREAK02', 'BREAK03', 'FGONLYT'] + }, + { + bossAppId: 'rO34jReRezcPv4HS', + titleId: 0x0004001000021B00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['NZOffPg'] + }, + { + bossAppId: 'b3Gq6LF6EqE1bvKy', + titleId: 0x00040000001B5100n, + titleRegion: 'JPN', + name: 'Pokémon Ultra Moon', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'OpIF7z4Uzjoww4Jw', + titleId: 0x00040000000DCD00n, + titleRegion: 'USA', + name: 'Mario Golf: World Tour', + tasks: ['spnt_t', 'spnt_x', 'FGONLYT'] + }, + { + bossAppId: 't9PZWHTdBZ57jYL6', + titleId: 0x00040000000A9000n, + titleRegion: 'EUR', + name: 'Nintendo presents: New Style Boutique', + tasks: ['girls', 'GMTM', 'info'] + }, + { + bossAppId: 'vD1TyxppgptrZdfK', + titleId: 0x0004001000027900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BGM1', 'BGM2', 'TIGER1'] + }, + { + bossAppId: 'vgBivYesOH9RS5I8', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'Slv7vHlUOfqrKMpz', + titleId: 0x0004000000055E00n, + titleRegion: 'JPN', + name: 'Pokémon Y', + tasks: ['FGONLYT', 'horogra'] + }, + { + bossAppId: 'EeqptDDf7v2IL7OP', + titleId: 0x00040000001AB800n, + titleRegion: 'USA', + name: 'Team Kirby Clash Deluxe', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'xNwjHvSQy3aGBb4C', + titleId: 0x0004000000137F00n, + titleRegion: 'UNK', + name: 'New SUPER MARIO BROS. 2: Special Edition', + tasks: ['patch', 'present'] + }, + { + bossAppId: 'AH3oZwrEbne6qHCO', + titleId: 0x0004001000022900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BGM1', 'BGM2', 'TIGER1'] + }, + { + bossAppId: 'tjca9oAeXj1R9EfU', + titleId: 0x0004001000022800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['MIIDATA', 'ETC', 'PANEL', 'PANELLM', 'UPDATE', 'LEGEND'] + }, + { + bossAppId: 'ZBq1ITue8b9aw64j', + titleId: 0x00040000000EE000n, + titleRegion: 'EUR', + name: 'Super Smash Bros. for Nintendo 3DS', + tasks: ['NEWS', 'amiibo', 'FGONLYT'] + }, + { + bossAppId: 'dMtiFHzm5OOf0y2O', + titleId: 0x000400100002CE00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['thmtop', 'thmnews', 'thmlist', 'thmdtls'] + }, + { + bossAppId: 'UrXSeurnxhPrq7AS', + titleId: 0x0004003000009802n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['sysmsg1', 'sysmsg2', 'sysmsg3', 'basho1', 'basho2', 'basho3', 'basho4', 'basho5', 'basho0'] + }, + { + bossAppId: 'Y5G9cKWHtFCre5ni', + titleId: 0x0004000000086400n, + titleRegion: 'UNK', + name: 'Animal Crossing New Leaf', + tasks: ['dlvexb', 'news', 'news_p', 'FGONLYT', 'pnews', 'dream', 'dream_p'] + }, + { + bossAppId: 'oC02RURp92o3o6XQ', + titleId: 0x00040000001B8D00n, + titleRegion: 'EUR', + name: 'Miitopia: Casting Call', + tasks: ['MiiDL', 'Enquete'] + }, + { + bossAppId: '8zLdgUwAeyD4Bn3b', + titleId: 0x0004000000102F00n, + titleRegion: 'JPN', + name: '太鼓の達人 どんとかつの時空大冒険', + tasks: ['DlcInfo', 'FGONLYT'] + }, + { + bossAppId: 'nn2h3ad9wxrX42UF', + titleId: 0x0004000000141000n, + titleRegion: 'JPN', + name: 'Pokémon Shuffle', + tasks: ['pktrpsh'] + }, + { + bossAppId: 'wfGi1AUhRfVvZ2KR', + titleId: 0x0004001000022B00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['NZOffPg'] + }, + { + bossAppId: '6HThIi5QlwGZNYFs', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: '8QjtffIMWFhiFpTz', + titleId: 0x0004000000164800n, + titleRegion: 'JPN', + name: 'Pokémon Sun', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'gfRN888w01lMrpSr', + titleId: 0x0004000000190E00n, + titleRegion: 'JPN', + name: '太鼓の達人 ドコドン! ミステリーアドベンチャー', + tasks: ['DlcInfo'] + }, + { + bossAppId: 'cRFY0WFHNjPh44If', + titleId: 0x000400000011C400n, + titleRegion: 'JPN', + name: 'Pokémon Omega Ruby', + tasks: ['horogra', 'FGONLYT'] + }, + { + bossAppId: 'wkiOHAEndV3fVHOF', + titleId: 0x00040000000DCE00n, + titleRegion: 'EUR', + name: 'Mario Golf: World Tour', + tasks: ['spnt_t', 'FGONLYT', 'spnt_x'] + }, + { + bossAppId: 'nTik1y6PI2QheWpi', + titleId: 0x00040000000D4B00n, + titleRegion: 'EUR', + name: 'Disney Magical World', + tasks: ['FGONLYT', 'BNMCASI', 'BNMCASL'] + }, + { + bossAppId: 'uAiur5PJTg0lFji6', + titleId: 0x00040000001C2600n, + titleRegion: 'EUR', + name: 'Nintendo Presents New Style Boutique 3', + tasks: ['NEWS', 'POSTER'] + }, + { + bossAppId: 'hatYUcYm85RPNRYv', + titleId: 0x0004000000032D00n, + titleRegion: 'USA', + name: 'SUPER STREET FIGHTER Ⅳ 3D EDITION', + tasks: ['SPA4APP'] + }, + { + bossAppId: 'nT8epEHh2yCVHTZk', + titleId: 0x0004000000198E00n, + titleRegion: 'USA', + name: 'Animal Crossing: New Leaf - Welcome amiibo', + tasks: ['news', 'news_p', 'dlvexb', 'FGONLYT', 'dream', 'dream_p'] + }, + { + bossAppId: 'P2mPjVWZUv2Dw8tw', + titleId: 0x000400000017EA00n, + titleRegion: 'USA', + name: 'HYRULE WARRIORS LEGENDS', + tasks: ['zdltdat', 'FGONLYT'] + }, + { + bossAppId: 'KJshB9eMGJCdvEBK', + titleId: 0x00040000000BA800n, + titleRegion: 'USA', + name: 'Pokémon Mystery Dungeon Gates to Infinity', + tasks: ['PAINF00'] + }, + { + bossAppId: 'tjZ5w8RGlAKd82y0', + titleId: 0x00040000001B2700n, + titleRegion: 'USA', + name: 'YO-KAI WATCH 2: PSYCHIC SPECTERS', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: 'GHsikcsO3zjZO8bm', + titleId: 0x00040000000A9100n, + titleRegion: 'USA', + name: 'Style Savvy: Trendsetters', + tasks: ['GMTM', 'info', 'girls'] + }, + { + bossAppId: 'kIHxKNVp3BHrr9bb', + titleId: 0x0004000000163200n, + titleRegion: 'EUR', + name: 'Fullblox', + tasks: ['annouce', 'FGONLYT'] + }, + { + bossAppId: 'qOBtWp4rFtI6PoPl', + titleId: 0x00040000000B4F00n, + titleRegion: 'EUR', + name: 'Fallblox', + tasks: ['JAU'] + }, + { + bossAppId: '3xU3A5ONnnC5ZxG9', + titleId: 0x0004000000031600n, + titleRegion: 'EUR', + name: 'nintendogs + cats', + tasks: ['task_EU'] + }, + { + bossAppId: 'n7KLcJdvAfV8jrr5', + titleId: 0x00040002001B4F01n, + titleRegion: 'UNK', + name: 'Miitopia: Demo Version', + tasks: ['MiiDL', 'Enquete'] + }, + { + bossAppId: 'ZMcgy6xtWsbsQE6P', + titleId: 0x0004000000198F00n, + titleRegion: 'EUR', + name: 'Animal Crossing: New Leaf - Welcome amiibo', + tasks: ['news_p', 'dlvexb', 'news', 'FGONLYT', 'dream', 'dream_p'] + }, + { + bossAppId: 'x7rRVYrCfwjmpOOX', + titleId: 0x0004000000163100n, + titleRegion: 'USA', + name: 'Stretchmo', + tasks: ['annouce', 'FGONLYT'] + }, + { + bossAppId: 'svHxRGPIJ2AWA5QJ', + titleId: 0x00040002001B4E01n, + titleRegion: 'UNK', + name: 'Miitopia Demo', + tasks: ['MiiDL', 'Enquete'] + }, + { + bossAppId: 'vlru9ZBRLJNiPwcm', + titleId: 0x000400000004AB00n, + titleRegion: 'UNK', + name: 'Nintendo Video', + tasks: ['ESP_NWS', 'ESP_CNF'] + }, + { + bossAppId: '6a2i4ewtEkJhoUS0', + titleId: 0x00040000000D9A00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SN7P'] + }, + { + bossAppId: '9vMstzmE8vG7XDOo', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['EWP_NWS', 'EWP_CNF'] + }, + { + bossAppId: 'VCcHstgngvFjng8c', + titleId: 0x0004000000032600n, + titleRegion: 'UNK', + name: 'Pokédex 3D', + tasks: ['basrao0', 'basrao1'] + }, + { + bossAppId: 'gekBa3SxolnJEyXJ', + titleId: 0x000400000016A100n, + titleRegion: 'EUR', + name: 'Nintendo presents New Style Boutique 2', + tasks: ['NEWS', 'POSTER', 'FGONLYT'] + }, + { + bossAppId: 'AOG9w8nspw1vnerP', + titleId: 0x0004000000084F00n, + titleRegion: 'EUR', + name: 'New Art Academy', + tasks: ['london1'] + }, + { + bossAppId: 'AZdLFt0b2qPaChrb', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['ptc3nws'] + }, + { + bossAppId: 'EPqObwlC8lOg12az', + titleId: 0x000400000013F800n, + titleRegion: 'UNK', + name: 'Rayman and Rabbids - Family Pack', + tasks: ['NEWRABS'] + }, + { + bossAppId: 'ytpowavlB10VzRDI', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['GS5'] + }, + { + bossAppId: 'bX6vTcQgC6ojhnM8', + titleId: 0x0004000000125D00n, + titleRegion: 'JPN', + name: 'Denpa Ningen RPG FREE!', + tasks: ['labos01', 'labos02', 'FGONLYT'] + }, + { + bossAppId: 'sKBxpm1uEGbaKj3Z', + titleId: 0x0004000000030C00n, + titleRegion: 'EUR', + name: 'nintendogs + cats', + tasks: ['task_EU'] + }, + { + bossAppId: 'd2AUo8Ku903Uz7Oy', + titleId: 0x000400000018FA00n, + titleRegion: 'EUR', + name: 'Phoenix Wright: Ace Attorney Spirit of Justice', + tasks: ['GS6'] + }, + { + bossAppId: 'h06QNObaOsEeWbLS', + titleId: 0x0004000000034D00n, + titleRegion: 'USA', + name: 'SAMURAI WARRIORS: Chronicles', + tasks: ['JMCDLC0', 'JMCDLC1', 'JMCDLC2'] + }, + { + bossAppId: 'au22VYFVclQ3t3fK', + titleId: 0x0004000000082000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQBE'] + }, + { + bossAppId: 'nw66VXNswH2IYbJk', + titleId: 0x0004000000082200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ5Z'] + }, + { + bossAppId: 'ClDVV1OhC2nPQfiz', + titleId: 0x0004000000081F00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQAE'] + }, + { + bossAppId: 'eD9gjUWfENOf7RX4', + titleId: 0x0004000000033C00n, + titleRegion: 'EUR', + name: 'SUPER STREET FIGHTER Ⅳ 3D EDITION', + tasks: ['SPA4APP'] + }, + { + bossAppId: 'Ne7L2H9zREHMz0QT', + titleId: 0x000400000017A800n, + titleRegion: 'UNK', + name: 'Fire Emblem Fates', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'asLNEQeEuHnm71Qo', + titleId: 0x0004000000079000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ5P'] + }, + { + bossAppId: 'KafSZFWApstWG1oT', + titleId: 0x0004000000038A00n, + titleRegion: 'EUR', + name: 'DEAD OR ALIVE Dimensions', + tasks: ['costume', 'foeevnt', 'info'] + }, + { + bossAppId: 's6jH81JriWS8EFL4', + titleId: 0x0004000000079300n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJYP'] + }, + { + bossAppId: '7FgFpXT7yPCI7d5K', + titleId: 0x0004000000079100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJZP'] + }, + { + bossAppId: 'H0aS5wTwrP8xYZbj', + titleId: 0x0004000000079400n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJVP'] + }, + { + bossAppId: '2jvsEXOhaXMEJRqU', + titleId: 0x0004000000079200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJXP'] + }, + { + bossAppId: '9eqERrZPrfwHsXw8', + titleId: 0x0004000000078E00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ6P'] + }, + { + bossAppId: 'pvKf3eKlZM86SftD', + titleId: 0x0004000000078F00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ7P'] + }, + { + bossAppId: 'Q9zLfpAkDHiIAQtD', + titleId: 0x0004000000080F00n, + titleRegion: 'UNK', + name: 'Mario & Sonic - London 2012 Virtual Card Album', + tasks: ['Z_CARD0'] + }, + { + bossAppId: 'KmJKbxqqJ1ISUDgN', + titleId: 0x0004000000155C00n, + titleRegion: 'EUR', + name: 'Gardening mama Forest Friends', + tasks: ['add-on'] + }, + { + bossAppId: 'rNRksUC46LBJUQps', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['weekly'] + }, + { + bossAppId: 'HxHhUeO7QSr7fTf0', + titleId: 0x0004000000137E00n, + titleRegion: 'UNK', + name: 'New Super Mario Bros. 2: Gold Edition', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'p2uEU1eJqc8tFvqS', + titleId: 0x0004000000034F00n, + titleRegion: 'USA', + name: 'DEAD OR ALIVE Dimensions', + tasks: ['costume', 'foeevnt', 'info'] + }, + { + bossAppId: 'MjQnB45RHoQhqIf4', + titleId: 0x00040000001C2500n, + titleRegion: 'USA', + name: 'Style Savvy: Styling Star', + tasks: ['NEWS', 'POSTER'] + }, + { + bossAppId: '0Fkb5zYwkA04nKYK', + titleId: 0x00040000001B8C00n, + titleRegion: 'USA', + name: 'Miitopia: Casting Call', + tasks: ['MiiDL', 'Enquete'] + }, + { + bossAppId: 'CkG3Q4aOnF2ALmis', + titleId: 0x000400000017EB00n, + titleRegion: 'EUR', + name: 'HYRULE WARRIORS LEGENDS', + tasks: ['zdltcm', 'zdltdat', 'FGONLYT'] + }, + { + bossAppId: 'Fk5Tz3LAVAZ2SvqP', + titleId: 0x0004000000031200n, + titleRegion: 'USA', + name: 'nintendogs + cats', + tasks: ['task_US'] + }, + { + bossAppId: 'UKUcmS9e4XbCsq3x', + titleId: 0x00040000000B8B00n, + titleRegion: 'JPN', + name: 'Super Smash Bros. for Nintendo 3DS', + tasks: ['NEWS', 'amiibo', 'FGONLYT'] + }, + { + bossAppId: 'QmLaxX884X14gr28', + titleId: 0x0004000000030D00n, + titleRegion: 'USA', + name: 'nintendogs + cats', + tasks: ['task_US'] + }, + { + bossAppId: 'eVnDsHy7ix4xPKLs', + titleId: 0x000400000019A900n, + titleRegion: 'USA', + name: 'Yo-kai Watch 2 Bony Spirits', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: 'fnCAH3KrGIl9dgSd', + titleId: 0x00040000001B5000n, + titleRegion: 'JPN', + name: 'Pokémon Ultra Sun', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'Co6AOQENlqDZDWNw', + titleId: 0x0004000000106200n, + titleRegion: 'EUR', + name: 'Nintendo Pocket Football Club', + tasks: ['hobbit2', 'hobbit1'] + }, + { + bossAppId: 'dJ4hv6uMXYNhbYGJ', + titleId: 0x000400000018AF00n, + titleRegion: 'UNK', + name: 'Disney Magical World 2', + tasks: ['MC2NWS', 'FGONLYT'] + }, + { + bossAppId: 'cd1qe3EXYyRuEdld', + titleId: 0x0004000000109800n, + titleRegion: 'JPN', + name: 'niconico', + tasks: ['news'] + }, + { + bossAppId: 'dpi2G2X44RWYcFKi', + titleId: 0x0004000000031100n, + titleRegion: 'EUR', + name: 'nintendogs + cats', + tasks: ['task_EU'] + }, + { + bossAppId: 'AQzcgnW7ozCHjk0e', + titleId: 0x000400000019AA00n, + titleRegion: 'USA', + name: 'Yo-kai Watch 2 Fleshy Souls', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: 'wKrAF9Z9DXZdXZQx', + titleId: 0x00040000001B9C00n, + titleRegion: 'USA', + name: 'Cooking Mama: Sweet Shop', + tasks: ['weekly'] + }, + { + bossAppId: 'ZzWhdXdx7bv5p8bP', + titleId: 0x0004000000095800n, + titleRegion: 'USA', + name: 'Art Academy Lessons for Everyone!', + tasks: ['london1'] + }, + { + bossAppId: 'ME0gcVqjUOI7Xdz4', + titleId: 0x0004000000075100n, + titleRegion: 'USA', + name: 'Heroes of Ruin', + tasks: ['IMAHERO'] + }, + { + bossAppId: 'dAWysFTQbZ8yEBBd', + titleId: 0x000400000016F200n, + titleRegion: 'USA', + name: 'SAMURAI WARRIORS: Chronicles 3', + tasks: ['SC3DLC0', 'FGONLYT'] + }, + { + bossAppId: 'Bj9GROrjies5XMOa', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['GS5'] + }, + { + bossAppId: '65YaZF7skxsw5cdY', + titleId: 0x000400000018F400n, + titleRegion: 'USA', + name: 'Phoenix Wright: Ace Attorney Spirit of Justice', + tasks: ['GS6'] + }, + { + bossAppId: 'InwBEl2EEhnbr5EC', + titleId: 0x0004000000031700n, + titleRegion: 'USA', + name: 'nintendogs + cats', + tasks: ['task_US'] + }, + { + bossAppId: 'gfRXFtm46orJeAoe', + titleId: 0x0004000000086200n, + titleRegion: 'UNK', + name: 'Animal Crossing New Leaf', + tasks: ['news', 'news_p', 'pnews', 'FGONLYT', 'dream', 'dream_p'] + }, + { + bossAppId: 'h9yS1FDz26uNM9lj', + titleId: 0x00040000000B4A00n, + titleRegion: 'JPN', + name: 'Tongariboshi To Maho no Machi', + tasks: ['tg4', 'tg4home', 'FGONLYT'] + }, + { + bossAppId: 'hLU6fGE4vzOsrlmw', + titleId: 0x0004000000031000n, + titleRegion: 'JPN', + name: 'nintendogs + cats', + tasks: ['task_JP'] + }, + { + bossAppId: '5H2IWrmjM8PiN5SV', + titleId: 0x00040000000B4700n, + titleRegion: 'UNK', + name: 'どこでも本屋さん', + tasks: ['diibo00', 'diibo02', 'diibo01', 'FGONLYT'] + }, + { + bossAppId: 'E5GjXFKIix3yGqgK', + titleId: 0x0004000000099700n, + titleRegion: 'JPN', + name: 'RECOCHOKU', + tasks: ['RECOBNT', 'FGONLYT'] + }, + { + bossAppId: 'VEPSPo1c55w9ydWV', + titleId: 0x0004000000157B00n, + titleRegion: 'JPN', + name: '引ク出ス ヒッパランド', + tasks: ['annouce', 'FGONLYT'] + }, + { + bossAppId: 'sA2vpi47OPnR78Ge', + titleId: 0x0004000000180A00n, + titleRegion: 'JPN', + name: 'ChoChariso ver.1.2', + tasks: ['taskImg', 'taskID', 'taskFlg'] + }, + { + bossAppId: 'PAuBgnpL4UGBpboR', + titleId: 0x0004000000141D00n, + titleRegion: 'JPN', + name: '大合奏!バンドブラザーズP デビュー', + tasks: ['allhp'] + }, + { + bossAppId: '76cct9EjYJJ17weU', + titleId: 0x00040002000C9E01n, + titleRegion: 'UNK', + name: '(体験版)実写でちびロボ!', + tasks: ['mesdat', 'dat'] + }, + { + bossAppId: 'AcYFJX9F9mNBA34T', + titleId: 0x000400000005C800n, + titleRegion: 'USA', + name: 'Crosswords Plus', + tasks: ['CW_WEEK', 'CW_RGN'] + }, + { + bossAppId: 'ZppKDH9dFwci1xuM', + titleId: 0x00040000000F1500n, + titleRegion: 'USA', + name: 'JUMP TRIALS SUPREME', + tasks: ['dqu5vs9', 'x7a9u1y'] + }, + { + bossAppId: '7mXz0DXR4b4CdD8r', + titleId: 0x0004000000175E00n, + titleRegion: 'JPN', + name: 'Pokémon Moon', + tasks: ['FGONLYT'] + }, + { + bossAppId: '5XpCMVOjCHJOLeh6', + titleId: 0x0004000000079700n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ6E'] + }, + { + bossAppId: 'Kgg1X843lmd8Qx4H', + titleId: 0x0004000000074000n, + titleRegion: 'EUR', + name: 'Heroes of Ruin', + tasks: ['IMAHERO'] + }, + { + bossAppId: 'nmU69oGizolOYGSc', + titleId: 0x00040000000FCA00n, + titleRegion: 'EUR', + name: 'THEATRHYTHM FINAL FANTASY CURTAIN CALL', + tasks: ['DUOINFO'] + }, + { + bossAppId: 'OXEubGVtO2scl6qt', + titleId: 0x0004000000132500n, + titleRegion: 'JPN', + name: 'Code Name: S.T.E.A.M.', + tasks: ['NEWS'] + }, + { + bossAppId: 'wujsRH3kkwPXc5p9', + titleId: 0x0004000000179500n, + titleRegion: 'UNK', + name: 'Fire Emblem Fates Birthright', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'xxVJpVHJVIX1unyz', + titleId: 0x000400000018B000n, + titleRegion: 'JPN', + name: 'YO-KAI SANGOKUSHI', + tasks: ['yk_news', 'FGONLYT'] + }, + { + bossAppId: '9bUGzV1nUl8wxkPY', + titleId: 0x0004000000198D00n, + titleRegion: 'JPN', + name: 'Animal Crossing: New Leaf - Welcome amiibo', + tasks: ['news', 'news_p', 'FGONLYT', 'dream', 'dream_p'] + }, + { + bossAppId: 'MbNeoMBork0x5U9D', + titleId: 0x0004000000137D00n, + titleRegion: 'UNK', + name: 'New SUPER MARIO BROS. 2: Gold Edition', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'wESLUfmWpm322iLC', + titleId: 0x00040000000A5300n, + titleRegion: 'JPN', + name: 'Mario Golf: World Tour', + tasks: ['spnt_t', 'spnt_x', 'FGONLYT'] + }, + { + bossAppId: 'TISVjpYatgrPk9By', + titleId: 0x00040000001B9500n, + titleRegion: 'EUR', + name: 'Cooking Mama: Sweet Shop', + tasks: ['weekly'] + }, + { + bossAppId: 'JK6188WXQQi46VGk', + titleId: 0x0004000000192F00n, + titleRegion: 'EUR', + name: 'JUMP TRIALS SUPREME', + tasks: ['dqu5vs9', 'x7a9u1y'] + }, + { + bossAppId: 'r8bumkFVkPoNhc9c', + titleId: 0x00040000000E6700n, + titleRegion: 'USA', + name: 'Skater Cat', + tasks: ['boss1', 'FGONLYT'] + }, + { + bossAppId: 'INTHvj3cbPjeXOIp', + titleId: 0x00040000001B7400n, + titleRegion: 'EUR', + name: 'Pic-a-Pix Colour', + tasks: ['NEWPACK'] + }, + { + bossAppId: 'yYddhP0UtHK9WfXf', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['item'] + }, + { + bossAppId: 'oRUi0TW540yJfcFI', + titleId: 0x0004000000165400n, + titleRegion: 'UNK', + name: 'Karaoke JOYSOUND', + tasks: ['KRKJ001'] + }, + { + bossAppId: 'YTCRfaHNkqkyAoR0', + titleId: 0x0004000000036F00n, + titleRegion: 'JPN', + name: '花といきもの立体図鑑(Ver. 1.1)', + tasks: ['sflower', 'sflocal'] + }, + { + bossAppId: 'eOywu80lc2iSQrGQ', + titleId: 0x0004000000079800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ7E'] + }, + { + bossAppId: 'Bjlq1Q32KdkSazF0', + titleId: 0x0004000000179700n, + titleRegion: 'UNK', + name: 'Fire Emblem Fates Conquest', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'fY56qJDdLSRY1zwh', + titleId: 0x00040000000F7D00n, + titleRegion: 'EUR', + name: 'Inazuma Eleven 3 Lightning Bolt', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: 'wkGEp5JKVH1sHK4j', + titleId: 0x0004000000055400n, + titleRegion: 'USA', + name: 'Rabbids Rumble', + tasks: ['NEWRABS'] + }, + { + bossAppId: 'zXfHuj8k9mgzKrpM', + titleId: 0x0004000000196500n, + titleRegion: 'USA', + name: 'Nintendo presents Style Savvy: Fashion Forward', + tasks: ['NEWS', 'POSTER', 'FGONLYT'] + }, + { + bossAppId: 'gaM5Hqp8XiLyp3pU', + titleId: 0x000400000008CD00n, + titleRegion: 'USA', + name: 'Sparkle Snapshots 3D', + tasks: ['NadlTsk'] + }, + { + bossAppId: 'LJ569qrUUTcV5MWa', + titleId: 0x0004000000079A00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJZE'] + }, + { + bossAppId: 'LvMjVLZITq73RRfN', + titleId: 0x0004000000079B00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJXE'] + }, + { + bossAppId: '8Urw573qCWPTMHt1', + titleId: 0x0004000000079D00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJVE'] + }, + { + bossAppId: '7oIHiYcoNKUFPXvo', + titleId: 0x0004000000079C00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJYE'] + }, + { + bossAppId: 'tGgHjsqB1lxz7laV', + titleId: 0x000400000018AE00n, + titleRegion: 'UNK', + name: 'Disney Magical World 2', + tasks: ['MC2NWS', 'FGONLYT'] + }, + { + bossAppId: 'zd1wgUFX5L74rQjK', + titleId: 0x00040000000F1F00n, + titleRegion: 'UNK', + name: 'Pokemon tretta LAB MAIN SYSTEM', + tasks: ['Notice', 'FGONLYT'] + }, + { + bossAppId: 'qJastY9KRjKlmpx7', + titleId: 0x000400000009AC00n, + titleRegion: 'UNK', + name: 'Déco Photo 3D', + tasks: ['NadlTsk'] + }, + { + bossAppId: 'AUonPa828Z3PtkeG', + titleId: 0x00040000000ECB00n, + titleRegion: 'EUR', + name: 'Skater Cat', + tasks: ['boss1'] + }, + { + bossAppId: '00ydKyJUl4PPMEM8', + titleId: 0x000400000016E600n, + titleRegion: 'EUR', + name: 'Devil Survivor 2: Record Breaker', + tasks: ['Ds2ocTk'] + }, + { + bossAppId: 'Sv1HvEbP3JAcxqvf', + titleId: 0x00040000000FC800n, + titleRegion: 'UNK', + name: 'honto for ニンテンドー3DS', + tasks: ['honto00'] + }, + { + bossAppId: '40Ox7iM6hyihJOiz', + titleId: 0x000400000016B200n, + titleRegion: 'JPN', + name: 'ファイアーエムブレム if', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'yMkh08f1lW0DafIi', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQ2J'] + }, + { + bossAppId: 'Nctmz7ODGZtPf69M', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'o8ZLmDtyL1j7SXyQ', + titleId: 0x0004000000144400n, + titleRegion: 'JPN', + name: 'ワンピース 超グランドバトル!X', + tasks: ['compe'] + }, + { + bossAppId: 'snhV1gVHgJLnjhid', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BREAK01', 'BREAK03', 'BREAK02', 'FGONLYT'] + }, + { + bossAppId: 'x7D5Xg4ezlaVAJyE', + titleId: 0x00040000000ABD00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNJJ'] + }, + { + bossAppId: 'jPDSZKCbiBNMsEq9', + titleId: 0x0004000000182F00n, + titleRegion: 'JPN', + name: 'PUZZLE & DRAGONS CROSS GOD type', + tasks: ['INFITEM', 'INFEVT', 'INFPUB', 'FGONLYT'] + }, + { + bossAppId: 'im5wnzidQQs5uoKe', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['mesdat', 'dat'] + }, + { + bossAppId: 'eFz4GxIjoCccrVSV', + titleId: 0x0004000000031500n, + titleRegion: 'JPN', + name: 'nintendogs + cats', + tasks: ['task_JP'] + }, + { + bossAppId: 'KWrfnjzo2LKhZy4s', + titleId: 0x00040000000C3A00n, + titleRegion: 'JPN', + name: 'デビルサバイバー2 ブレイクレコード', + tasks: ['Ds2ocTk'] + }, + { + bossAppId: '8qwKR8OSh5GOapqe', + titleId: 0x000400300000B102n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['basho0', 'basho1', 'basho2', 'basho3', 'basho4', 'basho5', 'sysmsg1', 'sysmsg2', 'sysmsg3'] + }, + { + bossAppId: 'j7tO10mcyMaz5ig6', + titleId: 0x000400300000A102n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['basho5', 'basho4', 'basho0', 'basho1', 'basho2', 'basho3', 'sysmsg1', 'sysmsg2', 'sysmsg3'] + }, + { + bossAppId: 'kRlrRG3XMEZShz9a', + titleId: 0x0004001000028900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['BGM1', 'BGM2', 'TIGER1'] + }, + { + bossAppId: 'VPFqk5XKRlkeei6V', + titleId: 0x000400300000A902n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['basho0', 'basho1', 'basho2', 'basho3', 'basho4', 'basho5', 'sysmsg1', 'sysmsg2', 'sysmsg3'] + }, + { + bossAppId: 'pgNuDMNihGLmz8EQ', + titleId: 0x00040000000AAD00n, + titleRegion: 'JPN', + name: 'Crashmo', + tasks: ['JAU'] + }, + { + bossAppId: 'HiSDUEw0agNVyQQh', + titleId: 0x0004000000090700n, + titleRegion: 'TWN', + name: 'nintendogs + cats', + tasks: ['task_CH', 'task_TW'] + }, + { + bossAppId: 'dnfsqL9yQI945m6N', + titleId: 0x0004000000160A00n, + titleRegion: 'JPN', + name: 'Greeting cards', + tasks: ['KGCTask'] + }, + { + bossAppId: '0AzfphwTbGD6PV9y', + titleId: 0x00040000000DC900n, + titleRegion: 'JPN', + name: 'ちび☆デビ!2 ~魔法のゆめえほん~', + tasks: ['chibi2'] + }, + { + bossAppId: 'CU4vwXZB68V4M5dd', + titleId: 0x000400000005D100n, + titleRegion: 'JPN', + name: 'GIRLS MODE よくばり宣言! トキメキUP!', + tasks: ['girls', 'info', 'GMTM', 'plus'] + }, + { + bossAppId: 'SAV0iYvYj2VU3WLm', + titleId: 0x00040000000A3500n, + titleRegion: 'UNK', + name: 'Skylanders Giants™', + tasks: ['SLGTASK'] + }, + { + bossAppId: 'HfTkA7wv9Xq7uCba', + titleId: 0x00040000000A9800n, + titleRegion: 'JPN', + name: 'モデル☆おしゃれオーディション プラチナ', + tasks: ['MO3000', 'MO3001'] + }, + { + bossAppId: 'pIjCA0tlmrOQXZSx', + titleId: 0x0004000000170900n, + titleRegion: 'JPN', + name: 'Nobunaga\'s Ambition 2', + tasks: ['NA2F000'] + }, + { + bossAppId: 'RjlyCZV4WuOMLhdI', + titleId: 0x00040000000E9000n, + titleRegion: 'JPN', + name: 'JUMP TRIALS SUPREME', + tasks: ['dqu5vs9', 'x7a9u1y'] + }, + { + bossAppId: 'yivdj3PrGaiLW2q4', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['GS6'] + }, + { + bossAppId: '6KHx0Ly7P5BU2gMs', + titleId: 0x0004000000039B00n, + titleRegion: 'JPN', + name: 'ポケットサッカーリーグ カルチョビット', + tasks: ['hobbit1'] + }, + { + bossAppId: 'qZJMRyERIxHiLC94', + titleId: 0x0004000000090900n, + titleRegion: 'TWN', + name: 'nintendogs + cats', + tasks: ['task_CH', 'task_TW'] + }, + { + bossAppId: 'Vgw1b4CRc6tTJAuW', + titleId: 0x00040000000F4900n, + titleRegion: 'JPN', + name: 'Gardening Mama Mama and Her Forest Friends', + tasks: ['add-on'] + }, + { + bossAppId: '51DuhF1iuLnlykWa', + titleId: 0x0004000000169C00n, + titleRegion: 'JPN', + name: 'ちゃおイラストクラブ', + tasks: ['BMDJFOR'] + }, + { + bossAppId: 'Sx5rjDrrIqdlfck9', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['news'] + }, + { + bossAppId: 'SAV0iYvYj2VU3WLm', + titleId: 0x00040000000E7F00n, + titleRegion: 'UNK', + name: 'Skylanders SWAP Force™', + tasks: ['SLGTASK'] + }, + { + bossAppId: 'SAV0iYvYj2VU3WLm', + titleId: 0x00040000000E6500n, + titleRegion: 'USA', + name: 'Skylanders SWAP Force™', + tasks: ['SLGTASK'] + }, + { + bossAppId: 'hKHhZ3jJeHOdJ1dh', + titleId: 0x0004000000178000n, + titleRegion: 'JPN', + name: 'ドリームガール プルミエ', + tasks: ['MO5000', 'MO5001'] + }, + { + bossAppId: 'SVCHF7ayaPXyV8da', + titleId: 0x000400000012D700n, + titleRegion: 'USA', + name: 'Gardening Mama 2 Forest Friends', + tasks: ['add-on'] + }, + { + bossAppId: 'DRegPN9WZfUgWBa2', + titleId: 0x0004000000090800n, + titleRegion: 'TWN', + name: 'nintendogs + cats', + tasks: ['task_CH', 'task_TW'] + }, + { + bossAppId: 'gSU6Tz36YEQi4zCC', + titleId: 0x00040000000C9E00n, + titleRegion: 'JPN', + name: '実写でちびロボ!', + tasks: ['mesdat', 'dat', 'mesdat2'] + }, + { + bossAppId: 'SXDe1d24jEHW5Ddy', + titleId: 0x000400000017B700n, + titleRegion: 'UNK', + name: 'Fire Emblem if', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'd9DVo37kDln5fsfx', + titleId: 0x00040000000F9C00n, + titleRegion: 'JPN', + name: 'A Penguin\'s Troubles +', + tasks: ['PGDL000'] + }, + { + bossAppId: 'gt2GNl074uoLp9VR', + titleId: 0x00040000000BAA00n, + titleRegion: 'JPN', + name: '逆転裁判5', + tasks: ['GS5'] + }, + { + bossAppId: 'VEC7I57wA8AL2D5C', + titleId: 0x00040000000B6300n, + titleRegion: 'UNK', + name: 'Girls\' Fashion Shoot', + tasks: ['MO2000', 'MO2001'] + }, + { + bossAppId: 'nhIB89NAaMLIOJq5', + titleId: 0x0004000000053600n, + titleRegion: 'JPN', + name: 'ペンギンの問題 ザ・ウォーズ 1.1', + tasks: ['penwar0'] + }, + { + bossAppId: 'K3cSIGJ2usdI91Mn', + titleId: 0x0004000000078B00n, + titleRegion: 'JPN', + name: 'PROFESSOR LAYTON VS ACE ATTORNEY', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'jm9bQFGAHJxWREsU', + titleId: 0x00040000000A4D00n, + titleRegion: 'JPN', + name: '戦国無双クロニクル セカンド 更新データ', + tasks: ['SC2DLC0'] + }, + { + bossAppId: 'SAV0iYvYj2VU3WLm', + titleId: 0x0004000000091D00n, + titleRegion: 'USA', + name: 'Skylanders Giants™', + tasks: ['SLGTASK'] + }, + { + bossAppId: 'QehlmytT9n95yB2j', + titleId: 0x0004000000197F00n, + titleRegion: 'JPN', + name: 'アイカツスターズ! ファーストアピール', + tasks: ['AKT5F2P'] + }, + { + bossAppId: 'EGcBTbh5QbT5PjmV', + titleId: 0x0004000000156300n, + titleRegion: 'JPN', + name: 'SHIKAKUMARU', + tasks: ['SAM01'] + }, + { + bossAppId: 'iZWCrW72Sx50ObA4', + titleId: 0x0004000000112800n, + titleRegion: 'JPN', + name: 'マギ 新たなる世界', + tasks: ['mg20bst'] + }, + { + bossAppId: 'r6xdZT62kmgPHbxK', + titleId: 0x0004000000156A00n, + titleRegion: 'JPN', + name: 'SHIKAKUMARU', + tasks: ['SAM02'] + }, + { + bossAppId: '4zQ0tSdYf0MbtMcf', + titleId: 0x00040000000C9500n, + titleRegion: 'JPN', + name: 'peakvox Mew Mew Train', + tasks: ['3MD', '3MD2'] + }, + { + bossAppId: 'UikW5d4DcNOUltN1', + titleId: 0x0004000000156500n, + titleRegion: 'JPN', + name: 'SHIKAKUMARU', + tasks: ['SAM03'] + }, + { + bossAppId: '3B5MnqGceTBxIyHq', + titleId: 0x00040000000FB200n, + titleRegion: 'JPN', + name: 'Nobunaga\'s Ambition', + tasks: ['NA3DFR0'] + }, + { + bossAppId: 'SAV0iYvYj2VU3WLm', + titleId: 0x00040000000A3600n, + titleRegion: 'UNK', + name: 'Skylanders Giants™', + tasks: ['SLGTASK'] + }, + { + bossAppId: 'VEC7I57wA8AL2D5C', + titleId: 0x00040000000B6F00n, + titleRegion: 'UNK', + name: 'Girls\' Fashion Shoot', + tasks: ['MO2000'] + }, + { + bossAppId: 'VEC7I57wA8AL2D5C', + titleId: 0x0004000000065700n, + titleRegion: 'JPN', + name: 'nicola監修 モデル☆おしゃれオーディション2', + tasks: ['MO2000', 'MO2001'] + }, + { + bossAppId: 'RkgNduGP6rRkubPC', + titleId: 0x0004000000068300n, + titleRegion: 'UNK', + name: 'とびだすプリクラ☆ キラデコレボリューション', + tasks: ['NadlTsk'] + }, + { + bossAppId: 'oqxYfft6J9X33Xcm', + titleId: 0x0004000000170400n, + titleRegion: 'JPN', + name: 'nicopuchi', + tasks: ['BNPJFOR'] + }, + { + bossAppId: 'rS9N1JsddhwfKlVp', + titleId: 0x000400000014DF00n, + titleRegion: 'JPN', + name: '戦国無双 Chronicle 3', + tasks: ['SC3DLC0', 'FGONLYT'] + }, + { + bossAppId: 'VEC7I57wA8AL2D5C', + titleId: 0x00040000000B4800n, + titleRegion: 'UNK', + name: 'Girls\' Fashion Shoot', + tasks: ['MO2000'] + }, + { + bossAppId: '8xzpJYrYTQQLhs5U', + titleId: 0x00040000000FB400n, + titleRegion: 'JPN', + name: 'モデル☆おしゃれオーディション ドリームガール', + tasks: ['MO4000', 'MO4001'] + }, + { + bossAppId: 'huaVwH8vmGDlynY5', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'gQdtAv8S5QqhAMq6', + titleId: 0x00040000000F8000n, + titleRegion: 'UNK', + name: 'Inazuma Eleven 3 Team Ogre Attacks!', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: 'TPqWmZW8y6NEWz9N', + titleId: 0x000400000008B500n, + titleRegion: 'UNK', + name: 'MARIO KART 7', + tasks: ['comm', 'ranking', 'ghost'] + }, + { + bossAppId: '4813nyO3ACQf2cwz', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['item'] + }, + { + bossAppId: 'sk1jvwem58tprBkZ', + titleId: 0x0004000000032000n, + titleRegion: 'JPN', + name: 'DEAD OR ALIVE Dimensions', + tasks: ['costume', 'foeevnt', 'info'] + }, + { + bossAppId: 'crLAF7DBkd3coa3v', + titleId: 0x0004000000030500n, + titleRegion: 'JPN', + name: 'SUPER STREET FIGHTER Ⅳ 3D EDITION', + tasks: ['SPA4APP'] + }, + { + bossAppId: '9DLWs8Zl50qCaWxp', + titleId: 0x0004000000040200n, + titleRegion: 'UNK', + name: 'SAMURAI WARRIORS: Chronicles', + tasks: ['JMCDLC0', 'JMCDLC1', 'JMCDLC2'] + }, + { + bossAppId: 'gzPtSkOPonlun2Ug', + titleId: 0x0004000000173B00n, + titleRegion: 'EUR', + name: 'SAMURAI WARRIORS: Chronicles 3', + tasks: ['SC3DLC0', 'FGONLYT'] + }, + { + bossAppId: 'J8jLC3PD2ERSNrmK', + titleId: 0x000400000008CC00n, + titleRegion: 'EUR', + name: 'Sparkle Snapshots 3D', + tasks: ['NadlTsk'] + }, + { + bossAppId: 'HEMBJCoTyLKjm9pU', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['weekly'] + }, + { + bossAppId: 'OTjVevatduCI863l', + titleId: 0x00040000000F7B00n, + titleRegion: 'EUR', + name: 'Inazuma Eleven 3 Bomb Blast', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: 'NiAJFnn4fyAAiggq', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['PTASK01', 'ITASK01', 'PTASK02'] + }, + { + bossAppId: 'oHl1Ay3RPWlWngp3', + titleId: 0x0004000000030B00n, + titleRegion: 'JPN', + name: 'nintendogs + cats', + tasks: ['task_JP'] + }, + { + bossAppId: 'V3xe0ZHtBxzJwJtL', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'v5fxzRdEaBW5ujTi', + titleId: 0x00040000000CC900n, + titleRegion: 'KOR', + name: 'Swapnote', + tasks: ['JFR_NT1', 'JFR_NT2', 'JFR_NT3', 'JFR_LS2', 'JFR_AP2', 'JFR_DFR', 'JFR_GM2', 'JFR_U01', 'JFR_U02', 'JFR_U03', 'JFR_U04', 'JFR_U05', 'JFR_U06', 'JFR_U07', 'JFR_UUS'] + }, + { + bossAppId: 'OjI5Ysd6x9mFk22c', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['zdltdat', 'FGONLYT'] + }, + { + bossAppId: '99qH2qwTlJy9XDKs', + titleId: 0x0004000000030F00n, + titleRegion: 'KOR', + name: 'nintendogs + cats', + tasks: ['task_KR'] + }, + { + bossAppId: '7Fqa3EZYYlvKEh5v', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQ5J'] + }, + { + bossAppId: '4r5wpBYnaf6rldJ3', + titleId: 0x00040000000F7E00n, + titleRegion: 'UNK', + name: 'Inazuma Eleven 3 Lightning Bolt', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: '3ERCXw1170g54UBs', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JC9K'] + }, + { + bossAppId: '9dXYzFjKdKbJMCWe', + titleId: 0x0004001000027800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['MIIDATA', 'PANEL', 'PANELLM', 'LEGEND', 'UPDATE'] + }, + { + bossAppId: 'hMwDQ4myueRLe1J9', + titleId: 0x0004000000092E00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JZ8J'] + }, + { + bossAppId: 'L7j0ivgZfXSskmmY', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JMCDLC0', 'JMCDLC1', 'JMCDLC2'] + }, + { + bossAppId: 'CAsKat7WKddnAZX3', + titleId: 0x000400000019F600n, + titleRegion: 'JPN', + name: 'Girls Mode 4 スター☆スタイリスト', + tasks: ['NEWS', 'POSTER'] + }, + { + bossAppId: 'S8m7Nskf5LDHOAFr', + titleId: 0x0004000000182E00n, + titleRegion: 'JPN', + name: 'PUZZLE & DRAGONS CROSS DRAGON type', + tasks: ['INFITEM', 'INFEVT', 'INFPUB', 'FGONLYT'] + }, + { + bossAppId: 's9byficX77gemj2B', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'hXDTHQ4WSEmdUHmH', + titleId: 0x000400000012D800n, + titleRegion: 'JPN', + name: 'GIRLS MODE 3 キラキラ☆コーデ', + tasks: ['NEWS', 'POSTER', 'FGONLYT'] + }, + { + bossAppId: 'txq0HfojeCsTXwdB', + titleId: 0x0004001000028800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['PANELLM', 'MIIDATA', 'PANEL', 'LEGEND'] + }, + { + bossAppId: 'v77n3f1JniCBO6DY', + titleId: 0x00040000000EC000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPAJ'] + }, + { + bossAppId: 'IWu6JntKmBfUFK0D', + titleId: 0x0004000000105E00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPFJ'] + }, + { + bossAppId: 'x6bxVnGPunxrUEvR', + titleId: 0x00040000000B9700n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SN2J'] + }, + { + bossAppId: 'oYzBvnIepXcRAWB8', + titleId: 0x0004000000155D00n, + titleRegion: 'KOR', + name: 'Cooking Mama', + tasks: ['weekly'] + }, + { + bossAppId: 'wSajRGAQdY6dffMd', + titleId: 0x000400000006C500n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJGJ'] + }, + { + bossAppId: 'riNWIl9p0ZdQORe5', + titleId: 0x0004000000095700n, + titleRegion: 'JPN', + name: 'New Art Academy', + tasks: ['london1'] + }, + { + bossAppId: 'IGctFNjjb8E1TONI', + titleId: 0x000400000018F100n, + titleRegion: 'USA', + name: 'Dragon Quest VIII', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'XT7XAO9p7C9Rfvvx', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQZJ'] + }, + { + bossAppId: 'LUX1uLHXK82phCiX', + titleId: 0x00040000001AB900n, + titleRegion: 'EUR', + name: 'Team Kirby Clash Deluxe', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'uzKXEvs4hNMA25oG', + titleId: 0x0004000000137800n, + titleRegion: 'EUR', + name: 'Puzzle & Dragons Z + Super Mario Bros. Edition', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'iR1A5X1wiLEkSynO', + titleId: 0x0004000000167C00n, + titleRegion: 'KOR', + name: 'Super Smash Bros. for Nintendo 3DS', + tasks: ['NEWS', 'amiibo', 'FGONLYT'] + }, + { + bossAppId: '24XiNVrAmhfjS82s', + titleId: 0x0004000000137700n, + titleRegion: 'USA', + name: 'Puzzle & Dragons Z + Super Mario Bros. Edition', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'S2OgfKUn0ZVKpO2G', + titleId: 0x000400000004DC00n, + titleRegion: 'UNK', + name: 'Imagine™ Fashion World 3D', + tasks: ['NL00001'] + }, + { + bossAppId: 'drVvdr4bzLr25i9t', + titleId: 0x00040000001A0000n, + titleRegion: 'JPN', + name: 'Team Kirby Clash Deluxe', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'l3gOdKu5CZ45zxTk', + titleId: 0x000400000009AD00n, + titleRegion: 'UNK', + name: 'Foto Glitter 3D', + tasks: ['NadlTsk'] + }, + { + bossAppId: '0AgBH2eLZV5YqTMW', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: '4n7UxjbCGnuq5FYy', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: '7Cs4OOUfDbCC9su5', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'Duu3faN13x95aVMY', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'ERl9vWxc1lKwBCYk', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'NbmO38IyGwh7nt7t', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'X00vNlT4qqcyzxlL', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'XUWPufbAdkuBsj26', + titleId: 0x0004000000198500n, + titleRegion: 'KOR', + name: 'Dragon Quest VIII', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'YqwaX4YDDsNecFub', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['miku_01', 'miku'] + }, + { + bossAppId: 'oX0GI0HwZvOJHMo9', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'tY7USAZRiWlUgQVe', + titleId: 0x0004000000047800n, + titleRegion: 'USA', + name: 'Imagine™ Fashion Life', + tasks: ['NL00001'] + }, + { + bossAppId: 'wJoxCKvP6AFEHecL', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'wTy7ND4OqUdfR7nH', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: 'xSycjLbQyJJBlc7x', + titleId: 0x000400000015CD00n, + titleRegion: 'JPN', + name: 'Dragon Quest VIII', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'xedkyOsho6iccqUT', + titleId: 0x000400000018F200n, + titleRegion: 'EUR', + name: 'Dragon Quest VIII', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'y1kgSu9oYpgC1oOm', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0000001'] + }, + { + bossAppId: '2qmhIndG8vluZoEO', + titleId: 0x000400000008C800n, + titleRegion: 'JPN', + name: 'DRAGON QUEST Ⅹ Morph de Battle', + tasks: ['lucky01', 'lucky03', 'lucky02'] + }, + { + bossAppId: 'SDIA7fBPxg6X01AW', + titleId: 0x0004001000026800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['PANEL', 'MIIDATA', 'LEGEND', 'PANELLM'] + }, + { + bossAppId: 'oUTbEtsyaJ7Fzx4h', + titleId: 0x000400000006B600n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJYJ'] + }, + { + bossAppId: 'ZwMc34IYS96zWJRD', + titleId: 0x00040000000F7F00n, + titleRegion: 'EUR', + name: 'Inazuma Eleven 3 Team Ogre Attacks!', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: '4mMi9ll6zFrSftt9', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQ4J'] + }, + { + bossAppId: 'MSN04ONcEZyPL5dW', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPEJ'] + }, + { + bossAppId: '2hZYkL2D64eCrTzg', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['MESSAGE', 'DL'] + }, + { + bossAppId: 'FMhu53ZtDLnz2ZZ1', + titleId: 0x0004000000168600n, + titleRegion: 'TWN', + name: 'Super Smash Bros. for Nintendo 3DS', + tasks: ['NEWS', 'amiibo', 'FGONLYT'] + }, + { + bossAppId: 'xjMBeunnHUfxuK2d', + titleId: 0x0004000000151F00n, + titleRegion: 'TWN', + name: 'Pokémon Art Academy', + tasks: ['pnote', 'FGONLYT'] + }, + { + bossAppId: 'kuQRrwInvnbJvfa6', + titleId: 0x0004000000143400n, + titleRegion: 'UNK', + name: 'New SUPER MARIO BROS. 2: Gold Edition', + tasks: ['present', 'patch'] + }, + { + bossAppId: 'HWC1oxxoFRhulvSL', + titleId: 0x00040000000B9600n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNZJ'] + }, + { + bossAppId: 'cxWp2lsjKrG7diwk', + titleId: 0x00040000000B9200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNVJ'] + }, + { + bossAppId: 'h8S6cmA4FdBvPYZc', + titleId: 0x00040000000EC100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPBJ'] + }, + { + bossAppId: 'i3iGTnVbl4tKi9KW', + titleId: 0x00040000000EBF00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SN9J'] + }, + { + bossAppId: 'NxEAqX0zZl4f0Im0', + titleId: 0x00040000000FA500n, + titleRegion: 'JPN', + name: 'クッキングママ5', + tasks: ['weekly'] + }, + { + bossAppId: 'N6VoGN3wJ698hOiI', + titleId: 0x0004000000106000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPHJ'] + }, + { + bossAppId: '9SCQWB4mFxhoXyQK', + titleId: 0x00040000001AE200n, + titleRegion: 'JPN', + name: '大逆転裁判2 ―成歩堂龍ノ介の覺悟―', + tasks: ['SHOLMES'] + }, + { + bossAppId: 'pdR21q4ZRkxaSeVJ', + titleId: 0x000400000006BC00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJSJ'] + }, + { + bossAppId: '3BraiaIl0mpnuoOA', + titleId: 0x00040000000AA000n, + titleRegion: 'JPN', + name: 'Unknown', + tasks: ['MAJ'] + }, + { + bossAppId: 'YVH4WfoFszQwwG3w', + titleId: 0x00040000000CCA00n, + titleRegion: 'TWN', + name: 'Swapnote', + tasks: ['JFR_LS2', 'JFR_NT1', 'JFR_NT2', 'JFR_NT3', 'JFR_AP2', 'JFR_DFR', 'JFR_GM2', 'JFR_U01', 'JFR_U02', 'JFR_U03', 'JFR_U04', 'JFR_U05', 'JFR_U06', 'JFR_U07', 'JFR_UUS'] + }, + { + bossAppId: 'DciJTpUDHw8APohR', + titleId: 0x000400000009AB00n, + titleRegion: 'UNK', + name: 'Foto-Zauber 3D', + tasks: ['NadlTsk'] + }, + { + bossAppId: 'HLLrgikagMK0pzbs', + titleId: 0x000400000006B000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ6J'] + }, + { + bossAppId: 'prddiG9hlRiH8NEC', + titleId: 0x000400000006AF00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ7J'] + }, + { + bossAppId: 'cmL3N7HUvMJGfGFp', + titleId: 0x000400000006B100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ5J'] + }, + { + bossAppId: 'Ryh3zAYKR1zjJWc3', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['zdltdat'] + }, + { + bossAppId: 'HvwgRXicx4vCBFTL', + titleId: 0x0004000000199000n, + titleRegion: 'KOR', + name: 'Animal Crossing: New Leaf - Welcome amiibo', + tasks: ['news', 'news_p', 'dream', 'dream_p', 'FGONLYT'] + }, + { + bossAppId: 'OIC58iiMakdqQSLH', + titleId: 0x00040000000C4F00n, + titleRegion: 'KOR', + name: 'Nintendo presents: New Style Boutique', + tasks: ['GMTM', 'girls', 'info'] + }, + { + bossAppId: 'J0ovIyLP9tYaJqaq', + titleId: 0x00040000000D1C00n, + titleRegion: 'JPN', + name: 'Unknown', + tasks: ['MBE'] + }, + { + bossAppId: 'BAIXhLmD2rGJVAvQ', + titleId: 0x00040000000AA400n, + titleRegion: 'JPN', + name: 'Unknown', + tasks: ['MAL'] + }, + { + bossAppId: 'AOGimz8AhOCZLJks', + titleId: 0x00040000000D1B00n, + titleRegion: 'JPN', + name: 'Unknown', + tasks: ['MBD'] + }, + { + bossAppId: 'MSswwTMHhCzsTg4l', + titleId: 0x00040000000AA300n, + titleRegion: 'JPN', + name: 'Unknown', + tasks: ['MAK'] + }, + { + bossAppId: '83j0DZ0YuEbd6OTR', + titleId: 0x000400000006B700n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJXJ'] + }, + { + bossAppId: '0kXLgixMNsmA8Ulw', + titleId: 0x000400000006AE00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ8J'] + }, + { + bossAppId: '34pqkzXxOsXTju6N', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQBJ'] + }, + { + bossAppId: '3mqMq8p8Vu7clovO', + titleId: 0x000400000006C800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJDJ'] + }, + { + bossAppId: '4z69DLEmm8eWmAWh', + titleId: 0x00040000000AC100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNNJ'] + }, + { + bossAppId: '7NAqDIHJVMr4Svya', + titleId: 0x0004000000092F00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JZ7J'] + }, + { + bossAppId: 'A9ikIHjkbhlbkQGZ', + titleId: 0x000400000006C700n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJEJ'] + }, + { + bossAppId: 'aA9hBmp4EagWTNM5', + titleId: 0x000400000006AD00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ9J'] + }, + { + bossAppId: 'AY0oNJfCHmoAqyGJ', + titleId: 0x000400000006C200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJLJ'] + }, + { + bossAppId: 'BDgoKPBE6iIm6GvU', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQYJ'] + }, + { + bossAppId: 'BnYCVYAGDPA2ODsF', + titleId: 0x000400000006BB00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJTJ'] + }, + { + bossAppId: 'bO0kDGfd70dY8jP7', + titleId: 0x000400000007B500n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQEJ'] + }, + { + bossAppId: 'bP3RO5aNzfiUhTqG', + titleId: 0x0004000000110D00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPKJ'] + }, + { + bossAppId: 'BXzMU4QDn6gJzHL8', + titleId: 0x000400000006C900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJCJ'] + }, + { + bossAppId: 'dCdWyWjvMdwC55jp', + titleId: 0x00040000000B9500n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNYJ'] + }, + { + bossAppId: 'eUDyHqSSYL8Dbnw7', + titleId: 0x000400000006BE00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJQJ'] + }, + { + bossAppId: 'gH85V9hCAuisHPiI', + titleId: 0x00040000000A9D00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNSJ'] + }, + { + bossAppId: 'gIKNS5K3QTXk8uZ0', + titleId: 0x000400000006B300n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ3J'] + }, + { + bossAppId: 'IwPki1shz2Sa5Jtf', + titleId: 0x000400000006C000n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJNJ'] + }, + { + bossAppId: 'kIk3rjwlpys7cBgb', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'MjC52YHyd1mUxL3K', + titleId: 0x000400000006B500n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJZJ'] + }, + { + bossAppId: 'Mrv7MaHG7NUKIssp', + titleId: 0x00040000000ABF00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNLJ'] + }, + { + bossAppId: 'mvFYPcqk08wjN1Lf', + titleId: 0x0004000000105F00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SPGJ'] + }, + { + bossAppId: 'NSfT7yEow0T98638', + titleId: 0x000400000007B700n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQGJ'] + }, + { + bossAppId: 'O5RNJSKQSbaGMWMe', + titleId: 0x000400000006C300n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJKJ'] + }, + { + bossAppId: 'O7IQrnoLPrUckjY6', + titleId: 0x000400000006C400n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJHJ'] + }, + { + bossAppId: 'PR5223DvK2yJeLm7', + titleId: 0x00040000000A0300n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNDJ'] + }, + { + bossAppId: 'QI6XYVa7oCGM7PSb', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SN5J'] + }, + { + bossAppId: 'QIr38311UsvE7kpy', + titleId: 0x000400000006C600n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJFJ'] + }, + { + bossAppId: 'rbxZVYCrNIIZiV1Y', + titleId: 0x00040000000B9400n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNXJ'] + }, + { + bossAppId: 'Rk3lD7SViJwXHqxY', + titleId: 0x00040000000A0100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNCJ'] + }, + { + bossAppId: 's332O6HVmb1mrZlJ', + titleId: 0x000400000007B100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQAJ'] + }, + { + bossAppId: 'szZ7fUtxGj3C8vAm', + titleId: 0x000400000006BF00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJPJ'] + }, + { + bossAppId: 'ufWtleWnZVr175yL', + titleId: 0x000400000006B900n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJVJ'] + }, + { + bossAppId: 'uZp0tj5WSQXC1ajP', + titleId: 0x00040000000A9C00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNRJ'] + }, + { + bossAppId: 'VaaaQIH2Y4MCHZoE', + titleId: 0x000400000006B400n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ2J'] + }, + { + bossAppId: 'ValTqsbfLsdprJ9D', + titleId: 0x00040000000B9300n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNWJ'] + }, + { + bossAppId: 'VJotYqkLqCNb6WEZ', + titleId: 0x000400000006C100n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJMJ'] + }, + { + bossAppId: 'vWajW15RdM4DoMkY', + titleId: 0x000400000007B600n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQFJ'] + }, + { + bossAppId: 'wiDPs6x6gT5GuQ0L', + titleId: 0x000400000006B800n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJWJ'] + }, + { + bossAppId: 'wIfkwczLlzXY1Ih0', + titleId: 0x00040000000A0400n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNEJ'] + }, + { + bossAppId: 'WvnKyyWlllNC1P3l', + titleId: 0x00040000000A9B00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNQJ'] + }, + { + bossAppId: 'x8ak29C2mgHwjQm5', + titleId: 0x000400000006B200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJ4J'] + }, + { + bossAppId: 'XAdIxXWPa1PDWMkm', + titleId: 0x0004000000150A00n, + titleRegion: 'UNK', + name: 'Imagine® Collection', + tasks: ['RACHEL'] + }, + { + bossAppId: 'XH9X43PmZ9lGVT7i', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JQ6J'] + }, + { + bossAppId: 'Yhg9sWpNbOT95HTu', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'yMP5bCaKlAgItWqV', + titleId: 0x00040000000D9200n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SN6J'] + }, + { + bossAppId: 'yoN5MAgfB2yTKrLz', + titleId: 0x000400000006BA00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JJUJ'] + }, + { + bossAppId: 'zztKIdGngfCS9cS5', + titleId: 0x00040000000A9A00n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['SNPJ'] + }, + { + bossAppId: 'Bo1owHMnKHjFK2Nk', + titleId: 0x0004000000143300n, + titleRegion: 'UNK', + name: 'New SUPER MARIO BROS. 2: Gold Edition', + tasks: ['patch', 'present'] + }, + { + bossAppId: 'MLJEOnEgGcwd5uaP', + titleId: 0x00040000000F7C00n, + titleRegion: 'UNK', + name: 'Inazuma Eleven 3 Bomb Blast', + tasks: ['news', 'FGONLYT'] + }, + { + bossAppId: '1j4M12R6obSARq8y', + titleId: 0x0004000000086500n, + titleRegion: 'UNK', + name: 'Animal Crossing New Leaf', + tasks: ['news', 'pnews', 'FGONLYT', 'dream', 'dream_p', 'news_p'] + }, + { + bossAppId: '3l15Yk6cOm0AWsSP', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'BUvZ7pfEEhcLt4B2', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['news'] + }, + { + bossAppId: 'Qq4luhjWR94QqvhX', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['daily'] + }, + { + bossAppId: 'f9sKNCJ94RghSSyg', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['GS5'] + }, + { + bossAppId: 'CnnviXAnc0qGGTio', + titleId: 0x00040000000FEB00n, + titleRegion: 'KOR', + name: 'Phoenix Wright: Ace Attorney Dual Destinies', + tasks: ['GS5'] + }, + { + bossAppId: 'fMTQWnFhZQ2BijI5', + titleId: 0x000400000017B800n, + titleRegion: 'TWN', + name: 'Fire Emblem if', + tasks: ['TASK00', 'TASK02'] + }, + { + bossAppId: 'Xv543XtNNYZU2OXz', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'eyCw6Qph1Hz1uQmC', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['JAU'] + }, + { + bossAppId: 'jeUQ3nnY70ddJ87q', + titleId: 0x000400000009AE00n, + titleRegion: 'UNK', + name: 'Cámara Glamour 3D', + tasks: ['NadlTsk'] + }, + { + bossAppId: 'bRWXguqQXXAT99Th', + titleId: 0x00040000001BC900n, + titleRegion: 'TWN', + name: 'Fire Emblem Echoes: Shadows of Valentia', + tasks: ['TASK00', 'FGONLYT', 'TASK01'] + }, + { + bossAppId: '1XCuNDvhVlp5r7mO', + titleId: 0x00040000000C3900n, + titleRegion: 'JPN', + name: 'パズドラZ', + tasks: ['FGONLYT', 'PDZDLDG', 'PDZDLNP', 'PDZDLTI'] + }, + { + bossAppId: '7f094kCjE5HDVGlu', + titleId: 0x00040000000CBD00n, + titleRegion: 'JPN', + name: 'Oshaberi-Chat', + tasks: ['JCTTask'] + }, + { + bossAppId: 'aOJyK9TocYWjxMRR', + titleId: 0x000400000004A900n, + titleRegion: 'UNK', + name: 'Nintendo Video', + tasks: ['ESJ_CNF', 'ESJ_MD1', 'ESJ_MD2', 'ESJ_MD3', 'ESJ_MD4'] + }, + { + bossAppId: 'G0VUurQf4EVYe37v', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['0ER_CNF', '1ER_DBN', '2ER_DCM', '3ER_AX1', '4ER_CX1', '5ER_AX2', '6ER_CX2', '7ER_AX3', '8ER_CX3', '9ER_NTD'] + }, + { + bossAppId: '3PSCke6Q3B84IzWl', + titleId: 0x0004000000031900n, + titleRegion: 'KOR', + name: 'nintendogs + cats', + tasks: ['task_KR'] + }, + { + bossAppId: 'XaphN80NdywuA9Ef', + titleId: 0x0004000000155E00n, + titleRegion: 'KOR', + name: 'Gardening Mama', + tasks: ['add-on'] + }, + { + bossAppId: '0hYP8YjQTWpdlt0v', + titleId: 0x0004000000031400n, + titleRegion: 'KOR', + name: 'nintendogs + cats', + tasks: ['task_KR'] + }, + { + bossAppId: '7HXSa828OHpIlsMh', + titleId: 0x000400000012C700n, + titleRegion: 'KOR', + name: 'Disney Magical World', + tasks: ['BNMCASI', 'BNMCASL', 'FGONLYT'] + }, + { + bossAppId: 'j3JOMyqvsV4ocbUM', + titleId: 0x0004000000150B00n, + titleRegion: 'KOR', + name: 'Pokémon Art Academy', + tasks: ['FGONLYT', 'pnote'] + }, + { + bossAppId: 'l0BkjqC5MI1bEY29', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['Ds2ocTk'] + }, + { + bossAppId: 'sKFrnzQCMkGb60y7', + titleId: 0x0004000000106300n, + titleRegion: 'KOR', + name: 'Mario Golf: World Tour', + tasks: ['FGONLYT', 'spnt_t', 'spnt_x'] + }, + { + bossAppId: '3V3eHMNiNmweRqQO', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'HFnqrJv4fmyO5ItT', + titleId: 0x0004000000091F00n, + titleRegion: 'KOR', + name: 'Model Audition Superstar 2 ', + tasks: ['MO2000', 'MO2001'] + }, + { + bossAppId: 'IyMJMSHvuD2nC8r1', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'JJVi8mFl1LRVk4gS', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'VQU2jX0YCWHynn03', + titleId: 0x000400000015A700n, + titleRegion: 'KOR', + name: 'Puzzle & Dragons Z + Super Mario Bros. Edition', + tasks: ['FGONLYT'] + }, + { + bossAppId: 'cm0cPPTXpEkGEeII', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'y4Q3Z35IQzh6nWds', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['FGONLYT', 'news'] + }, + { + bossAppId: 'aKd4hjqxg87UDbwy', + titleId: 0x000400000018F900n, + titleRegion: 'KOR', + name: 'Phoenix Wright: Ace Attorney Spirit of Justice', + tasks: ['GS6'] + }, + { + bossAppId: '4q11abiKEAOBaBMD', + titleId: 0x000400000016D100n, + titleRegion: 'TWN', + name: 'Puzzle & Dragons Z + Super Mario Bros. Edition', + tasks: ['FGONLYT'] + }, + { + bossAppId: '3yire4GGq6BfeD71', + titleId: 0x0n, + titleRegion: 'UNK', + name: 'Unknown', + tasks: ['news'] + } + ] + }; +} diff --git a/src/services/grpc/boss/v2/list-tasks.ts b/src/services/grpc/boss/v2/list-tasks.ts new file mode 100644 index 0000000..5164e2d --- /dev/null +++ b/src/services/grpc/boss/v2/list-tasks.ts @@ -0,0 +1,22 @@ +import { getAllTasks } from '@/database'; +import type { ListTasksResponse } from '@pretendonetwork/grpc/boss/v2/list_tasks'; + +export async function listTasks(): Promise { + const tasks = await getAllTasks(false); + + return { + tasks: tasks.map(task => ({ + deleted: task.deleted, + id: task.id, + inGameId: task.in_game_id, + bossAppId: task.boss_app_id, + creatorPid: task.creator_pid, + status: task.status, + interval: 0, // TODO - Don't stub this + titleId: BigInt(parseInt(task.title_id, 16)), + description: task.description, + createdTimestamp: task.created, + updatedTimestamp: task.updated + })) + }; +} diff --git a/src/services/grpc/boss/v2/middleware/api-key-middleware.ts b/src/services/grpc/boss/v2/middleware/api-key-middleware.ts new file mode 100644 index 0000000..211c2cc --- /dev/null +++ b/src/services/grpc/boss/v2/middleware/api-key-middleware.ts @@ -0,0 +1,16 @@ +import { Status, ServerError } from 'nice-grpc'; +import { config } from '@/config-manager'; +import type { ServerMiddlewareCall, CallContext } from 'nice-grpc'; + +export async function* apiKeyMiddleware( + call: ServerMiddlewareCall, + context: CallContext +): AsyncGenerator { + const apiKey: string | undefined = context.metadata.get('X-API-Key'); + + if (!apiKey || apiKey !== config.grpc.boss.api_key) { + throw new ServerError(Status.UNAUTHENTICATED, 'Missing or invalid API key'); + } + + return yield* call.next(call.request, context); +} diff --git a/src/services/grpc/boss/v2/middleware/authentication-middleware.ts b/src/services/grpc/boss/v2/middleware/authentication-middleware.ts new file mode 100644 index 0000000..7456dd4 --- /dev/null +++ b/src/services/grpc/boss/v2/middleware/authentication-middleware.ts @@ -0,0 +1,52 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getUserDataByToken } from '@/util'; +import type { ServerMiddlewareCall, CallContext } from 'nice-grpc'; +import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc'; +import type { PNIDPermissionFlags } from '@pretendonetwork/grpc/account/pnid_permission_flags'; + +export type AuthenticationCallContextExt = { + user: GetUserDataResponse | null; +}; + +export async function* authenticationMiddleware( + call: ServerMiddlewareCall, + context: CallContext +): AsyncGenerator { + const token: string | undefined = context.metadata.get('X-Token')?.trim(); + + try { + let user: GetUserDataResponse | null = null; + + if (token) { + user = await getUserDataByToken(token); + if (!user) { + throw new ServerError(Status.UNAUTHENTICATED, 'User could not be found'); + } + } + + return yield* call.next(call.request, { + ...context, + user + }); + } catch (error) { + let message: string = 'Unknown server error'; + + console.log(error); + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.INVALID_ARGUMENT, message); + } +} + +export function hasPermission(ctx: AuthenticationCallContextExt, perm: keyof PNIDPermissionFlags): boolean { + if (!ctx.user) { + return true; // Non users are always allowed + } + if (!ctx.user.permissions) { + return false; // No permissions, no entry + } + return ctx.user.permissions[perm]; +} diff --git a/src/services/grpc/boss/v2/register-task.ts b/src/services/grpc/boss/v2/register-task.ts new file mode 100644 index 0000000..6ec103c --- /dev/null +++ b/src/services/grpc/boss/v2/register-task.ts @@ -0,0 +1,85 @@ +import { ServerError, Status } from 'nice-grpc'; +import { getTask } from '@/database'; +import { Task } from '@/models/task'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { RegisterTaskRequest, RegisterTaskResponse } from '@pretendonetwork/grpc/boss/v2/register_task'; + +const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/; + +export async function registerTask(request: RegisterTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'createBossTasks')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to register new tasks'); + } + + const taskID = request.id.trim(); + const bossAppID = request.bossAppId.trim(); + const titleID = request.titleId.toString(16).toLowerCase().padStart(16, '0'); + const status = request.status; + const interval = request.interval; + const description = request.description.trim(); + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + if (bossAppID.length !== 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must be 16 characters'); + } + + if (!BOSS_APP_ID_FILTER_REGEX.test(bossAppID)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must only contain letters and numbers'); + } + + if (await getTask(bossAppID, taskID)) { + throw new ServerError(Status.ALREADY_EXISTS, `Task ${taskID} already exists for BOSS app ${bossAppID}`); + } + + if (status !== 'open' && status !== 'close') { + throw new ServerError(Status.INVALID_ARGUMENT, `Status ${status} is invalid`); + } + + // * BOSS tasks have 2 IDs + // * - 1: The ID which is registered in-game + // * - 2: The ID which is registered on the server + // * The in-game task ID can be any length, but the + // * ID registered on the server is capped at 7 characters. + // * When querying tasks in the API, the server ignores + // * all characters after the 7th. For example, Splatoon + // * registers task optdata2 in-game, but the server + // * tracks it as task optdata + + const task = await Task.create({ + id: taskID.slice(0, 7), + in_game_id: taskID, + boss_app_id: bossAppID, + creator_pid: context.user?.pid, + status, + interval, + title_id: titleID, + description: description, + created: Date.now(), + updated: Date.now() + }); + + return { + task: { + deleted: task.deleted, + id: task.id, + inGameId: task.in_game_id, + bossAppId: task.boss_app_id, + creatorPid: task.creator_pid, + status: task.status, + interval: task.interval, + titleId: BigInt(parseInt(task.title_id, 16)), + description: task.description, + createdTimestamp: task.created, + updatedTimestamp: task.updated + } + }; +} diff --git a/src/services/grpc/boss/v2/update-file-metadata-ctr.ts b/src/services/grpc/boss/v2/update-file-metadata-ctr.ts new file mode 100644 index 0000000..5819e7e --- /dev/null +++ b/src/services/grpc/boss/v2/update-file-metadata-ctr.ts @@ -0,0 +1,55 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getCTRTaskFileBySerialNumber } from '@/database'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { UpdateFileMetadataCTRRequest } from '@pretendonetwork/grpc/boss/v2/update_file_metadata_ctr'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; + +export async function updateFileMetadataCTR(request: UpdateFileMetadataCTRRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'updateBossFiles')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to update file metadata'); + } + + const serialNumber = request.dataId; + const updateData = request.updateData; + + if (!serialNumber) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file serial number'); + } + + if (!updateData) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file update data'); + } + + const file = await getCTRTaskFileBySerialNumber(serialNumber); + + if (!file || file.deleted) { + throw new ServerError(Status.INVALID_ARGUMENT, `File ${serialNumber} not found`); + } + + file.task_id = updateData.taskId.slice(0, 7); + file.boss_app_id = updateData.bossAppId; + file.supported_countries = updateData.supportedCountries; + file.supported_languages = updateData.supportedLanguages; + file.attributes.attribute1 = updateData.attributes ? updateData.attributes.attribute1 : file.attributes.attribute1; + file.attributes.attribute2 = updateData.attributes ? updateData.attributes.attribute2 : file.attributes.attribute2; + file.attributes.attribute3 = updateData.attributes ? updateData.attributes.attribute3 : file.attributes.attribute3; + file.attributes.description = updateData.attributes ? updateData.attributes.description : file.attributes.description; + file.name = updateData.name; + file.updated = BigInt(Date.now()); + + if (updateData.payloadContents.length !== 0) { + file.payload_contents = updateData.payloadContents.map(payload => ({ + title_id: payload.titleId, + content_datatype: payload.contentDatatype, + ns_data_id: payload.nsDataId, + version: payload.version, + size: payload.size + })); + } + + await file.save(); + + return {}; +} diff --git a/src/services/grpc/boss/v2/update-file-metadata-wup.ts b/src/services/grpc/boss/v2/update-file-metadata-wup.ts new file mode 100644 index 0000000..2cdfbeb --- /dev/null +++ b/src/services/grpc/boss/v2/update-file-metadata-wup.ts @@ -0,0 +1,58 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getWUPTaskFileByDataID } from '@/database'; +import { isValidFileNotifyCondition, isValidFileType } from '@/util'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { UpdateFileMetadataWUPRequest } from '@pretendonetwork/grpc/boss/v2/update_file_metadata_wup'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; + +export async function updateFileMetadataWUP(request: UpdateFileMetadataWUPRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'updateBossFiles')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to update file metadata'); + } + + const dataID = request.dataId; + const updateData = request.updateData; + + if (!dataID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file data ID'); + } + + if (!updateData) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing file update data'); + } + + const file = await getWUPTaskFileByDataID(dataID); + + if (!file || file.deleted) { + throw new ServerError(Status.INVALID_ARGUMENT, `File ${dataID} not found`); + } + + if (!isValidFileType(updateData.type)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${updateData.type} is not a valid type`); + } + + for (const notifyCondition of updateData.notifyOnNew) { + if (!isValidFileNotifyCondition(notifyCondition)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${notifyCondition} is not a valid notify condition`); + } + } + + file.task_id = updateData.taskId.slice(0, 7); + file.boss_app_id = updateData.bossAppId; + file.supported_countries = updateData.supportedCountries; + file.supported_languages = updateData.supportedLanguages; + file.attributes = updateData.attributes ? updateData.attributes : file.attributes; + file.name = updateData.name; + file.type = updateData.type; + file.notify_on_new = updateData.notifyOnNew; + file.notify_led = updateData.notifyLed; + file.condition_played = updateData.conditionPlayed; + file.auto_delete = updateData.autoDelete; + file.updated = BigInt(Date.now()); + + await file.save(); + + return {}; +} diff --git a/src/services/grpc/boss/v2/update-task.ts b/src/services/grpc/boss/v2/update-task.ts new file mode 100644 index 0000000..e883678 --- /dev/null +++ b/src/services/grpc/boss/v2/update-task.ts @@ -0,0 +1,55 @@ +import { Status, ServerError } from 'nice-grpc'; +import { getTask } from '@/database'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { UpdateTaskRequest } from '@pretendonetwork/grpc/boss/v2/update_task'; +import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty'; + +export async function updateTask(request: UpdateTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'updateBossTasks')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to update tasks'); + } + + const taskID = request.id.trim(); + const bossAppID = request.bossAppId.trim(); + const updateData = request.updateData; + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + if (!updateData) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task update data'); + } + + const task = await getTask(bossAppID, taskID); + + if (!task) { + throw new ServerError(Status.INVALID_ARGUMENT, `Task ${taskID} not found for BOSS app ${bossAppID}`); + } + + if (updateData.status !== 'open' && updateData.status !== 'close') { + throw new ServerError(Status.INVALID_ARGUMENT, `Status ${updateData.status} is invalid`); + } + + if (updateData.id) { + task.id = updateData.id.slice(0, 7); + task.in_game_id = updateData.id; + } + + task.boss_app_id = updateData.bossAppId ? updateData.bossAppId : task.boss_app_id; + task.title_id = updateData.titleId ? updateData.titleId.toString(16).toLowerCase().padStart(16, '0') : task.title_id; + task.status = updateData.status ? updateData.status : task.status; + task.interval = updateData.interval ? updateData.interval : task.interval; + task.description = updateData.description ? updateData.description : task.description; + task.updated = BigInt(Date.now()); + + await task.save(); + + return {}; +} diff --git a/src/services/grpc/boss/v2/upload-file-ctr.ts b/src/services/grpc/boss/v2/upload-file-ctr.ts new file mode 100644 index 0000000..5f5be21 --- /dev/null +++ b/src/services/grpc/boss/v2/upload-file-ctr.ts @@ -0,0 +1,176 @@ +import { Status, ServerError } from 'nice-grpc'; +import { CTR_BOSS_FLAGS, encrypt3DS } from '@pretendonetwork/boss-crypto'; +import { isValidCountryCode, isValidLanguage, md5 } from '@/util'; +import { connection as databaseConnection, getTask, getCTRTaskFile } from '@/database'; +import { FileCTR } from '@/models/file-ctr'; +import { config } from '@/config-manager'; +import { uploadCDNFile } from '@/cdn'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { UploadFileCTRRequest, UploadFileCTRResponse } from '@pretendonetwork/grpc/boss/v2/upload_file_ctr'; +import type { HydratedFileCTRDocument } from '@/types/mongoose/file-ctr'; + +const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/; + +export async function uploadFileCTR(request: UploadFileCTRRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'uploadBossFiles')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to upload new files'); + } + + const taskID = request.taskId.trim(); + const bossAppID = request.bossAppId.trim(); + const supportedCountries = request.supportedCountries; + const supportedLanguages = request.supportedLanguages; + const name = request.name.trim(); + const payloads = request.payloadContents; + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + if (bossAppID.length !== 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must be 16 characters'); + } + + if (!BOSS_APP_ID_FILTER_REGEX.test(bossAppID)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must only contain letters and numbers'); + } + + if (!(await getTask(bossAppID, taskID))) { + throw new ServerError(Status.NOT_FOUND, `Task ${taskID} does not exist for BOSS app ${bossAppID}`); + } + + for (const country of supportedCountries) { + if (!isValidCountryCode(country)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${country} is not a valid country`); + } + } + + for (const language of supportedLanguages) { + if (!isValidLanguage(language)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${language} is not a valid language`); + } + } + + if (!request.attributes) { + request.attributes = { + attribute1: '', + attribute2: '', + attribute3: '', + description: '' + }; + } + + const session = await databaseConnection().startSession(); + await session.startTransaction(); + + let file: HydratedFileCTRDocument | null; + + try { + // * Create the FileCTR first since encrypt3DS relies on the serial number + file = await getCTRTaskFile(bossAppID, taskID, name); + + if (file) { + file.deleted = true; + file.updated = BigInt(Date.now()); + + await file.save({ session }); + } + + [file] = await FileCTR.create([{ + creator_pid: context.user?.pid, + // * hash: String, + // * file_key: String, + // * size: BigInt, + task_id: taskID, + boss_app_id: bossAppID, + supported_countries: supportedCountries, + supported_languages: supportedLanguages, + attributes: request.attributes, + name: name, + payload_contents: payloads.map(payload => ({ + title_id: payload.titleId, + content_datatype: payload.contentDatatype, + ns_data_id: payload.nsDataId, + version: payload.version, + size: payload.content.length + })), + flags: { + mark_arrived_privileged: request.flags?.markArrivedPrivileged || false + }, + created: Date.now(), + updated: Date.now() + }], { session }); + + const cryptoOptions = payloads.map(payload => ({ + program_id: payload.titleId, + content_datatype: payload.contentDatatype, + ns_data_id: payload.nsDataId, + version: payload.version, + content: payload.content + })); + + let flags = 0n; + if (request.flags?.markArrivedPrivileged) { + flags |= CTR_BOSS_FLAGS.MARK_ARRIVED_PRIVILEGED; + } + + // TODO - Somehow support pre-encrypted content? + const encryptedData = encrypt3DS(config.crypto.ctr.aes_key, file.serial_number, cryptoOptions, flags); + const contentHash = md5(encryptedData); + const key = `${bossAppID}/${taskID}/${contentHash}`; + + await uploadCDNFile('taskFile', key, encryptedData); + + file.hash = contentHash; + file.file_key = key; + file.size = BigInt(encryptedData.length); + + await file.save({ session }); + await session.commitTransaction(); + } catch (error: unknown) { + let message = 'Unknown file upload error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.ABORTED, message); + } finally { + await session.endSession(); + } + + return { + file: { + deleted: file.deleted, + dataId: file.serial_number, + taskId: file.task_id, + bossAppId: file.boss_app_id, + supportedCountries: file.supported_countries, + supportedLanguages: file.supported_languages, + attributes: file.attributes, + creatorPid: file.creator_pid, + name: file.name, + hash: file.hash, + serialNumber: file.serial_number, + payloadContents: file.payload_contents.map(payloadContentInfo => ({ + titleId: payloadContentInfo.title_id, + contentDatatype: payloadContentInfo.content_datatype, + nsDataId: payloadContentInfo.ns_data_id, + version: payloadContentInfo.version, + size: payloadContentInfo.size + })), + size: file.size, + createdTimestamp: file.created, + updatedTimestamp: file.updated, + flags: { + markArrivedPrivileged: file.flags.mark_arrived_privileged + } + } + }; +} diff --git a/src/services/grpc/boss/v2/upload-file-wup.ts b/src/services/grpc/boss/v2/upload-file-wup.ts new file mode 100644 index 0000000..6eaf577 --- /dev/null +++ b/src/services/grpc/boss/v2/upload-file-wup.ts @@ -0,0 +1,182 @@ +import { Status, ServerError } from 'nice-grpc'; +import { encryptWiiU } from '@pretendonetwork/boss-crypto'; +import { isValidCountryCode, isValidFileNotifyCondition, isValidFileType, isValidLanguage, md5 } from '@/util'; +import { getTask, getWUPTaskFile } from '@/database'; +import { FileWUP } from '@/models/file-wup'; +import { config } from '@/config-manager'; +import { uploadCDNFile } from '@/cdn'; +import { hasPermission } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { AuthenticationCallContextExt } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import type { CallContext } from 'nice-grpc'; +import type { UploadFileWUPRequest, UploadFileWUPResponse } from '@pretendonetwork/grpc/boss/v2/upload_file_wup'; + +const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/; + +export async function uploadFileWUP(request: UploadFileWUPRequest, context: CallContext & AuthenticationCallContextExt): Promise { + if (!hasPermission(context, 'uploadBossFiles')) { + throw new ServerError(Status.PERMISSION_DENIED, 'PNID not authorized to upload new files'); + } + + const taskID = request.taskId.trim(); + const bossAppID = request.bossAppId.trim(); + const supportedCountries = request.supportedCountries; + const supportedLanguages = request.supportedLanguages; + const name = request.name.trim(); + const type = request.type.trim(); + const notifyOnNew = [...new Set(request.notifyOnNew)]; + const notifyLed = request.notifyLed; + const data = request.data; + const nameEqualsDataID = request.nameEqualsDataId; + + if (!taskID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing task ID'); + } + + if (!bossAppID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Missing BOSS app ID'); + } + + if (bossAppID.length !== 16) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must be 16 characters'); + } + + if (!BOSS_APP_ID_FILTER_REGEX.test(bossAppID)) { + throw new ServerError(Status.INVALID_ARGUMENT, 'BOSS app ID must only contain letters and numbers'); + } + + if (!(await getTask(bossAppID, taskID))) { + throw new ServerError(Status.NOT_FOUND, `Task ${taskID} does not exist for BOSS app ${bossAppID}`); + } + + for (const country of supportedCountries) { + if (!isValidCountryCode(country)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${country} is not a valid country`); + } + } + + for (const language of supportedLanguages) { + if (!isValidLanguage(language)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${language} is not a valid language`); + } + } + + if (!name && !nameEqualsDataID) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Must provide a file name is enable nameEqualsDataId'); + } + + if (!isValidFileType(type)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${type} is not a valid type`); + } + + for (const notifyCondition of notifyOnNew) { + if (!isValidFileNotifyCondition(notifyCondition)) { + throw new ServerError(Status.INVALID_ARGUMENT, `${notifyCondition} is not a valid notify condition`); + } + } + + if (data.length === 0) { + throw new ServerError(Status.INVALID_ARGUMENT, 'Cannot upload empty file'); + } + + if (!request.attributes) { + request.attributes = { + attribute1: '', + attribute2: '', + attribute3: '', + description: '' + }; + } + + let encryptedData: Buffer; + + try { + // TODO - Check the first few bytes of the uploaded content to see if it's encrypted already, to support pre-encrypted content? + encryptedData = encryptWiiU(data, config.crypto.wup.aes_key, config.crypto.wup.hmac_key); + } catch (error: unknown) { + let message = 'Unknown file encryption error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.ABORTED, message); + } + + const contentHash = md5(encryptedData); + + // * Upload file first to prevent ghost DB entries on upload failures + const key = `${bossAppID}/${taskID}/${contentHash}`; + try { + // * Some tasks have file names which are dynamic. + // * They change depending on the files data ID. + // * Because of this, using the file name in the + // * upload key is not viable, as it is not always + // * known during upload + await uploadCDNFile('taskFile', key, encryptedData); + } catch (error: unknown) { + let message = 'Unknown file upload error'; + + if (error instanceof Error) { + message = error.message; + } + + throw new ServerError(Status.ABORTED, message); + } + + let file = await getWUPTaskFile(bossAppID, taskID, name); + + if (file) { + file.deleted = true; + file.updated = BigInt(Date.now()); + + await file.save(); + } + + file = await FileWUP.create({ + task_id: taskID.slice(0, 7), + boss_app_id: bossAppID, + file_key: key, + supported_countries: supportedCountries, + supported_languages: supportedLanguages, + attributes: request.attributes, + creator_pid: context.user?.pid, + name: name, + type: type, + hash: contentHash, + size: BigInt(encryptedData.length), + notify_on_new: notifyOnNew, + notify_led: notifyLed, + condition_played: request.conditionPlayed, + auto_delete: request.autoDelete, + created: Date.now(), + updated: Date.now() + }); + + if (nameEqualsDataID) { + file.name = file.data_id.toString(16).padStart(8, '0'); + await file.save(); + } + + return { + file: { + deleted: file.deleted, + dataId: file.data_id, + taskId: file.task_id, + bossAppId: file.boss_app_id, + supportedCountries: file.supported_countries, + supportedLanguages: file.supported_languages, + attributes: file.attributes, + creatorPid: file.creator_pid, + name: file.name, + type: file.type, + hash: file.hash, + size: file.size, + notifyOnNew: file.notify_on_new, + notifyLed: file.notify_led, + conditionPlayed: request.conditionPlayed, + autoDelete: request.autoDelete, + createdTimestamp: file.created, + updatedTimestamp: file.updated + } + }; +} diff --git a/src/services/grpc/server.ts b/src/services/grpc/server.ts index f156275..0ef5475 100644 --- a/src/services/grpc/server.ts +++ b/src/services/grpc/server.ts @@ -1,15 +1,23 @@ import { createServer } from 'nice-grpc'; -import { BOSSDefinition } from '@pretendonetwork/grpc/boss/boss_service'; -import { apiKeyMiddleware } from '@/services/grpc/boss/middleware/api-key-middleware'; -import { authenticationMiddleware } from '@/services/grpc/boss/middleware/authentication-middleware'; -import { implementation } from '@/services/grpc/boss/implementation'; +import { BOSSDefinition as BossServiceDefinitionV1 } from '@pretendonetwork/grpc/boss/boss_service'; +import { BossServiceDefinition as BossServiceDefinitionV2 } from '@pretendonetwork/grpc/boss/v2/boss_service'; +import { apiKeyMiddleware as apiKeyMiddlewareV1 } from '@/services/grpc/boss/v1/middleware/api-key-middleware'; +import { authenticationMiddleware as authenticationMiddlewareV1 } from '@/services/grpc/boss/v1/middleware/authentication-middleware'; +import { bossServiceImplementationV1 } from '@/services/grpc/boss/v1/implementation'; +import { apiKeyMiddleware as apiKeyMiddlewareV2 } from '@/services/grpc/boss/v2/middleware/api-key-middleware'; +import { authenticationMiddleware as authenticationMiddlewareV2 } from '@/services/grpc/boss/v2/middleware/authentication-middleware'; +import { bossServiceImplementationV2 } from '@/services/grpc/boss/v2/implementation'; import { config } from '@/config-manager'; import type { Server } from 'nice-grpc'; export async function startGRPCServer(): Promise { - const server: Server = createServer(); + const server: Server = createServer({ + 'grpc.max_receive_message_length': config.grpc.max_receive_message_length * 1024 * 1024, + 'grpc.max_send_message_length': config.grpc.max_send_message_length * 1024 * 1024 + }); - server.with(apiKeyMiddleware).with(authenticationMiddleware).add(BOSSDefinition, implementation); + server.with(apiKeyMiddlewareV1).with(authenticationMiddlewareV1).add(BossServiceDefinitionV1, bossServiceImplementationV1); + server.with(apiKeyMiddlewareV2).with(authenticationMiddlewareV2).add(BossServiceDefinitionV2, bossServiceImplementationV2); await server.listen(`${config.grpc.boss.address}:${config.grpc.boss.port}`); } diff --git a/src/services/npdi.ts b/src/services/npdi.ts index 5af9c89..df3a9b2 100644 --- a/src/services/npdi.ts +++ b/src/services/npdi.ts @@ -2,7 +2,7 @@ import express from 'express'; import { restrictHostnames } from '@/middleware/host-limit'; import { config } from '@/config-manager'; import { getCDNFileAsStream, streamFileToResponse } from '@/cdn'; -import { getTaskFileByDataID } from '@/database'; +import { getWUPTaskFileByDataID } from '@/database'; import { handleEtag, sendEtagCacheResponse } from '@/util'; const npdi = express.Router(); @@ -10,7 +10,7 @@ const npdi = express.Router(); npdi.get('/p01/data/1/:bossAppId/:dataId/:fileHash', async (request, response) => { const { dataId, fileHash, bossAppId } = request.params; - const file = await getTaskFileByDataID(BigInt(dataId)); + const file = await getWUPTaskFileByDataID(BigInt(dataId)); if (!file) { return response.sendStatus(404); } diff --git a/src/services/npdl.ts b/src/services/npdl.ts index d41a998..7585196 100644 --- a/src/services/npdl.ts +++ b/src/services/npdl.ts @@ -1,9 +1,9 @@ import express from 'express'; -import { getTaskFile } from '@/database'; +import { getCTRTaskFile } from '@/database'; import { config } from '@/config-manager'; import { restrictHostnames } from '@/middleware/host-limit'; import { getCDNFileAsStream, streamFileToResponse } from '@/cdn'; -import { handleEtag, sendEtagCacheResponse } from '@/util'; +import { handleEtag, isValidCountryCode, sendEtagCacheResponse } from '@/util'; const npdl = express.Router(); @@ -23,7 +23,26 @@ npdl.get([ }>, response) => { const { appID, taskID, countryCode, languageCode, fileName } = request.params; - const file = await getTaskFile(appID, taskID, fileName, countryCode, languageCode); + // * There are some special cases that we need to account for some specific 3DS task files: + // * + // * 1. The country and language being represented in a single parameter with an underscore :languageCode_:countryCode + // * 2. Only the country parameter being set instead of the language + // * + // * This isn't the standard behavior as it doesn't work for all tasks, only some of them do need it + // * (this is so unstandard that you can't officially find tasks which use an underscore with the file list endpoint). + // * I'm sure whoever designed this behavior must be the most evil person I've ever met + let country: string | undefined; + let language: string | undefined; + if (countryCode == undefined && languageCode !== undefined && languageCode.includes('_')) { + [language, country] = languageCode.split('_'); + } else if (countryCode == undefined && languageCode !== undefined && isValidCountryCode(languageCode)) { + country = languageCode; + } else { + country = countryCode; + language = languageCode; + } + + const file = await getCTRTaskFile(appID, taskID, fileName, country, language); if (!file) { response.sendStatus(404); diff --git a/src/services/npfl.ts b/src/services/npfl.ts index c4329b1..327bf3f 100644 --- a/src/services/npfl.ts +++ b/src/services/npfl.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; import express from 'express'; -import { getTaskFilesWithAttributes } from '@/database'; +import { getCTRTaskFilesWithAttributes } from '@/database'; import { restrictHostnames } from '@/middleware/host-limit'; import { config } from '@/config-manager'; @@ -40,7 +40,7 @@ npfl.get('/p01/filelist/:appID/:taskID', async (request: express.Request<{ const attribute2 = request.query.a2; const attribute3 = request.query.a3; - const files = await getTaskFilesWithAttributes(false, appID, taskID, country, language, attribute1, attribute2, attribute3); + const files = await getCTRTaskFilesWithAttributes(false, appID, taskID, country, language, attribute1, attribute2, attribute3); // * https://gist.github.com/DaniElectra/ada7ecc930a84432f2045f6fcabac84f#nintendo-boss-file-list-server-npfl // * @@ -55,11 +55,11 @@ npfl.get('/p01/filelist/:appID/:taskID', async (request: express.Request<{ // * File lines have the following fields: // * // * - File name - // * - Unknown (password?) + // * - Description // * - Attribute 1 // * - Attribute 2 // * - Attribute 3 - // * - File size (0 is allowed) + // * - Content size (size of the first payload content) // * - Updated time (seconds) // * // * All fields of a file line are separated by a tab (\t) and are present even if no value. @@ -83,11 +83,11 @@ npfl.get('/p01/filelist/:appID/:taskID', async (request: express.Request<{ for (const file of files) { const params = [ file.name, - file.password, // * Unsure if this is really what this is for. Team Kirby Clash Deluxe uses this for passwords though - file.attribute1, - file.attribute2, - file.attribute3, - file.size, + file.attributes.description, + file.attributes.attribute1, + file.attributes.attribute2, + file.attributes.attribute3, + file.payload_contents[0]?.size ?? 0, file.updated / 1000n // * Expects time as seconds, not milliseconds ]; const line = `${params.join('\t')}\r\n`; diff --git a/src/services/npts.ts b/src/services/npts.ts index 93c3753..dc44593 100644 --- a/src/services/npts.ts +++ b/src/services/npts.ts @@ -2,37 +2,62 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; import { config } from '@/config-manager'; import { restrictHostnames } from '@/middleware/host-limit'; -import { getTask, getTaskFile, getTaskFiles } from '@/database'; -import type { HydratedFileDocument } from '@/types/mongoose/file'; +import { getTask, getWUPTaskFile, getWUPTaskFiles } from '@/database'; +import type { HydratedFileWUPDocument } from '@/types/mongoose/file-wup'; import type { HydratedTaskDocument } from '@/types/mongoose/task'; const npts = express.Router(); const xmlHeadSettings = { encoding: 'UTF-8', version: '1.0' }; -function buildFile(task: HydratedTaskDocument, file: HydratedFileDocument): any { - return { - Filename: file.name, - DataId: file.data_id, - Type: file.type, - Url: `https://${config.domains.npdi}/p01/data/1/${task.boss_app_id}/${file.data_id}/${file.hash}`, - Size: file.size, - Notify: { - New: file.notify_on_new.join(','), - LED: file.notify_led - } - }; +function buildFile(task: HydratedTaskDocument, file: HydratedFileWUPDocument, attributesMode: boolean): any { + if (attributesMode) { + return { + Filename: file.name, + Type: file.type, + Size: file.size, + Attributes: { + Attribute1: file.attributes.attribute1, + Attribute2: file.attributes.attribute2, + Attribute3: file.attributes.attribute3, + Description: file.attributes.description + } + }; + } else { + return { + Filename: file.name, + DataId: file.data_id, + Type: file.type, + Url: `https://${config.domains.npdi}/p01/data/1/${task.boss_app_id}/${file.data_id}/${file.hash}`, + Size: file.size, + Notify: { + New: file.notify_on_new.join(','), + LED: file.notify_led + } + }; + } } -npts.get('/p01/tasksheet/:id/:bossAppId/:taskId', async (request, response) => { +npts.get('/p01/tasksheet/:id/:bossAppId/:taskId', async (request: express.Request<{ + id: string; + bossAppId: string; + taskId: string; +}, any, any, { + c?: string; + l?: string; + mode?: string; +}>, response) => { const { bossAppId, taskId } = request.params; + const country = request.query.c; + const language = request.query.l; + const mode = request.query.mode; const task = await getTask(bossAppId, taskId); if (!task) { return response.sendStatus(404); } - const files = await getTaskFiles(false, bossAppId, taskId); + const files = await getWUPTaskFiles(false, bossAppId, taskId, country, language); const xmlContent = { TaskSheet: { @@ -40,7 +65,7 @@ npts.get('/p01/tasksheet/:id/:bossAppId/:taskId', async (request, response) => { TaskId: task.id, ServiceStatus: task.status, Files: { - File: files.map(f => buildFile(task, f)) + File: files.map(f => buildFile(task, f, mode === 'attr')) } } }; @@ -49,15 +74,27 @@ npts.get('/p01/tasksheet/:id/:bossAppId/:taskId', async (request, response) => { response.send(xmlbuilder.create(xmlContent, xmlHeadSettings).end({ pretty: true })); }); -npts.get('/p01/tasksheet/:id/:bossAppId/:taskId/:fileName', async (request, response) => { +npts.get('/p01/tasksheet/:id/:bossAppId/:taskId/:fileName', async (request: express.Request<{ + id: string; + bossAppId: string; + taskId: string; + fileName: string; +}, any, any, { + c?: string; + l?: string; + mode?: string; +}>, response) => { const { bossAppId, taskId, fileName } = request.params; + const country = request.query.c; + const language = request.query.l; + const mode = request.query.mode; const task = await getTask(bossAppId, taskId); if (!task) { return response.sendStatus(404); } - const file = await getTaskFile(bossAppId, taskId, fileName); + const file = await getWUPTaskFile(bossAppId, taskId, fileName, country, language); if (!file) { return response.sendStatus(404); } @@ -68,7 +105,7 @@ npts.get('/p01/tasksheet/:id/:bossAppId/:taskId/:fileName', async (request, resp TaskId: task.id, ServiceStatus: task.status, Files: { - File: buildFile(task, file) + File: buildFile(task, file, mode === 'attr') } } }; diff --git a/src/types/mongoose/file-ctr.ts b/src/types/mongoose/file-ctr.ts new file mode 100644 index 0000000..60d38b2 --- /dev/null +++ b/src/types/mongoose/file-ctr.ts @@ -0,0 +1,41 @@ +import type { Model, HydratedDocument } from 'mongoose'; + +export interface IFileCTR { + deleted: boolean; + creator_pid: number; + hash: string; + file_key: string; + size: bigint; + task_id: string; + boss_app_id: string; + supported_countries: string[]; + supported_languages: string[]; + attributes: { + attribute1: string; + attribute2: string; + attribute3: string; + description: string; + }; + name: string; + serial_number: bigint; // * This is effectively the predecessor of the Wii U DataID + payload_contents: { + title_id: bigint; + content_datatype: number; + ns_data_id: number; + version: number; + size: number; + }[]; + flags: { + mark_arrived_privileged: boolean; + }; + created: bigint; + updated: bigint; +} + +export interface IFileCTRMethods {} + +interface IFileCTRQueryHelpers {} + +export type FileCTRModel = Model; + +export type HydratedFileCTRDocument = HydratedDocument; diff --git a/src/types/mongoose/file-wup.ts b/src/types/mongoose/file-wup.ts new file mode 100644 index 0000000..b3e6078 --- /dev/null +++ b/src/types/mongoose/file-wup.ts @@ -0,0 +1,36 @@ +import type { Model, HydratedDocument } from 'mongoose'; + +export interface IFileWUP { + deleted: boolean; + file_key: string; + data_id: bigint; + task_id: string; + boss_app_id: string; + supported_countries: string[]; + supported_languages: string[]; + attributes: { + attribute1: string; + attribute2: string; + attribute3: string; + description: string; + }; + creator_pid: number; + name: string; + type: string; + hash: string; + size: bigint; + notify_on_new: string[]; + notify_led: boolean; + condition_played: bigint; + auto_delete: boolean; + created: bigint; + updated: bigint; +} + +export interface IFileWUPMethods {} + +interface IFileWUPQueryHelpers {} + +export type FileWUPModel = Model; + +export type HydratedFileWUPDocument = HydratedDocument; diff --git a/src/types/mongoose/file.ts b/src/types/mongoose/file.ts deleted file mode 100644 index 845f3c8..0000000 --- a/src/types/mongoose/file.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Model, HydratedDocument } from 'mongoose'; - -export interface IFile { - deleted: boolean; - file_key: string; - data_id: bigint; - task_id: string; - boss_app_id: string; - supported_countries: string[]; - supported_languages: string[]; - password: string; - attribute1: string; - attribute2: string; - attribute3: string; - creator_pid: number; - name: string; - type: string; - hash: string; - size: bigint; - notify_on_new: string[]; - notify_led: boolean; - created: bigint; - updated: bigint; -} - -export interface IFileMethods {} - -interface IFileQueryHelpers {} - -export type FileModel = Model; - -export type HydratedFileDocument = HydratedDocument; diff --git a/src/types/mongoose/task.ts b/src/types/mongoose/task.ts index 9204472..6e92fdb 100644 --- a/src/types/mongoose/task.ts +++ b/src/types/mongoose/task.ts @@ -6,7 +6,8 @@ export interface ITask { in_game_id: string; boss_app_id: string; creator_pid: number; - status: 'open'; // TODO - Make this a union. What else is there? + status: 'open' | 'close'; + interval: number; title_id: string; description: string; created: bigint;