mirror of
https://github.com/PretendoNetwork/BOSS.git
synced 2026-03-21 17:34:19 -05:00
Merge branch 'master' of github.com:PretendoNetwork/BOSS
This commit is contained in:
commit
2ebe0f2054
70
README.md
70
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 |
|
||||
|
|
|
|||
110
package-lock.json
generated
110
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
@ -917,6 +917,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 +2028,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 +2045,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 +2070,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 +3161,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",
|
||||
|
|
@ -5807,15 +5822,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",
|
||||
|
|
@ -7818,13 +7824,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",
|
||||
|
|
@ -9467,7 +9473,6 @@
|
|||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -10622,6 +10627,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 +11273,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 +11286,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 +11307,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 +12079,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"
|
||||
|
|
@ -13877,11 +13893,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",
|
||||
|
|
@ -15191,12 +15202,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",
|
||||
|
|
@ -16295,8 +16306,7 @@
|
|||
"typescript": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="
|
||||
},
|
||||
"typescript-eslint": {
|
||||
"version": "8.39.1",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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((err) => {
|
||||
|
|
|
|||
205
src/cli/files-3ds.cmd.ts
Normal file
205
src/cli/files-3ds.cmd.ts
Normal file
|
|
@ -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('<app_id>', 'BOSS app to search in')
|
||||
.argument('<task_id>', '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<void> => {
|
||||
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('<app_id>', 'BOSS app that contains the task')
|
||||
.argument('<task_id>', 'Task that contains the task file')
|
||||
.argument('<id>', 'Task file ID to lookup', BigInt)
|
||||
.action(commandHandler<[string, string, bigint]>(async (cmd): Promise<void> => {
|
||||
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('<app_id>', 'BOSS app that contains the task')
|
||||
.argument('<task_id>', 'Task that contains the task file')
|
||||
.argument('<id>', 'Task file ID to lookup', BigInt)
|
||||
.option('-d, --decrypt', 'Decrypt the file before return')
|
||||
.action(commandHandler<[string, string, bigint]>(async (cmd): Promise<void> => {
|
||||
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('<app_id>', 'BOSS app to store the task file in')
|
||||
.argument('<task_id>', 'Task to store the task file in')
|
||||
.requiredOption('--name <name>', 'Name of the task file')
|
||||
.requiredOption('--title-id <titleId>', 'Target title ID of the payload')
|
||||
.requiredOption('--content-datatype <contentDatatype>', 'Content datatype of the payload')
|
||||
.requiredOption('--ns-data-id <nsDataId>', 'NS Data ID of the payload')
|
||||
.requiredOption('--version <version>', 'Version of the payload')
|
||||
.requiredOption('--file <file>', 'Path of the file to upload')
|
||||
.option('--country <country...>', 'Countries for this task file')
|
||||
.option('--lang <language...>', '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<void> => {
|
||||
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('<app_id>', 'BOSS app that contains the task')
|
||||
.argument('<task_id>', 'Task that contains the task file')
|
||||
.argument('<id>', 'Task file ID to delete', BigInt)
|
||||
.action(commandHandler<[string, string, bigint]>(async (cmd): Promise<void> => {
|
||||
// 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);
|
||||
|
|
@ -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('<app_id>', 'BOSS app to search in')
|
||||
.argument('<task_id>', '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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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 <file>', 'Path of the file to upload')
|
||||
.option('--country <country...>', 'Countries for this task file')
|
||||
.option('--lang <language...>', '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 <type...>', 'Add entry to NotifyNew')
|
||||
.option('--notify-led', 'Enable NotifyLED')
|
||||
.action(commandHandler<[string, string]>(async (cmd): Promise<void> => {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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<void>
|
|||
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<void>
|
|||
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)) {
|
||||
|
|
|
|||
|
|
@ -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('<app_id>', 'BOSS app to store the task in')
|
||||
.requiredOption('--id <id>', 'Id of the task')
|
||||
.requiredOption('--title-id <titleId>', '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<void> => {
|
||||
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!`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() || ''),
|
||||
|
|
|
|||
157
src/database.ts
157
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<HydratedTask
|
|||
});
|
||||
}
|
||||
|
||||
export function getTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string): Promise<HydratedFileDocument[]> {
|
||||
export function getCTRTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string, any: boolean = false): Promise<HydratedFileCTRDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
const filter: mongoose.FilterQuery<IFile> = {
|
||||
const filter: mongoose.FilterQuery<IFileCTR> = {
|
||||
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<HydratedFileDocument[]> {
|
||||
export function getWUPTaskFiles(allowDeleted: boolean, bossAppID: string, taskID: string, country?: string, language?: string, any: boolean = false): Promise<HydratedFileWUPDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
const filter: mongoose.FilterQuery<IFile> = {
|
||||
const filter: mongoose.FilterQuery<IFileWUP> = {
|
||||
task_id: taskID.slice(0, 7),
|
||||
boss_app_id: bossAppID,
|
||||
$and: []
|
||||
|
|
@ -110,6 +120,10 @@ export function getTaskFilesWithAttributes(allowDeleted: boolean, bossAppID: str
|
|||
{ supported_countries: country }
|
||||
]
|
||||
});
|
||||
} else if (!any) {
|
||||
filter.$and?.push({
|
||||
supported_countries: { $eq: [] }
|
||||
});
|
||||
}
|
||||
|
||||
if (language) {
|
||||
|
|
@ -119,31 +133,81 @@ export function getTaskFilesWithAttributes(allowDeleted: boolean, bossAppID: str
|
|||
{ 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<HydratedFileCTRDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
const filter: mongoose.FilterQuery<IFileCTR> = {
|
||||
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) {
|
||||
filter.$and?.push({
|
||||
$or: [
|
||||
{ supported_languages: { $eq: [] } },
|
||||
{ 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<HydratedFileDocument | null> {
|
||||
export function getCTRTaskFile(bossAppID: string, taskID: string, name: string, country?: string, language?: string): Promise<HydratedFileCTRDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
const filter: mongoose.FilterQuery<IFile> = {
|
||||
const filter: mongoose.FilterQuery<IFileCTR> = {
|
||||
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 File.findOne<HydratedFileDocument>(filter);
|
||||
return FileCTR.findOne<HydratedFileCTRDocument>(filter);
|
||||
}
|
||||
|
||||
export function getTaskFileByDataID(dataID: bigint): Promise<HydratedFileDocument | null> {
|
||||
export function getWUPTaskFile(bossAppID: string, taskID: string, name: string, country?: string, language?: string): Promise<HydratedFileWUPDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return File.findOne<HydratedFileDocument>({
|
||||
const filter: mongoose.FilterQuery<IFileWUP> = {
|
||||
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 FileWUP.findOne<HydratedFileWUPDocument>(filter);
|
||||
}
|
||||
|
||||
export function getCTRTaskFileBySerialNumber(serialNumber: bigint): Promise<HydratedFileCTRDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return FileCTR.findOne<HydratedFileCTRDocument>({
|
||||
deleted: false,
|
||||
serial_number: serialNumber
|
||||
});
|
||||
}
|
||||
|
||||
export function getWUPTaskFileByDataID(dataID: bigint): Promise<HydratedFileWUPDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return FileWUP.findOne<HydratedFileWUPDocument>({
|
||||
deleted: false,
|
||||
data_id: Number(dataID)
|
||||
});
|
||||
|
|
|
|||
48
src/models/file-ctr.ts
Normal file
48
src/models/file-ctr.ts
Normal file
|
|
@ -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<IFileCTR, FileCTRModel, IFileCTRMethods>({
|
||||
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<IFileCTR, FileCTRModel>('FileCTR', FileCTRSchema, 'files-ctr');
|
||||
43
src/models/file-wup.ts
Normal file
43
src/models/file-wup.ts
Normal file
|
|
@ -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<IFileWUP, FileWUPModel, IFileWUPMethods>({
|
||||
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<IFileWUP, FileWUPModel>('FileWUP', FileWUPSchema, 'files-wup');
|
||||
|
|
@ -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<IFile, FileModel, IFileMethods>({
|
||||
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<IFile, FileModel>('File', FileSchema);
|
||||
|
|
@ -15,6 +15,7 @@ const TaskSchema = new mongoose.Schema<ITask, TaskModel, ITaskMethods>({
|
|||
required: true,
|
||||
enum: ['open', 'close']
|
||||
},
|
||||
interval: Number,
|
||||
title_id: String,
|
||||
description: String,
|
||||
created: BigInt,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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<Empty> {
|
||||
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}`);
|
||||
|
|
@ -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<Empty> {
|
||||
if (!hasPermission(context, 'deleteBossTasks')) {
|
||||
22
src/services/grpc/boss/v1/implementation.ts
Normal file
22
src/services/grpc/boss/v1/implementation.ts
Normal file
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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<ListFilesRes
|
|||
throw new ServerError(Status.INVALID_ARGUMENT, `${language} is not a valid language`);
|
||||
}
|
||||
|
||||
const files = await getTaskFiles(false, bossAppID, taskID, country, language);
|
||||
const files = await getWUPTaskFiles(false, bossAppID, taskID, country, language);
|
||||
|
||||
return {
|
||||
files: files.map(file => ({
|
||||
|
|
@ -45,10 +45,10 @@ export async function listFiles(request: ListFilesRequest): Promise<ListFilesRes
|
|||
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,
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { ServerError, Status } from 'nice-grpc';
|
||||
import { getTask } from '@/database';
|
||||
import { Task } from '@/models/task';
|
||||
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 { RegisterTaskRequest, RegisterTaskResponse } from '@pretendonetwork/grpc/boss/register_task';
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ export async function registerTask(request: RegisterTaskRequest, context: CallCo
|
|||
in_game_id: taskID,
|
||||
boss_app_id: bossAppID,
|
||||
creator_pid: context.user?.pid,
|
||||
status: 'open', // TODO - Make this configurable
|
||||
status: 'open',
|
||||
title_id: titleID,
|
||||
description: description,
|
||||
created: Date.now(),
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { getTaskFileByDataID } from '@/database';
|
||||
import { getWUPTaskFileByDataID } from '@/database';
|
||||
import { isValidFileNotifyCondition, isValidFileType } from '@/util';
|
||||
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 { UpdateFileMetadataRequest } from '@pretendonetwork/grpc/boss/update_file_metadata';
|
||||
import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
|
||||
import type { Empty } from '@pretendonetwork/grpc/google/protobuf/empty';
|
||||
|
||||
export async function updateFileMetadata(request: UpdateFileMetadataRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
|
||||
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;
|
||||
|
|
@ -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<Empty> {
|
||||
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;
|
||||
|
|
@ -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,
|
||||
44
src/services/grpc/boss/v2/delete-file.ts
Normal file
44
src/services/grpc/boss/v2/delete-file.ts
Normal file
|
|
@ -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<Empty> {
|
||||
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 {};
|
||||
}
|
||||
37
src/services/grpc/boss/v2/delete-task.ts
Normal file
37
src/services/grpc/boss/v2/delete-task.ts
Normal file
|
|
@ -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<Empty> {
|
||||
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 {};
|
||||
}
|
||||
28
src/services/grpc/boss/v2/implementation.ts
Normal file
28
src/services/grpc/boss/v2/implementation.ts
Normal file
|
|
@ -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
|
||||
};
|
||||
69
src/services/grpc/boss/v2/list-files-ctr.ts
Normal file
69
src/services/grpc/boss/v2/list-files-ctr.ts
Normal file
|
|
@ -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<ListFilesCTRResponse> {
|
||||
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
|
||||
}
|
||||
}))
|
||||
};
|
||||
}
|
||||
63
src/services/grpc/boss/v2/list-files-wup.ts
Normal file
63
src/services/grpc/boss/v2/list-files-wup.ts
Normal file
|
|
@ -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<ListFilesWUPResponse> {
|
||||
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
|
||||
}))
|
||||
};
|
||||
}
|
||||
4586
src/services/grpc/boss/v2/list-known-boss-apps.ts
Normal file
4586
src/services/grpc/boss/v2/list-known-boss-apps.ts
Normal file
File diff suppressed because it is too large
Load Diff
22
src/services/grpc/boss/v2/list-tasks.ts
Normal file
22
src/services/grpc/boss/v2/list-tasks.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { getAllTasks } from '@/database';
|
||||
import type { ListTasksResponse } from '@pretendonetwork/grpc/boss/v2/list_tasks';
|
||||
|
||||
export async function listTasks(): Promise<ListTasksResponse> {
|
||||
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
|
||||
}))
|
||||
};
|
||||
}
|
||||
16
src/services/grpc/boss/v2/middleware/api-key-middleware.ts
Normal file
16
src/services/grpc/boss/v2/middleware/api-key-middleware.ts
Normal file
|
|
@ -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<Request, Response>(
|
||||
call: ServerMiddlewareCall<Request, Response>,
|
||||
context: CallContext
|
||||
): AsyncGenerator<Response, Response | void, undefined> {
|
||||
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);
|
||||
}
|
||||
|
|
@ -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<Request, Response>(
|
||||
call: ServerMiddlewareCall<Request, Response, AuthenticationCallContextExt>,
|
||||
context: CallContext
|
||||
): AsyncGenerator<Response, Response | void, undefined> {
|
||||
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];
|
||||
}
|
||||
85
src/services/grpc/boss/v2/register-task.ts
Normal file
85
src/services/grpc/boss/v2/register-task.ts
Normal file
|
|
@ -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<RegisterTaskResponse> {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
55
src/services/grpc/boss/v2/update-file-metadata-ctr.ts
Normal file
55
src/services/grpc/boss/v2/update-file-metadata-ctr.ts
Normal file
|
|
@ -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<Empty> {
|
||||
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 {};
|
||||
}
|
||||
58
src/services/grpc/boss/v2/update-file-metadata-wup.ts
Normal file
58
src/services/grpc/boss/v2/update-file-metadata-wup.ts
Normal file
|
|
@ -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<Empty> {
|
||||
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 {};
|
||||
}
|
||||
55
src/services/grpc/boss/v2/update-task.ts
Normal file
55
src/services/grpc/boss/v2/update-task.ts
Normal file
|
|
@ -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<Empty> {
|
||||
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 {};
|
||||
}
|
||||
176
src/services/grpc/boss/v2/upload-file-ctr.ts
Normal file
176
src/services/grpc/boss/v2/upload-file-ctr.ts
Normal file
|
|
@ -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<UploadFileCTRResponse> {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
182
src/services/grpc/boss/v2/upload-file-wup.ts
Normal file
182
src/services/grpc/boss/v2/upload-file-wup.ts
Normal file
|
|
@ -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<UploadFileWUPResponse> {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
41
src/types/mongoose/file-ctr.ts
Normal file
41
src/types/mongoose/file-ctr.ts
Normal file
|
|
@ -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<IFileCTR, IFileCTRQueryHelpers, IFileCTRMethods>;
|
||||
|
||||
export type HydratedFileCTRDocument = HydratedDocument<IFileCTR, IFileCTRMethods>;
|
||||
36
src/types/mongoose/file-wup.ts
Normal file
36
src/types/mongoose/file-wup.ts
Normal file
|
|
@ -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<IFileWUP, IFileWUPQueryHelpers, IFileWUPMethods>;
|
||||
|
||||
export type HydratedFileWUPDocument = HydratedDocument<IFileWUP, IFileWUPMethods>;
|
||||
|
|
@ -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<IFile, IFileQueryHelpers, IFileMethods>;
|
||||
|
||||
export type HydratedFileDocument = HydratedDocument<IFile, IFileMethods>;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user