Compare commits
90 Commits
main
...
splashcat-
| Author | SHA1 | Date |
|---|---|---|
|
|
91a0e0cc46 | |
|
|
c2fde4bdfd | |
|
|
6ca0bb06d4 | |
|
|
563cd3d92b | |
|
|
6a21775c90 | |
|
|
58dbafaa22 | |
|
|
2ee9321612 | |
|
|
9ad3fc3c50 | |
|
|
0574a12f96 | |
|
|
cd3088eb16 | |
|
|
96a2607da5 | |
|
|
7b18e23988 | |
|
|
cab8af8e31 | |
|
|
14405e996f | |
|
|
a75b200d7d | |
|
|
52f42bc42f | |
|
|
75ba29e5c7 | |
|
|
60562fedcd | |
|
|
610bcdf424 | |
|
|
6d5f4b66a5 | |
|
|
d7bd6309eb | |
|
|
c77519c229 | |
|
|
5bc3003f6d | |
|
|
109658680a | |
|
|
aa039c7be6 | |
|
|
9d222cbac2 | |
|
|
854c02f8cb | |
|
|
bd569c1c80 | |
|
|
32079a50e7 | |
|
|
f69aaef6d7 | |
|
|
a5fde51eeb | |
|
|
5f04eb68a2 | |
|
|
2c72bb7eaf | |
|
|
16c83c34e3 | |
|
|
55bdb2d284 | |
|
|
715a28f198 | |
|
|
02a01188c6 | |
|
|
54a7ff55fa | |
|
|
ed5d286ac7 | |
|
|
f33c03c691 | |
|
|
99eb9e71c8 | |
|
|
7d6b71d752 | |
|
|
94fb731fe0 | |
|
|
cb0df39102 | |
|
|
425fa1ef73 | |
|
|
055b1405df | |
|
|
b8e53fc719 | |
|
|
cbe7a5424a | |
|
|
21b02fb44d | |
|
|
cc348653d6 | |
|
|
4a0cda32ab | |
|
|
454f294045 | |
|
|
209e5e75ed | |
|
|
02c1c92191 | |
|
|
6a4fd3cceb | |
|
|
a7b0783f89 | |
|
|
bfb7d79609 | |
|
|
e4be0f2fe3 | |
|
|
af0ea16ecc | |
|
|
addb535d96 | |
|
|
94c33bae8f | |
|
|
f236a523f7 | |
|
|
b2555783bb | |
|
|
0cfe618f2f | |
|
|
5e36f6c33d | |
|
|
41d71073dc | |
|
|
5b7d320267 | |
|
|
a770901759 | |
|
|
7a2dedfbe5 | |
|
|
cad2edeaf5 | |
|
|
6582ab408b | |
|
|
348cf6045a | |
|
|
0baad9c04b | |
|
|
56c75385fa | |
|
|
417a52138d | |
|
|
5867740de3 | |
|
|
8707feac01 | |
|
|
740259e156 | |
|
|
2702e6cdf3 | |
|
|
1bc0d3eefc | |
|
|
a67bb4814d | |
|
|
6e5c2e05f3 | |
|
|
40cfd13e6c | |
|
|
6d044a15ae | |
|
|
63ea9347da | |
|
|
a5f35c78c9 | |
|
|
91f528a3be | |
|
|
8a96cb321c | |
|
|
0517bda98d | |
|
|
cabfa8f8c0 |
|
|
@ -7,7 +7,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
deno: [1.x, "1.31.x", canary]
|
deno: [1.x, "1.37.x", canary]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: denoland/setup-deno@v1
|
- uses: denoland/setup-deno@v1
|
||||||
|
|
@ -15,8 +15,10 @@ jobs:
|
||||||
deno-version: ${{ matrix.deno }}
|
deno-version: ${{ matrix.deno }}
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: deno fmt --check
|
run: deno fmt --check
|
||||||
|
if: ${{ matrix.deno != '1.31.x' }}
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: deno lint
|
run: deno lint
|
||||||
|
if: ${{ matrix.deno != '1.31.x' }}
|
||||||
- name: All entries
|
- name: All entries
|
||||||
uses: tj-actions/glob@v16
|
uses: tj-actions/glob@v16
|
||||||
id: entries
|
id: entries
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
name: Constant Check
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: denoland/setup-deno@v1
|
|
||||||
with:
|
|
||||||
deno-version: 1.x
|
|
||||||
- name: Check constant updates
|
|
||||||
run: deno run -A ./scripts/update-constant.ts
|
|
||||||
- name: Check if workspace is clean
|
|
||||||
run: git diff --exit-code
|
|
||||||
|
|
@ -27,9 +27,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
deno-version: 1.x
|
deno-version: 1.x
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 7.29.1
|
version: 8.11.0
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
|
||||||
89
CHANGELOG.md
89
CHANGELOG.md
|
|
@ -1,3 +1,92 @@
|
||||||
|
## 0.4.20
|
||||||
|
|
||||||
|
fix: update `NSOAPP_VERSION`
|
||||||
|
|
||||||
|
## 0.4.19
|
||||||
|
|
||||||
|
feat: update `NSOAPP_VERSION`
|
||||||
|
|
||||||
|
## 0.4.18
|
||||||
|
|
||||||
|
feat: update `WEB_VIEW_VERSION` and `NSOAPP_VERSION`, update VersionData
|
||||||
|
|
||||||
|
## 0.4.17
|
||||||
|
|
||||||
|
feat: update `WEB_VIEW_VERSION` and `NSOAPP_VERSION`
|
||||||
|
|
||||||
|
## 0.4.16
|
||||||
|
|
||||||
|
fix: `The stream is already locked` in monitor mode
|
||||||
|
|
||||||
|
feat: add Fresh Season 2024
|
||||||
|
|
||||||
|
## 0.4.15
|
||||||
|
|
||||||
|
feat: add znca headers ([#95](https://github.com/spacemeowx2/s3si.ts/issues/95))
|
||||||
|
|
||||||
|
feat: update User-Agent ([#94](https://github.com/spacemeowx2/s3si.ts/pull/94))
|
||||||
|
|
||||||
|
## 0.4.14
|
||||||
|
|
||||||
|
chore: update `WEB_VIEW_VERSION` and queries
|
||||||
|
|
||||||
|
fix: readLines may read corrupted data
|
||||||
|
|
||||||
|
## 0.4.13
|
||||||
|
|
||||||
|
refactor: upgraded the version of dependencies and fixed the deprecated API
|
||||||
|
([#92](https://github.com/spacemeowx2/s3si.ts/issues/92))
|
||||||
|
|
||||||
|
## 0.4.12
|
||||||
|
|
||||||
|
feat: add 6.0.0 special
|
||||||
|
|
||||||
|
## 0.4.11
|
||||||
|
|
||||||
|
chore: update `WEB_VIEW_VERSION` and queries
|
||||||
|
|
||||||
|
## 0.4.10
|
||||||
|
|
||||||
|
feat: support random primary ability
|
||||||
|
|
||||||
|
## 0.4.9
|
||||||
|
|
||||||
|
feat: add species and crown_type
|
||||||
|
|
||||||
|
## 0.4.8
|
||||||
|
|
||||||
|
chore: update `WEB_VIEW_VERSION` and queries
|
||||||
|
|
||||||
|
feat: update VersionData
|
||||||
|
|
||||||
|
## 0.4.7
|
||||||
|
|
||||||
|
chore: update `WEB_VIEW_VERSION`
|
||||||
|
|
||||||
|
## 0.4.6
|
||||||
|
|
||||||
|
chore: update constants
|
||||||
|
|
||||||
|
fix: skip updateState if history if empty
|
||||||
|
([#81](https://github.com/spacemeowx2/s3si.ts/issues/81))
|
||||||
|
|
||||||
|
## 0.4.5
|
||||||
|
|
||||||
|
fix: list method is not auto
|
||||||
|
|
||||||
|
## 0.4.4
|
||||||
|
|
||||||
|
feat: send Anarchy (Open) Power
|
||||||
|
|
||||||
|
## 0.4.3
|
||||||
|
|
||||||
|
feat: add `list-method` option
|
||||||
|
([#73](https://github.com/spacemeowx2/s3si.ts/issues/73))
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
fix: `coral_user_id` is string
|
||||||
|
|
||||||
## 0.4.1
|
## 0.4.1
|
||||||
|
|
||||||
feat: add support for Challenges
|
feat: add support for Challenges
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -3,7 +3,7 @@
|
||||||
[](https://github.com/spacemeowx2/s3si.ts/actions/workflows/ci.yaml)
|
[](https://github.com/spacemeowx2/s3si.ts/actions/workflows/ci.yaml)
|
||||||
[](https://github.com/spacemeowx2/s3si.ts/actions/workflows/constant-check.yaml)
|
[](https://github.com/spacemeowx2/s3si.ts/actions/workflows/constant-check.yaml)
|
||||||
|
|
||||||
Export your battles from SplatNet to stat.ink.
|
Export your battles from SplatNet to stat.ink and Splashcat.
|
||||||
|
|
||||||
If you have used s3s, please see [here](#migrate-from-s3s).
|
If you have used s3s, please see [here](#migrate-from-s3s).
|
||||||
|
|
||||||
|
|
@ -19,13 +19,18 @@ Options:
|
||||||
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
||||||
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
|
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
|
||||||
Multiple exporters can be separated by commas
|
Multiple exporters can be separated by commas
|
||||||
(e.g. "stat.ink,file")
|
(e.g. "stat.ink,file,splashcat")
|
||||||
|
--list-method When set to "latest", the latest 50 matches will be obtained.
|
||||||
|
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
|
||||||
|
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
|
||||||
|
"auto" is the default setting.
|
||||||
--no-progress, -n Disable progress bar
|
--no-progress, -n Disable progress bar
|
||||||
--monitor, -m Monitor mode
|
--monitor, -m Monitor mode
|
||||||
--skip-mode <mode>, -s Skip mode (default: null)
|
--skip-mode <mode>, -s Skip mode (default: null)
|
||||||
("vs", "coop")
|
("vs", "coop")
|
||||||
--with-summary Include summary in the output
|
--with-summary Include summary in the output
|
||||||
--help Show this help message and exit`,
|
--help Show this help message and exit
|
||||||
|
--nxapi-presence Extends monitoring mode to use Nintendo Switch presence from nxapi
|
||||||
```
|
```
|
||||||
|
|
||||||
3. If it's your first time running this, follow the instructions to login to
|
3. If it's your first time running this, follow the instructions to login to
|
||||||
|
|
@ -34,6 +39,12 @@ Options:
|
||||||
- If you want to use a different profile, use `-p` to specify the path to the
|
- If you want to use a different profile, use `-p` to specify the path to the
|
||||||
profile file.
|
profile file.
|
||||||
|
|
||||||
|
### Splashcat Notes
|
||||||
|
|
||||||
|
Due to limitations with SplatNet 3 data, Splashcat requires battles uploaded to
|
||||||
|
use `en-US` (set with `userLang`). Splashcat will localize most parts of battle
|
||||||
|
results into the user's language when displayed.
|
||||||
|
|
||||||
### Track your rank
|
### Track your rank
|
||||||
|
|
||||||
- Run
|
- Run
|
||||||
|
|
@ -67,7 +78,8 @@ Options:
|
||||||
// userLang will effect the language of the exported games to stat.ink
|
// userLang will effect the language of the exported games to stat.ink
|
||||||
"userLang": "zh-CN",
|
"userLang": "zh-CN",
|
||||||
"userCountry": "JP",
|
"userCountry": "JP",
|
||||||
"statInkApiKey": "..."
|
"statInkApiKey": "...",
|
||||||
|
"splashcatApiKey": "..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
329
deno.lock
329
deno.lock
|
|
@ -1,46 +1,167 @@
|
||||||
{
|
{
|
||||||
"version": "2",
|
"version": "3",
|
||||||
"remote": {
|
"remote": {
|
||||||
"https://deno.land/std@0.141.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
"https://deno.land/std@0.210.0/fmt/colors.ts": "2685c524bef9b16b3059a417daf6860c754eb755e19e812762ef5dff62f24481",
|
||||||
"https://deno.land/std@0.141.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
|
"https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||||
"https://deno.land/std@0.141.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
|
"https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
|
||||||
"https://deno.land/std@0.141.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
|
"https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
|
||||||
"https://deno.land/std@0.141.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
|
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
|
||||||
"https://deno.land/std@0.141.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
|
"https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
|
||||||
"https://deno.land/std@0.141.0/io/types.d.ts": "01f60ae7ec02675b5dbed150d258fc184a78dfe5c209ef53ba4422b46b58822c",
|
"https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
|
||||||
"https://deno.land/std@0.141.0/streams/conversion.ts": "8268f3f1a43324953dd8e9e4e31adb42e3caddb4502433bde03c279e43d70a3b",
|
"https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
|
||||||
"https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
"https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
|
||||||
"https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934",
|
"https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
|
||||||
"https://deno.land/std@0.160.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4",
|
"https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
|
||||||
"https://deno.land/std@0.160.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a",
|
"https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
|
||||||
"https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179",
|
"https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
|
||||||
"https://deno.land/std@0.160.0/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2",
|
"https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
|
||||||
"https://deno.land/std@0.160.0/flags/mod.ts": "686b6b36e14b00f11c9e26cecf439021158436a6e34f60eeb0d927f0b169ae20",
|
"https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
|
||||||
"https://deno.land/std@0.160.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289",
|
"https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
|
||||||
"https://deno.land/std@0.160.0/io/mod.ts": "6e781ebafd5cdccf9ab4afa1f499b08c513602d023cb08ceebc58758501f78bd",
|
"https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
|
||||||
"https://deno.land/std@0.160.0/io/readers.ts": "45847ad404afd2f605eae1cff193f223462bc55eeb9ae313c2f3db28aada0fd6",
|
"https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c",
|
||||||
"https://deno.land/std@0.160.0/io/types.d.ts": "107e1e64834c5ba917c783f446b407d33432c5d612c4b3430df64fc2b4ecf091",
|
"https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
|
||||||
"https://deno.land/std@0.160.0/io/util.ts": "23e706b4b6a3ebb34af00ad74d7549d906f3211fc98c1fba1185a36e017fb727",
|
"https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
|
||||||
"https://deno.land/std@0.160.0/io/writers.ts": "2e1c63ffd0cfba411b1fd8374609abff9ea86187c9d4d885d42e6fc20325ef0e",
|
"https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
|
||||||
"https://deno.land/std@0.160.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
|
"https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
|
||||||
"https://deno.land/std@0.160.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
|
"https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
|
||||||
"https://deno.land/std@0.160.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677",
|
"https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
|
||||||
"https://deno.land/std@0.160.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
|
"https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
|
||||||
"https://deno.land/std@0.160.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
|
"https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
|
||||||
"https://deno.land/std@0.160.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac",
|
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
|
||||||
"https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24",
|
"https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
|
||||||
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
|
"https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
|
||||||
"https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
|
"https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
|
||||||
"https://deno.land/std@0.160.0/streams/conversion.ts": "328afbedee0a7e0c330ac4c7b4c1af569ee53974f970230f6a78f545b93abb9b",
|
"https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
|
||||||
"https://deno.land/std@0.160.0/uuid/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
|
"https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
|
||||||
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5",
|
"https://deno.land/std@0.213.0/bytes/concat.ts": "9cac3b4376afbef98ff03588eb3cf948e0d1eb6c27cfe81a7651ab6dd3adc54a",
|
||||||
"https://deno.land/std@0.160.0/uuid/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
|
"https://deno.land/std@0.213.0/bytes/copy.ts": "f29c03168853720dfe82eaa57793d0b9e3543ebfe5306684182f0f1e3bfd422a",
|
||||||
"https://deno.land/std@0.160.0/uuid/v4.ts": "3e983c6ac895ea2a7ba03da927a2438fe1c26ac43fb38dc44f2f8aa50c23cb53",
|
"https://deno.land/std@0.213.0/crypto/_fnv/fnv32.ts": "ba2c5ef976b9f047d7ce2d33dfe18671afc75154bcf20ef89d932b2fe8820535",
|
||||||
"https://deno.land/std@0.160.0/uuid/v5.ts": "43973aeda44ad212f2ec9b8d6c042b74d5cef4ce583d6aa6fc4cdb339344c74c",
|
"https://deno.land/std@0.213.0/crypto/_fnv/fnv64.ts": "580cadfe2ff333fe253d15df450f927c8ac7e408b704547be26aab41b5772558",
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/cookie.ts": "72d6a6633ea13dd2f13b53d9726735b194996353a958024072c4d6b077c97baf",
|
"https://deno.land/std@0.213.0/crypto/_fnv/mod.ts": "8dbb60f062a6e77b82f7a62ac11fabfba52c3cd408c21916b130d8f57a880f96",
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/cookie_jar.ts": "9accd36e76929f2f06fa710d2165fb544703617245fa36ac63560b9fa2a22a25",
|
"https://deno.land/std@0.213.0/crypto/_fnv/util.ts": "27b36ce3440d0a180af6bf1cfc2c326f68823288540a354dc1d636b781b9b75f",
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/fetch_wrapper.ts": "d8918c0776413b2d4a675415727973390b4401a026f6dfdcffedce3296b5e0dc",
|
"https://deno.land/std@0.213.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "76c727912539737def4549bb62a96897f37eb334b979f49c57b8af7a1617635e",
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/mod.ts": "eff949014965771f2cd447fe78625a1ad28b59333afa40640f02c0922534d89a",
|
"https://deno.land/std@0.213.0/crypto/_wasm/mod.ts": "c55f91473846827f077dfd7e5fc6e2726dee5003b6a5747610707cdc638a22ba",
|
||||||
|
"https://deno.land/std@0.213.0/crypto/crypto.ts": "633e3ac52c496c52b1b6815dc6565db9af93a316665d2719bf7457f7342f372c",
|
||||||
|
"https://deno.land/std@0.213.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376",
|
||||||
|
"https://deno.land/std@0.213.0/encoding/base64.ts": "0ec6d6e6b68fc38f6396277e5184bcd47c1a9db0222fd0b563487eb67e352741",
|
||||||
|
"https://deno.land/std@0.213.0/flags/mod.ts": "58da4edceb20cbcb30fba78583e64525177aff1d80fb6b7f8bea85ecfd21463b",
|
||||||
|
"https://deno.land/std@0.213.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb",
|
||||||
|
"https://deno.land/std@0.213.0/io/_common.ts": "36705cdb4dfcd338d6131bca1b16e48a4d5bf0d1dada6ce397268e88c17a5835",
|
||||||
|
"https://deno.land/std@0.213.0/io/_constants.ts": "3c7ad4695832e6e4a32e35f218c70376b62bc78621ef069a4a0a3d55739f8856",
|
||||||
|
"https://deno.land/std@0.213.0/io/buf_reader.ts": "ccbd43ace0a9eebbd5e1b4765724b79da79d1e28b90c2b08537b99192da4a1f7",
|
||||||
|
"https://deno.land/std@0.213.0/io/buf_writer.ts": "bf68b9c74b1bccf51b9960c54db5eec60e7e3d922c7c62781b0d3971770021ba",
|
||||||
|
"https://deno.land/std@0.213.0/io/buffer.ts": "79182995c8340ece2fa8763a8da86d282c507e854921d0a4c2ba7425c63609ef",
|
||||||
|
"https://deno.land/std@0.213.0/io/copy.ts": "63c6a4acf71fb1e89f5e47a7b3b2972f9d2c56dd645560975ead72db7eb23f61",
|
||||||
|
"https://deno.land/std@0.213.0/io/copy_n.ts": "e4a169b8965b69e6a05175d06bf14565caa91266143ec895e54e95b6cdb27cf2",
|
||||||
|
"https://deno.land/std@0.213.0/io/limited_reader.ts": "2b3e6c2d134bbbabbc918584db5fd2f8b21091843357f75af0d9f262cb5c94c1",
|
||||||
|
"https://deno.land/std@0.213.0/io/mod.ts": "571384032c5f60530542a28f2e8b0e73e47e87eca77056ba7e2363f4d4a4573a",
|
||||||
|
"https://deno.land/std@0.213.0/io/multi_reader.ts": "ca8a7813208a3393dfaed75894d955fe58a38c21b880e69839a4e0547eadbf61",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_all.ts": "876c1cb20adea15349c72afc86cecd3573335845ae778967aefb5e55fe5a8a4a",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_delim.ts": "fb0884d97adc398877c6f59e1d1450be12e078790f52845fae7876dc119bb8f6",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_int.ts": "6ada4e0eec5044982df530e4de804e32ae757a2c318b57eba622d893841ffe2a",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_lines.ts": "34555eaa25269f6cfb9a842a03daedc9eae4f8295c8f933bd2b1639274ce89e3",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_long.ts": "199cba44526464f8499e1f3d96008d513bcadc8e5665356a9b84425cac6b16ad",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_range.ts": "a0c930ea61fdc3ea5520be4df34a7927fe8a2d6da9b04bfaa7b9588ef2e1a718",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_short.ts": "73777709ad41b6faeff3638c275a329cc820c1082f4dad07909f48875a35a71d",
|
||||||
|
"https://deno.land/std@0.213.0/io/read_string_delim.ts": "8c604ceea5c3c7ab244583570b467ce194238ace6d49b1d47f25d4f75de86d59",
|
||||||
|
"https://deno.land/std@0.213.0/io/slice_long_to_bytes.ts": "9769174a8f3b4449f1e1af1a79f78e58ef84d0aaf2f457e1fdc31a01f92439b7",
|
||||||
|
"https://deno.land/std@0.213.0/io/string_reader.ts": "b0176211e61e235a684abef722e7ecc7a6481238ba264f1a7b199b8a1d2a62f5",
|
||||||
|
"https://deno.land/std@0.213.0/io/string_writer.ts": "4fe4dcbdadff11c726bf79b0239e14fa9b1e8468a795b465622e4dbd6c1f819c",
|
||||||
|
"https://deno.land/std@0.213.0/io/to_readable_stream.ts": "ed03a44a1ec1cc55a85a857acf6cac472035298f6f3b6207ea209f93b4aefb39",
|
||||||
|
"https://deno.land/std@0.213.0/io/to_writable_stream.ts": "ef422e0425963c8a1e0481674e66c3023da50f0acbe5ef51ec9789efc3c1e2ed",
|
||||||
|
"https://deno.land/std@0.213.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96",
|
||||||
|
"https://deno.land/std@0.213.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/assert_path.ts": "2ca275f36ac1788b2acb60fb2b79cb06027198bc2ba6fb7e163efaedde98c297",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/common.ts": "6157c7ec1f4db2b4a9a187efd6ce76dcaf1e61cfd49f87e40d4ea102818df031",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/glob_to_reg_exp.ts": "2007aa87bed6eb2c8ae8381adcc3125027543d9ec347713c1ad2c68427330770",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/normalize_string.ts": "dfdf657a1b1a7db7999f7c575ee7e6b0551d9c20f19486c6c3f5ff428384c965",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a",
|
||||||
|
"https://deno.land/std@0.213.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883",
|
||||||
|
"https://deno.land/std@0.213.0/path/_interface.ts": "a1419fcf45c0ceb8acdccc94394e3e94f99e18cfd32d509aab514c8841799600",
|
||||||
|
"https://deno.land/std@0.213.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15",
|
||||||
|
"https://deno.land/std@0.213.0/path/basename.ts": "5d341aadb7ada266e2280561692c165771d071c98746fcb66da928870cd47668",
|
||||||
|
"https://deno.land/std@0.213.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643",
|
||||||
|
"https://deno.land/std@0.213.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36",
|
||||||
|
"https://deno.land/std@0.213.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c",
|
||||||
|
"https://deno.land/std@0.213.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441",
|
||||||
|
"https://deno.land/std@0.213.0/path/format.ts": "98fad25f1af7b96a48efb5b67378fcc8ed77be895df8b9c733b86411632162af",
|
||||||
|
"https://deno.land/std@0.213.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069",
|
||||||
|
"https://deno.land/std@0.213.0/path/glob_to_regexp.ts": "83c5fd36a8c86f5e72df9d0f45317f9546afa2ce39acaafe079d43a865aced08",
|
||||||
|
"https://deno.land/std@0.213.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7",
|
||||||
|
"https://deno.land/std@0.213.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141",
|
||||||
|
"https://deno.land/std@0.213.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a",
|
||||||
|
"https://deno.land/std@0.213.0/path/join_globs.ts": "e9589869a33dc3982101898ee50903db918ca00ad2614dbe3934d597d7b1fbea",
|
||||||
|
"https://deno.land/std@0.213.0/path/mod.ts": "ffeaccb713dbe6c72e015b7c767f753f8ec5fbc3b621ff5eeee486ffc2c0ddda",
|
||||||
|
"https://deno.land/std@0.213.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352",
|
||||||
|
"https://deno.land/std@0.213.0/path/normalize_glob.ts": "98ee8268fad271193603271c203ae973280b5abfbdd2cbca1053fd2af71869ca",
|
||||||
|
"https://deno.land/std@0.213.0/path/parse.ts": "65e8e285f1a63b714e19ef24b68f56e76934c3df0b6e65fd440d3991f4f8aefb",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/basename.ts": "39ee27a29f1f35935d3603ccf01d53f3d6e0c5d4d0f84421e65bd1afeff42843",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/dirname.ts": "6535d2bdd566118963537b9dda8867ba9e2a361015540dc91f5afbb65c0cce8b",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/extname.ts": "8d36ae0082063c5e1191639699e6f77d3acf501600a3d87b74943f0ae5327427",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/glob_to_regexp.ts": "54d3ff40f309e3732ab6e5b19d7111d2d415248bcd35b67a99defcbc1972e697",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/join.ts": "aef88d5fa3650f7516730865dbb951594d1a955b785e2450dbee93b8e32694f3",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/join_globs.ts": "ee2f4676c5b8a0dfa519da58b8ade4d1c4aa8dd3fe35619edec883ae9df1f8c9",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/mod.ts": "563a18c2b3ddc62f3e4a324ff0f583e819b8602a72ad880cb98c9e2e34f8db5b",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/normalize_glob.ts": "65f0138fa518ef9ece354f32889783fc38cdf985fb02dcf1c3b14fa47d665640",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/parse.ts": "d5bac4eb21262ab168eead7e2196cb862940c84cee572eafedd12a0d34adc8fb",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/resolve.ts": "bac20d9921beebbbb2b73706683b518b1d0c1b1da514140cee409e90d6b2913a",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/separator.ts": "c9ecae5c843170118156ac5d12dc53e9caf6a1a4c96fc8b1a0ab02dff5c847b0",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf",
|
||||||
|
"https://deno.land/std@0.213.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0",
|
||||||
|
"https://deno.land/std@0.213.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add",
|
||||||
|
"https://deno.land/std@0.213.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d",
|
||||||
|
"https://deno.land/std@0.213.0/path/separator.ts": "c6c890507f944a1f5cb7d53b8d638d6ce3cf0f34609c8d84a10c1eaa400b77a9",
|
||||||
|
"https://deno.land/std@0.213.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b",
|
||||||
|
"https://deno.land/std@0.213.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/basename.ts": "e2dbf31d1d6385bfab1ce38c333aa290b6d7ae9e0ecb8234a654e583cf22f8fe",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/glob_to_regexp.ts": "6dcd1242bd8907aa9660cbdd7c93446e6927b201112b0cba37ca5d80f81be51b",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/join.ts": "e0b3356615c1a75c56ebb6a7311157911659e11fd533d80d724800126b761ac3",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/join_globs.ts": "ee2f4676c5b8a0dfa519da58b8ade4d1c4aa8dd3fe35619edec883ae9df1f8c9",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/mod.ts": "7d6062927bda47c47847ffb55d8f1a37b0383840aee5c7dfc93984005819689c",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/normalize_glob.ts": "179c86ba89f4d3fe283d2addbe0607341f79ee9b1ae663abcfb3439db2e97810",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/parse.ts": "b9239edd892a06a06625c1b58425e199f018ce5649ace024d144495c984da734",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/resolve.ts": "75b2e3e1238d840782cee3d8864d82bfaa593c7af8b22f19c6422cf82f330ab3",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/separator.ts": "e51c5522140eff4f8402617c5c68a201fdfa3a1a8b28dc23587cff931b665e43",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/to_file_url.ts": "1cd63fd35ec8d1370feaa4752eccc4cc05ea5362a878be8dc7db733650995484",
|
||||||
|
"https://deno.land/std@0.213.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/_common.ts": "05c787c5735776c4e48e30294878332c39cb7738f50b209df4eb9f2b0facce4d",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/constants.ts": "eb6c96871e968adf3355507d7ae79adce71525fd6c1ca55c51d32ace0196d64e",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/mod.ts": "b9cb1cf73c3d87e15817486df7e885a63b74a7384768a927c708ccd045fcbf78",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/v1.ts": "a089755e9ba5a172f3d568b617164d4692bd71e548b018e039d1bcb17d4f1bb6",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/v3.ts": "aff081baee55498ed5804d006735a77b252ac1645e3b418058807218371de577",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/v4.ts": "8a9c60c887446651be5d50b468a3d702b87bb821fc35f0edcb5515c3bc07b256",
|
||||||
|
"https://deno.land/std@0.213.0/uuid/v5.ts": "f6771dc89e89f26e74a9b51d25d6b711c27d2ddf3a3650312dd46e7edfe2491e",
|
||||||
|
"https://deno.land/x/another_cookiejar@v5.0.4/cookie.ts": "2be7548d01a3a9df97deb187761a843a77fd824057478919abf1e1e89ae1eb2e",
|
||||||
|
"https://deno.land/x/another_cookiejar@v5.0.4/cookie_jar.ts": "e47d7b2c608bcd9600fd26825b600946f16ae167216cea71935049188d2fc6d1",
|
||||||
|
"https://deno.land/x/another_cookiejar@v5.0.4/fetch_wrapper.ts": "73434cb1b7d5e595eecdcc23e60af6fbc099b9e4cb82bb92d9f1617a85516286",
|
||||||
|
"https://deno.land/x/another_cookiejar@v5.0.4/mod.ts": "eff949014965771f2cd447fe78625a1ad28b59333afa40640f02c0922534d89a",
|
||||||
"https://deno.land/x/msgpack@v1.4/CachedKeyDecoder.ts": "c39b6f1572902ae08c0e4971f639e81031ac59403957fc43c6fb3c7fe69d99a1",
|
"https://deno.land/x/msgpack@v1.4/CachedKeyDecoder.ts": "c39b6f1572902ae08c0e4971f639e81031ac59403957fc43c6fb3c7fe69d99a1",
|
||||||
"https://deno.land/x/msgpack@v1.4/Decoder.ts": "bdb68309cd51da2b9a897f269784c6d636796258838a97f25b0e1b399c6f369b",
|
"https://deno.land/x/msgpack@v1.4/Decoder.ts": "bdb68309cd51da2b9a897f269784c6d636796258838a97f25b0e1b399c6f369b",
|
||||||
"https://deno.land/x/msgpack@v1.4/Encoder.ts": "4852bbacb30cd66eb2bd61a9e20476802458b991e13aacb5eb984d0348247ffe",
|
"https://deno.land/x/msgpack@v1.4/Encoder.ts": "4852bbacb30cd66eb2bd61a9e20476802458b991e13aacb5eb984d0348247ffe",
|
||||||
|
|
@ -57,128 +178,18 @@
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/stream.ts": "1315e29af5c1a40d97bfa6f1c4f7f73d26067b912236f56851981f2f049500b8",
|
"https://deno.land/x/msgpack@v1.4/utils/stream.ts": "1315e29af5c1a40d97bfa6f1c4f7f73d26067b912236f56851981f2f049500b8",
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/typedArrays.ts": "bb819c2f28cf7f85ed50b2e57f108462715555cc61ce315e8134cf1eef2ae662",
|
"https://deno.land/x/msgpack@v1.4/utils/typedArrays.ts": "bb819c2f28cf7f85ed50b2e57f108462715555cc61ce315e8134cf1eef2ae662",
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/utf8.ts": "93183055a7a41986080eeb711e83d553e7c8b121642da4379a5adf253b7beefd",
|
"https://deno.land/x/msgpack@v1.4/utils/utf8.ts": "93183055a7a41986080eeb711e83d553e7c8b121642da4379a5adf253b7beefd",
|
||||||
"https://deno.land/x/progress@v1.2.8/deps.ts": "e0abdc972a0c152508b28ced5ae9c4be26a5773f0aa4a3caa72371c84d2e28a2",
|
"https://deno.land/x/murmurhash@v1.0.0/mod.ts": "13fd2c5534dfd22ffbfcd4255ea13e47a2f2b99e9c90a83dc43e814a0e278829",
|
||||||
"https://deno.land/x/progress@v1.2.8/mod.ts": "5ef7c7ff079d71effed5055666af81cc58a566bc98e2df8473526bd6457976c5",
|
"https://deno.land/x/progress@v1.4.5/deps.ts": "f2886f3f87af20b397ffcf9723a0fabc5893491ce9ce7615a37b1d7a38539247",
|
||||||
"https://deno.land/x/progress@v1.2.8/multi.ts": "392553552243204539d83ee53cadda990db20b1b421520411318ff9bd0320646",
|
"https://deno.land/x/progress@v1.4.5/mod.ts": "e26996fb5f23863c3402133896a9739ea4059b155a3d89ba207cad10b50524ea",
|
||||||
"https://deno.land/x/semaphore@v1.1.1/mod.ts": "431abb51927a16c537cec1cfb05bf2de6a8f3916331f1ec3f9f13ad7ad6a4ea5",
|
"https://deno.land/x/progress@v1.4.5/multi.ts": "bf50eff76d4c1b1b1a3118e73a58631b19b0b30e0dd4166ae4ef0886efae88a5",
|
||||||
"https://deno.land/x/semaphore@v1.1.1/mutex.ts": "2cc6490481f0fdfe97c6b326a2073819d76b76eac3877864a8ada6a2127492f2",
|
"https://deno.land/x/progress@v1.4.5/time.ts": "001198ff9fe2a452830515fc944665c4369990102978b325e1c9094486cfd8ab",
|
||||||
"https://deno.land/x/semaphore@v1.1.1/semaphore.ts": "0acf1159d635fa3b9198a4ad4acac9e877d79196601aa80544ac0db5a71c646d",
|
"https://deno.land/x/semaphore@v1.1.2/mod.ts": "431abb51927a16c537cec1cfb05bf2de6a8f3916331f1ec3f9f13ad7ad6a4ea5",
|
||||||
|
"https://deno.land/x/semaphore@v1.1.2/mutex.ts": "2cc6490481f0fdfe97c6b326a2073819d76b76eac3877864a8ada6a2127492f2",
|
||||||
|
"https://deno.land/x/semaphore@v1.1.2/semaphore.ts": "a3da40292cd49c3f31be392aa16831d00c1ddf6daca50dd74eb61aa6ae8f52a3",
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/functions.ts": "20681c98ce82d503dba56f5ef9313c196f18a2317ce7c0c331cc3fdea0d56688",
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/functions.ts": "20681c98ce82d503dba56f5ef9313c196f18a2317ce7c0c331cc3fdea0d56688",
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/literal-types/mod.ts": "c1b9e16a7e49814e9509bed8a5dec25b717761a37d0ef1589d411bd6130dd2e5",
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/literal-types/mod.ts": "c1b9e16a7e49814e9509bed8a5dec25b717761a37d0ef1589d411bd6130dd2e5",
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b",
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b",
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603",
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603",
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345"
|
"https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345"
|
||||||
},
|
|
||||||
"npm": {
|
|
||||||
"specifiers": {
|
|
||||||
"mongodb": "mongodb@5.5.0",
|
|
||||||
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"@types/node@18.14.2": {
|
|
||||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"@types/webidl-conversions@7.0.0": {
|
|
||||||
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"@types/whatwg-url@8.2.2": {
|
|
||||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "@types/node@18.14.2",
|
|
||||||
"@types/webidl-conversions": "@types/webidl-conversions@7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bson@5.0.1": {
|
|
||||||
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"bson@5.3.0": {
|
|
||||||
"integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"ip@2.0.0": {
|
|
||||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"memory-pager@1.5.0": {
|
|
||||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"mongodb-connection-string-url@2.6.0": {
|
|
||||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/whatwg-url": "@types/whatwg-url@8.2.2",
|
|
||||||
"whatwg-url": "whatwg-url@11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mongodb@5.1.0": {
|
|
||||||
"integrity": "sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw==",
|
|
||||||
"dependencies": {
|
|
||||||
"bson": "bson@5.0.1",
|
|
||||||
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
|
|
||||||
"saslprep": "saslprep@1.0.3",
|
|
||||||
"socks": "socks@2.7.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mongodb@5.5.0": {
|
|
||||||
"integrity": "sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"bson": "bson@5.3.0",
|
|
||||||
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
|
|
||||||
"saslprep": "saslprep@1.0.3",
|
|
||||||
"socks": "socks@2.7.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"punycode@2.3.0": {
|
|
||||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"saslprep@1.0.3": {
|
|
||||||
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
|
||||||
"dependencies": {
|
|
||||||
"sparse-bitfield": "sparse-bitfield@3.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"smart-buffer@4.2.0": {
|
|
||||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"socks@2.7.1": {
|
|
||||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ip": "ip@2.0.0",
|
|
||||||
"smart-buffer": "smart-buffer@4.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sparse-bitfield@3.0.3": {
|
|
||||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"memory-pager": "memory-pager@1.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"splatnet3-types@0.2.20230227204004": {
|
|
||||||
"integrity": "sha512-FAY6pbUcrp5O8c49BNXSKxoyM3UlCrRx2AtA9Y3qlvqOLdHqwxtzcdzbk1b1hRam8ZcrxRzE/ii6ESRiPIAnZw==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"tr46@3.0.0": {
|
|
||||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "punycode@2.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webidl-conversions@7.0.0": {
|
|
||||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"whatwg-url@11.0.0": {
|
|
||||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "tr46@3.0.0",
|
|
||||||
"webidl-conversions": "webidl-conversions@7.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
deps.ts
21
deps.ts
|
|
@ -2,17 +2,14 @@ export {
|
||||||
Cookie,
|
Cookie,
|
||||||
CookieJar,
|
CookieJar,
|
||||||
wrapFetch,
|
wrapFetch,
|
||||||
} from "https://deno.land/x/another_cookiejar@v4.1.4/mod.ts";
|
} from "https://deno.land/x/another_cookiejar@v5.0.4/mod.ts";
|
||||||
export type { CookieOptions } from "https://deno.land/x/another_cookiejar@v4.1.4/mod.ts";
|
export type { CookieOptions } from "https://deno.land/x/another_cookiejar@v5.0.4/mod.ts";
|
||||||
export * as base64 from "https://deno.land/std@0.160.0/encoding/base64.ts";
|
export * as base64 from "https://deno.land/std@0.213.0/encoding/base64.ts";
|
||||||
export * as flags from "https://deno.land/std@0.160.0/flags/mod.ts";
|
export * as flags from "https://deno.land/std@0.213.0/flags/mod.ts";
|
||||||
export * as io from "https://deno.land/std@0.160.0/io/mod.ts";
|
export * as io from "https://deno.land/std@0.213.0/io/mod.ts";
|
||||||
export * as uuid from "https://deno.land/std@0.160.0/uuid/mod.ts";
|
export * as uuid from "https://deno.land/std@0.213.0/uuid/mod.ts";
|
||||||
export * as msgpack from "https://deno.land/x/msgpack@v1.4/mod.ts";
|
export * as msgpack from "https://deno.land/x/msgpack@v1.4/mod.ts";
|
||||||
export * as path from "https://deno.land/std@0.160.0/path/mod.ts";
|
export * as path from "https://deno.land/std@0.213.0/path/mod.ts";
|
||||||
export { MultiProgressBar } from "https://deno.land/x/progress@v1.2.8/mod.ts";
|
export { MultiProgressBar } from "https://deno.land/x/progress@v1.4.5/mod.ts";
|
||||||
export { Mutex } from "https://deno.land/x/semaphore@v1.1.1/mod.ts";
|
export { Mutex } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";
|
||||||
export type { DeepReadonly } from "https://deno.land/x/ts_essentials@v9.1.2/mod.ts";
|
export type { DeepReadonly } from "https://deno.land/x/ts_essentials@v9.1.2/mod.ts";
|
||||||
export * as MongoDB from "npm:mongodb";
|
|
||||||
export * as splatNet3Types from "npm:splatnet3-types/splatnet3";
|
|
||||||
export { writeAll } from "https://deno.land/std@0.160.0/streams/conversion.ts";
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { assertEquals } from "https://deno.land/std@0.160.0/testing/asserts.ts";
|
export { assertEquals } from "https://deno.land/std@0.213.0/assert/mod.ts";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Tauri + React + Typescript
|
# s3si.ts GUI
|
||||||
|
|
||||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
## Development
|
||||||
|
|
||||||
## Recommended IDE Setup
|
```
|
||||||
|
pnpm tauri dev
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
```
|
||||||
|
|
|
||||||
|
|
@ -11,50 +11,39 @@
|
||||||
"lint": "eslint --max-warnings=0 src"
|
"lint": "eslint --max-warnings=0 src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.5.3",
|
||||||
"classnames": "^2.3.2",
|
"clsx": "^2.1.0",
|
||||||
"daisyui": "^2.52.0",
|
"daisyui": "^4.6.1",
|
||||||
"i18next": "^22.5.0",
|
"i18next": "^23.8.1",
|
||||||
"i18next-browser-languagedetector": "^7.0.2",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^12.3.1",
|
"react-error-boundary": "^4.0.12",
|
||||||
"react-icons": "^4.9.0",
|
"react-i18next": "^14.0.1",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-icons": "^5.0.1",
|
||||||
"react-use": "^17.4.0"
|
"react-router-dom": "^6.21.3",
|
||||||
|
"react-use": "^17.5.0",
|
||||||
|
"swr": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"@types/node": "^20.2.5",
|
"@types/node": "^20.11.10",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
"eslint": "^8.41.0",
|
"@typescript-eslint/typescript-estree": "^6.20.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"i18next-http-backend": "^2.2.1",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.24",
|
"eslint": "^8.56.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"typescript": "^5.0.4",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"vite": "^4.3.9",
|
"i18next-http-backend": "^2.4.2",
|
||||||
|
"postcss": "^8.4.33",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.12",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.2.0"
|
"vite-tsconfig-paths": "^4.3.1"
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "react-app"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"packageExtensions": {
|
|
||||||
"eslint-plugin-flowtype": {
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@babel/plugin-syntax-flow": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@babel/plugin-transform-react-jsx": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4369
gui/pnpm-lock.yaml
4369
gui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -10,14 +10,22 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.2", features = [] }
|
tauri-build = { version = "1.5.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1.2", features = ["fs-all", "path-all", "process-relaunch", "shell-execute", "shell-open", "shell-sidecar", "window-all"] }
|
tauri = { version = "1.5.4", features = [
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
"fs-all",
|
||||||
serde_json = "1.0"
|
"path-all",
|
||||||
tokio = { version = "1.0", features = ["time"] }
|
"process-relaunch",
|
||||||
urlencoding = "2.1.2"
|
"shell-execute",
|
||||||
|
"shell-open",
|
||||||
|
"shell-sidecar",
|
||||||
|
"window-all",
|
||||||
|
] }
|
||||||
|
serde = { version = "^1.0.196", features = ["derive"] }
|
||||||
|
serde_json = "^1.0.113"
|
||||||
|
tokio = { version = "^1.35.1", features = ["time"] }
|
||||||
|
backtrace = "^0.3.69"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,12 @@ function onSelectUserClick(e) {
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// very hacky way...
|
// a little official way...
|
||||||
window.ipc.postMessage(JSON.stringify({
|
window.__TAURI_INVOKE__({
|
||||||
"cmd":"tauri",
|
|
||||||
"callback":0,
|
|
||||||
"error":0,
|
|
||||||
"__tauriModule":"Event",
|
"__tauriModule":"Event",
|
||||||
|
"cmd": "tauri",
|
||||||
"message":{"cmd":"emit","event":"login","payload":{"url":element.href}}
|
"message":{"cmd":"emit","event":"login","payload":{"url":element.href}}
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
function detectAndInject() {
|
function detectAndInject() {
|
||||||
const element = document.getElementById('authorize-switch-approval-link');
|
const element = document.getElementById('authorize-switch-approval-link');
|
||||||
|
|
@ -74,18 +72,13 @@ document.addEventListener("DOMContentLoaded", () => {{
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn open_login_window(app: tauri::AppHandle, url: String) -> Option<String> {
|
async fn open_login_window(app: tauri::AppHandle, url: String) -> Option<String> {
|
||||||
let encoded = urlencoding::encode(&url);
|
let window = WindowBuilder::new(&app, "login", tauri::WindowUrl::App(url.into()))
|
||||||
let window = WindowBuilder::new(
|
.title("Login")
|
||||||
&app,
|
.center()
|
||||||
"login",
|
.inner_size(1040.0, 960.0)
|
||||||
tauri::WindowUrl::App(format!("/redirect?url={encoded}").into()),
|
.initialization_script(INIT_SCRIPT)
|
||||||
)
|
.build()
|
||||||
.title("Login")
|
.ok()?;
|
||||||
.center()
|
|
||||||
.inner_size(1040.0, 960.0)
|
|
||||||
.initialization_script(INIT_SCRIPT)
|
|
||||||
.build()
|
|
||||||
.ok()?;
|
|
||||||
let result: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
let result: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||||
let r2 = result.clone();
|
let r2 = result.clone();
|
||||||
let r3 = result.clone();
|
let r3 = result.clone();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"$schema": "https://github.com/tauri-apps/tauri/raw/tauri-v1.4.1/core/tauri-config-schema/schema.json",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"beforeBuildCommand": "pnpm build",
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
|
@ -8,7 +9,7 @@
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "s3si-ts",
|
"productName": "s3si-ts",
|
||||||
"version": "0.4.1"
|
"version": "0.4.20"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
|
@ -69,7 +70,16 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null,
|
||||||
|
"dangerousRemoteDomainIpcAccess": [
|
||||||
|
{
|
||||||
|
"windows": [
|
||||||
|
"login"
|
||||||
|
],
|
||||||
|
"domain": "accounts.nintendo.com",
|
||||||
|
"enableTauriAPI": true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": false,
|
"active": false,
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,21 @@ import { Layout } from "components/Layout";
|
||||||
import { Home } from "pages/Home";
|
import { Home } from "pages/Home";
|
||||||
import { Settings } from "pages/Settings";
|
import { Settings } from "pages/Settings";
|
||||||
import { Guide } from 'pages/Guide';
|
import { Guide } from 'pages/Guide';
|
||||||
import { RedirectLogin } from 'pages/RedirectLogin';
|
|
||||||
import { useShowWindow } from 'hooks/useShowWindow';
|
import { useShowWindow } from 'hooks/useShowWindow';
|
||||||
|
import { AppContextProvider } from 'context/app';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useShowWindow();
|
useShowWindow();
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<AppContextProvider>
|
||||||
<Route path='/' element={<Layout />}>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route path='/' element={<Layout />}>
|
||||||
<Route path='/settings' element={<Settings />} />
|
<Route index element={<Home />} />
|
||||||
<Route path='/guide' element={<Guide />} />
|
<Route path='/settings' element={<Settings />} />
|
||||||
<Route path='/redirect' element={<RedirectLogin />} />
|
<Route path='/guide' element={<Guide />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</AppContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ export const CheckUpdate: React.FC<{ className?: string, children?: ReactNode }>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<button className={className} onClick={onClick}>{children}</button>
|
<button type='button' className={className} onClick={onClick}>{children}</button>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { AiOutlineWarning } from 'react-icons/ai'
|
import { AiOutlineWarning } from 'react-icons/ai'
|
||||||
|
import { FallbackProps } from 'react-error-boundary'
|
||||||
|
|
||||||
type ErrorContentProps = {
|
type ErrorContentProps = {
|
||||||
error: any
|
error: unknown
|
||||||
retry?: () => void
|
retry?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,9 +19,14 @@ export const ErrorContent: React.FC<ErrorContentProps> = ({ error, retry }) => {
|
||||||
<span className='inline-flex items-center'>
|
<span className='inline-flex items-center'>
|
||||||
<AiOutlineWarning className='inline-block scale-[2] mr-4 justify-end flex-none' />
|
<AiOutlineWarning className='inline-block scale-[2] mr-4 justify-end flex-none' />
|
||||||
<div className='max-w-full break-all'>
|
<div className='max-w-full break-all'>
|
||||||
<div>{t('发生了错误')}{retry && <button className='link link-info ml-1'>{t('重试')}</button>}</div>
|
<div>{t('发生了错误')}{retry && <button type='button' className='link link-info ml-1'>{t('重试')}</button>}</div>
|
||||||
{String(error)}
|
{String(error)}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const FallbackComponent: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
|
||||||
|
console.error('FallbackComponent', error)
|
||||||
|
return <ErrorContent error={error} retry={resetErrorBoundary} />
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@ type HeaderProps = {
|
||||||
export const Header: React.FC<HeaderProps> = ({ title }) => {
|
export const Header: React.FC<HeaderProps> = ({ title }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return <>
|
return <>
|
||||||
<h2 className="card-title" data-tauri-drag-region><button onClick={() => navigate(-1)}><AiOutlineLeft /></button>{title}</h2>
|
<h2 className="card-title" data-tauri-drag-region><button type='button' onClick={() => navigate(-1)}><AiOutlineLeft /></button>{title}</h2>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import { usePromise } from 'hooks/usePromise';
|
import { useService, useServiceMutation } from 'services/useService';
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { getConfig, getProfile, setProfile } from 'services/config';
|
|
||||||
import { ensureTokenValid } from 'services/s3si';
|
import { ensureTokenValid } from 'services/s3si';
|
||||||
import { composeLoadable } from 'utils/composeLoadable';
|
|
||||||
import { ErrorContent } from './ErrorContent';
|
import { ErrorContent } from './ErrorContent';
|
||||||
|
|
||||||
type OpenSplatnetProps = {
|
type OpenSplatnetProps = {
|
||||||
|
|
@ -12,30 +10,29 @@ type OpenSplatnetProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
|
export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
|
||||||
let { loading, error, retry, result } = composeLoadable({
|
const profileResult = useService('profile', 0)
|
||||||
config: usePromise(getConfig),
|
const { trigger: setProfile } = useServiceMutation('profile', 0)
|
||||||
profile: usePromise(() => getProfile(0)),
|
|
||||||
});
|
|
||||||
const [doing, setDoing] = useState(false);
|
const [doing, setDoing] = useState(false);
|
||||||
const [err, setError] = useState<any>();
|
const [err, setError] = useState<unknown>();
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
setDoing(true);
|
setDoing(true);
|
||||||
try {
|
try {
|
||||||
if (!result) {
|
if (!profileResult.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const state = result.profile.state;
|
const state = profileResult.data.state;
|
||||||
const newState = await ensureTokenValid(state);
|
const newState = await ensureTokenValid(state);
|
||||||
await setProfile(0, {
|
await setProfile({
|
||||||
...result.profile,
|
...profileResult.data,
|
||||||
state: newState,
|
state: newState,
|
||||||
});
|
});
|
||||||
retry?.();
|
|
||||||
const gtoken = newState.loginState?.gToken;
|
const gtoken = newState.loginState?.gToken;
|
||||||
await invoke('open_splatnet', {
|
await invoke('open_splatnet', {
|
||||||
gtoken,
|
gtoken,
|
||||||
lang: result.profile.state.userLang,
|
lang: profileResult.data.state.userLang,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e);
|
setError(e);
|
||||||
|
|
@ -45,16 +42,21 @@ export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (error || err) {
|
if (err) {
|
||||||
return <>
|
return <>
|
||||||
<ErrorContent error={error || err} retry={retry} />
|
<ErrorContent error={err} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const btnLoading = profileResult.isLoading || doing;
|
||||||
return <>
|
return <>
|
||||||
<button className={classNames('btn', {
|
<button
|
||||||
'btn-disabled': !result?.profile.state.loginState?.sessionToken,
|
type='button'
|
||||||
'loading': loading || doing,
|
className={clsx('btn w-full', {
|
||||||
})} onClick={onClick}>{children}</button>
|
'btn-disabled': !profileResult.data?.state?.loginState?.sessionToken,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={btnLoading}
|
||||||
|
>{btnLoading ? <span className='loading' /> : children}</button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,39 @@
|
||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import { usePromise } from 'hooks/usePromise';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { canExport, getProfile, setProfile } from 'services/config';
|
import { useLog } from 'services/s3si';
|
||||||
import { addLog, run, useLog } from 'services/s3si';
|
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
import { Loading } from './Loading';
|
import { Loading } from './Loading';
|
||||||
|
import { useService } from 'services/useService';
|
||||||
|
import { useAppContext } from 'context/app'
|
||||||
|
|
||||||
type RunPanelProps = {
|
type RunPanelProps = Record<string, never>
|
||||||
}
|
|
||||||
|
|
||||||
export const RunPanel: React.FC<RunPanelProps> = () => {
|
export const RunPanel: React.FC<RunPanelProps> = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { result } = usePromise(() => getProfile(0));
|
const { data: result } = useService('profile', 0)
|
||||||
const [exportBattle, setExportBattle] = useState(true);
|
const [exportBattle, setExportBattle] = useState(true);
|
||||||
const [exportCoop, setExportCoop] = useState(true);
|
const [exportCoop, setExportCoop] = useState(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const { exports } = useAppContext()
|
||||||
|
const disabled = !exports
|
||||||
|
const isExporting = exports?.isExporting ?? false
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return <Loading />
|
return <Loading />
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
addLog({
|
|
||||||
level: 'log',
|
|
||||||
msg: ['Export started at', new Date().toLocaleString()],
|
|
||||||
})
|
|
||||||
const { state } = result;
|
|
||||||
const newState = await run(state, {
|
|
||||||
exporter: "stat.ink",
|
|
||||||
monitor: false,
|
|
||||||
withSummary: false,
|
|
||||||
skipMode: exportBattle === false ? 'vs' : exportCoop === false ? 'coop' : undefined,
|
|
||||||
});
|
|
||||||
await setProfile(0, {
|
|
||||||
...result,
|
|
||||||
state: newState,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
addLog({
|
|
||||||
level: 'error',
|
|
||||||
msg: [e],
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
addLog({
|
|
||||||
level: 'log',
|
|
||||||
msg: ['Export ended at', new Date().toLocaleString()],
|
|
||||||
})
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const disabled = !canExport(result);
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className="tooltip" data-tip={disabled ? t('请先在设置中完成Nintendo Account登录和stat.ink的API密钥') : undefined}>
|
<div className="tooltip" data-tip={disabled ? t('请先在设置中完成Nintendo Account登录和stat.ink的API密钥') : undefined}>
|
||||||
<Checkbox disabled={disabled || loading} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
<Checkbox disabled={disabled || isExporting} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
||||||
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
<Checkbox disabled={disabled || isExporting} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
type='button'
|
||||||
className={classNames('btn w-full', {
|
onClick={() => exports?.trigger({ exportBattle, exportCoop })}
|
||||||
|
className={clsx('btn btn-primary w-full', {
|
||||||
'btn-disabled': disabled || (!exportBattle && !exportCoop),
|
'btn-disabled': disabled || (!exportBattle && !exportCoop),
|
||||||
'loading': loading,
|
|
||||||
})}
|
})}
|
||||||
>{t('导出')}</button>
|
disabled={isExporting}
|
||||||
|
>{isExporting ? <span className='loading' /> : t('导出')}</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { ReactNode, createContext, useContext } from 'react'
|
||||||
|
import { canExport } from 'services/config';
|
||||||
|
import { addLog, run } from 'services/s3si';
|
||||||
|
import { useService, useServiceMutation } from 'services/useService';
|
||||||
|
import useSWRMutation from 'swr/mutation';
|
||||||
|
|
||||||
|
export type ExportArgs = {
|
||||||
|
exportBattle: boolean,
|
||||||
|
exportCoop: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP_CONTEXT = createContext<{
|
||||||
|
exports?: {
|
||||||
|
isExporting: boolean
|
||||||
|
trigger: (args: ExportArgs) => Promise<void>
|
||||||
|
}
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
export const useAppContext = () => {
|
||||||
|
return useContext(APP_CONTEXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppContextProvider: React.FC<{ children?: ReactNode }> = ({ children }) => {
|
||||||
|
const { data: result } = useService('profile', 0)
|
||||||
|
const { trigger: setProfile } = useServiceMutation('profile', 0)
|
||||||
|
const { trigger: doExport, isMutating } = useSWRMutation<
|
||||||
|
unknown,
|
||||||
|
Error,
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
exportBattle: boolean,
|
||||||
|
exportCoop: boolean,
|
||||||
|
}
|
||||||
|
>('export', async (_, { arg: {
|
||||||
|
exportBattle, exportCoop,
|
||||||
|
} }) => {
|
||||||
|
try {
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addLog({
|
||||||
|
level: 'log',
|
||||||
|
msg: ['Export started at', new Date().toLocaleString()],
|
||||||
|
})
|
||||||
|
const { state } = result;
|
||||||
|
const newState = await run(state, {
|
||||||
|
exporter: "stat.ink",
|
||||||
|
monitor: false,
|
||||||
|
withSummary: false,
|
||||||
|
skipMode: exportBattle === false ? 'vs' : exportCoop === false ? 'coop' : undefined,
|
||||||
|
});
|
||||||
|
await setProfile({
|
||||||
|
...result,
|
||||||
|
state: newState,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
addLog({
|
||||||
|
level: 'error',
|
||||||
|
msg: [e],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
addLog({
|
||||||
|
level: 'log',
|
||||||
|
msg: ['Export ended at', new Date().toLocaleString()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return <APP_CONTEXT.Provider value={{
|
||||||
|
exports: result && canExport(result) ? {
|
||||||
|
isExporting: isMutating,
|
||||||
|
trigger: async (args: ExportArgs) => {
|
||||||
|
await doExport(args)
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</APP_CONTEXT.Provider>
|
||||||
|
}
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hook that returns a promise and its state.
|
|
||||||
*
|
|
||||||
* @param factory A function that returns a promise.
|
|
||||||
* @returns An object containing the promise's state and result.
|
|
||||||
* @example
|
|
||||||
* const { loading, result, error } = usePromise(() => fetch('https://example.com')
|
|
||||||
* .then(response => response.text())
|
|
||||||
* );
|
|
||||||
* if (loading) {
|
|
||||||
* return <p>Loading...</p>;
|
|
||||||
* }
|
|
||||||
* if (error) {
|
|
||||||
* return <p>Error: {error.message}</p>;
|
|
||||||
* }
|
|
||||||
* return <p>Result: {result}</p>;
|
|
||||||
*/
|
|
||||||
export function usePromise<T>(factory: () => Promise<T>) {
|
|
||||||
const init = () => {
|
|
||||||
const promise = factory();
|
|
||||||
if (!promise || typeof promise.then !== "function") {
|
|
||||||
throw new Error("The factory function must return a promise.");
|
|
||||||
}
|
|
||||||
return promise
|
|
||||||
.then(r => {
|
|
||||||
setResult(r);
|
|
||||||
setLoading(false);
|
|
||||||
return r;
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
setError(e);
|
|
||||||
setLoading(false);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [result, setResult] = useState<T | undefined>(undefined);
|
|
||||||
const [error, setError] = useState<any | undefined>(undefined);
|
|
||||||
const [promise, setPromise] = useState(init);
|
|
||||||
const retry = () => {
|
|
||||||
setLoading(true);
|
|
||||||
setResult(undefined);
|
|
||||||
setError(undefined);
|
|
||||||
setPromise(init);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { loading, result, error, promise, retry };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hook that returns a promise and its state.
|
|
||||||
*/
|
|
||||||
export function usePromiseLazy<T, Args extends any[]>(factory: (...args: Args) => Promise<T>) {
|
|
||||||
const init = (promise: Promise<T>) => {
|
|
||||||
if (!promise || typeof promise.then !== "function") {
|
|
||||||
throw new Error("The factory function must return a promise.");
|
|
||||||
}
|
|
||||||
return promise
|
|
||||||
.then(r => {
|
|
||||||
setResult(r);
|
|
||||||
setLoading(false);
|
|
||||||
return r;
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
setError(e);
|
|
||||||
setLoading(false);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [result, setResult] = useState<T | undefined>(undefined);
|
|
||||||
const [error, setError] = useState<any | undefined>(undefined);
|
|
||||||
const [promise, setPromise] = useState<Promise<T> | undefined>(undefined);
|
|
||||||
const execute = (...args: Args) => {
|
|
||||||
setLoading(true);
|
|
||||||
setResult(undefined);
|
|
||||||
setError(undefined);
|
|
||||||
setPromise(init(factory(...args)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [execute, { loading, result, error, promise }] as const;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
type Maybe<T> = T | null | undefined;
|
type Maybe<T> = T | null | undefined;
|
||||||
type KeyOf<T extends Record<string, any>, K = keyof T> = K extends string ? (T[K] extends Function ? never : K) : never;
|
type KeyOf<T extends Record<string, any>, K = keyof T> = K extends string ? (T[K] extends Function ? never : K) : never;
|
||||||
type DotField<T extends Maybe<Record<string, any>>, K = KeyOf<NonNullable<T>>> = K extends string
|
type DotField<T extends Maybe<Record<string, any>>, K = KeyOf<NonNullable<T>>> = K extends string
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export class JSONRPCClient<S extends Service> {
|
||||||
protected transport: Transport;
|
protected transport: Transport;
|
||||||
protected requestMap: Map<
|
protected requestMap: Map<
|
||||||
ID,
|
ID,
|
||||||
(result: RPCResult<any, ResponseError>) => void
|
(result: RPCResult<unknown, ResponseError>) => void
|
||||||
> = new Map();
|
> = new Map();
|
||||||
protected fatal: unknown = undefined;
|
protected fatal: unknown = undefined;
|
||||||
protected task: Promise<void>;
|
protected task: Promise<void>;
|
||||||
|
|
@ -55,6 +55,7 @@ export class JSONRPCClient<S extends Service> {
|
||||||
// receive response from server
|
// receive response from server
|
||||||
protected async run() {
|
protected async run() {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
const data = await this.transport.recv();
|
const data = await this.transport.recv();
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
|
|
@ -111,7 +112,7 @@ export class JSONRPCClient<S extends Service> {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
rej(new JSONRPCError(result.error));
|
rej(new JSONRPCError(result.error));
|
||||||
} else {
|
} else {
|
||||||
res(result.result);
|
res(result.result as R);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -120,6 +121,7 @@ export class JSONRPCClient<S extends Service> {
|
||||||
getProxy(): S {
|
getProxy(): S {
|
||||||
const proxy = new Proxy({}, {
|
const proxy = new Proxy({}, {
|
||||||
get: (_, method: string) => {
|
get: (_, method: string) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return (...params: unknown[]) => this.call(method, ...params as any);
|
return (...params: unknown[]) => this.call(method, ...params as any);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,17 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
color: #f6f6f6;
|
|
||||||
background-color: #2f2f2f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* custom classes */
|
/* custom classes */
|
||||||
|
|
||||||
.flex-auto-all > * {
|
.flex-auto-all > * {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import { Header } from 'components/Header';
|
import { Header } from 'components/Header';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -30,14 +30,16 @@ const Steps: React.FC<{ steps: Step[], className?: string }> = ({ className, ste
|
||||||
{Content && <Content onChange={setState} />}
|
{Content && <Content onChange={setState} />}
|
||||||
<div className='mt-4 flex gap-2'>
|
<div className='mt-4 flex gap-2'>
|
||||||
<button
|
<button
|
||||||
|
type='button'
|
||||||
onClick={() => setStep(s => s - 1)}
|
onClick={() => setStep(s => s - 1)}
|
||||||
className={classNames('btn', {
|
className={clsx('btn', {
|
||||||
'btn-disabled': !hasPrev || !state.prev,
|
'btn-disabled': !hasPrev || !state.prev,
|
||||||
})}
|
})}
|
||||||
>{t('上一步')}</button>
|
>{t('上一步')}</button>
|
||||||
<button
|
<button
|
||||||
|
type='button'
|
||||||
onClick={() => setStep(s => s + 1)}
|
onClick={() => setStep(s => s + 1)}
|
||||||
className={classNames('btn', {
|
className={clsx('btn', {
|
||||||
'btn-disabled': !hasNext || !state.next,
|
'btn-disabled': !hasNext || !state.next,
|
||||||
})}
|
})}
|
||||||
>{t('下一步')}</button>
|
>{t('下一步')}</button>
|
||||||
|
|
@ -49,7 +51,11 @@ const LoginNintendoAccount: React.FC<{ onChange: (v: StepState) => void }> = ({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <div className='my-3'>
|
return <div className='my-3'>
|
||||||
<button className='btn' onClick={() => onChange({ next: true, prev: true })}>{t('点击登录')}</button>
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn'
|
||||||
|
onClick={() => onChange({ next: true, prev: true })}
|
||||||
|
>{t('点击登录')}</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,32 @@
|
||||||
import { OpenSplatnet } from 'components/OpenSplatnet';
|
import { OpenSplatnet } from 'components/OpenSplatnet';
|
||||||
import { LogPanel, RunPanel } from 'components/RunPanel';
|
import { LogPanel, RunPanel } from 'components/RunPanel';
|
||||||
import { STAT_INK } from 'constant';
|
import { STAT_INK } from 'constant';
|
||||||
import React from 'react'
|
import React, { Suspense } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { FallbackComponent } from 'components/ErrorContent';
|
||||||
|
import { Loading } from 'components/Loading';
|
||||||
|
|
||||||
export const Home: React.FC = () => {
|
export const Home: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <div className='flex p-2 w-full h-full gap-2'>
|
return <ErrorBoundary FallbackComponent={FallbackComponent}>
|
||||||
<div className='max-w-full h-full md:max-w-sm flex-auto'>
|
<Suspense fallback={<Loading />}>
|
||||||
<div className='flex flex-col gap-2 h-full'>
|
<div className='flex p-2 w-full h-full gap-2'>
|
||||||
<LogPanel className='sm:hidden flex-auto' />
|
<div className='max-w-full h-full md:max-w-sm flex-auto'>
|
||||||
<RunPanel />
|
<div className='flex flex-col gap-2 h-full'>
|
||||||
<Link to='/settings' className='btn'>{t('设置')}</Link>
|
<LogPanel className='sm:hidden flex-auto' />
|
||||||
<div className='flex gap-2 flex-auto-all'>
|
<RunPanel />
|
||||||
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet>
|
<Link to='/settings' className='btn'>{t('设置')}</Link>
|
||||||
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
<div className='flex gap-2 flex-auto-all'>
|
||||||
|
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet>
|
||||||
|
<a className='btn w-full' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<LogPanel className='hidden sm:block flex-1' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Suspense>
|
||||||
<LogPanel className='hidden sm:block flex-1' />
|
</ErrorBoundary>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { Loading } from 'components/Loading';
|
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useLocation } from 'react-use';
|
|
||||||
|
|
||||||
export const RedirectLogin: React.FC = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const state = useLocation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const search = state.search ?? '';
|
|
||||||
|
|
||||||
const index = search.indexOf('url=');
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = decodeURIComponent(search.substring(index + 4));
|
|
||||||
|
|
||||||
window.location.href = url;
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
return <div className='h-full flex justify-center items-center'>
|
|
||||||
<span className='flex justify-center items-center gap-1'><Loading className='align-middle' />{t('正在跳转到登录页面...')}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { ErrorContent } from 'components/ErrorContent';
|
import { ErrorContent, FallbackComponent } from 'components/ErrorContent';
|
||||||
import { Loading } from 'components/Loading';
|
import { Loading } from 'components/Loading';
|
||||||
import { usePromise, usePromiseLazy } from 'hooks/usePromise';
|
import React, { Suspense, useState } from 'react'
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Config, getConfig, getProfile, Profile, setConfig, setProfile } from 'services/config';
|
import { Config, Profile } from 'services/config';
|
||||||
import { composeLoadable } from 'utils/composeLoadable';
|
import clsx from 'clsx';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useLogin } from 'services/s3si';
|
import { useLogin } from 'services/s3si';
|
||||||
import { STAT_INK } from 'constant';
|
import { STAT_INK } from 'constant';
|
||||||
import { Header } from 'components/Header';
|
import { Header } from 'components/Header';
|
||||||
import { useSubField } from 'hooks/useSubField';
|
import { useSubField } from 'hooks/useSubField';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useSWRMutation from 'swr/mutation'
|
||||||
|
import { useService, useServiceMutation } from 'services/useService';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
const STAT_INK_KEY_LENGTH = 43;
|
const STAT_INK_KEY_LENGTH = 43;
|
||||||
|
|
||||||
|
|
@ -57,6 +58,8 @@ const Form: React.FC<{
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const [value, setValue] = useState(oldValue);
|
const [value, setValue] = useState(oldValue);
|
||||||
const { subField } = useSubField({ value, onChange: setValue });
|
const { subField } = useSubField({ value, onChange: setValue });
|
||||||
|
const { trigger: setProfile } = useServiceMutation('profile', 0)
|
||||||
|
const { trigger: setConfig } = useServiceMutation('config')
|
||||||
|
|
||||||
const changed = JSON.stringify(value) !== JSON.stringify(oldValue);
|
const changed = JSON.stringify(value) !== JSON.stringify(oldValue);
|
||||||
|
|
||||||
|
|
@ -64,12 +67,12 @@ const Form: React.FC<{
|
||||||
const statInkApiKey = subField('profile.state.statInkApiKey')
|
const statInkApiKey = subField('profile.state.statInkApiKey')
|
||||||
const splatnet3Lang = subField('profile.state.userLang')
|
const splatnet3Lang = subField('profile.state.userLang')
|
||||||
|
|
||||||
const [onSave, { loading, error }] = usePromiseLazy(async () => {
|
const { trigger: onSave, isMutating: loading, error } = useSWRMutation('saveSettings', async () => {
|
||||||
await setProfile(0, value.profile);
|
await setProfile(value.profile);
|
||||||
await setConfig(value.config);
|
await setConfig(value.config);
|
||||||
onSaved?.();
|
onSaved?.();
|
||||||
})
|
})
|
||||||
const [onLogin, loginState] = usePromiseLazy(async () => {
|
const loginState = useSWRMutation('login', async () => {
|
||||||
const result = await login();
|
const result = await login();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -85,11 +88,12 @@ const Form: React.FC<{
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text">{t('Nintendo Account 会话令牌')}</span>
|
<span className="label-text">{t('Nintendo Account 会话令牌')}</span>
|
||||||
<span className="label-text-alt"><button
|
<span className="label-text-alt"><button
|
||||||
className={classNames('link', {
|
type='button'
|
||||||
loading: loginState.loading,
|
className={clsx('link', {
|
||||||
|
loading: loginState.isMutating,
|
||||||
})}
|
})}
|
||||||
onClick={onLogin}
|
onClick={() => loginState.trigger()}
|
||||||
disabled={loginState.loading}
|
disabled={loginState.isMutating}
|
||||||
>{t('网页登录')}</button></span>
|
>{t('网页登录')}</button></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -113,7 +117,7 @@ const Form: React.FC<{
|
||||||
</label>
|
</label>
|
||||||
<div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}>
|
<div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}>
|
||||||
<input
|
<input
|
||||||
className={classNames("input input-bordered w-full", {
|
className={clsx("input input-bordered w-full", {
|
||||||
'input-error': statInkKeyError,
|
'input-error': statInkKeyError,
|
||||||
})}
|
})}
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -148,38 +152,49 @@ const Form: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
<ErrorContent error={error} />
|
<ErrorContent error={error} />
|
||||||
<div className='flex gap-4 max-w-md justify-between flex-auto-all'>
|
<div className='flex gap-4 max-w-md justify-between flex-auto-all'>
|
||||||
<div className="tooltip" data-tip={changed ? undefined : t('没有更改')}>
|
<div className='tooltip' data-tip={changed ? undefined : t('没有更改')}>
|
||||||
<button className={classNames('btn btn-primary w-full', {
|
<button
|
||||||
loading,
|
type='button'
|
||||||
})} onClick={onSave} disabled={!changed || statInkKeyError}>{t('保存')}</button>
|
className={clsx('btn btn-primary w-full', {
|
||||||
|
loading,
|
||||||
|
})}
|
||||||
|
onClick={() => onSave()}
|
||||||
|
disabled={!changed || statInkKeyError}
|
||||||
|
>{t('保存')}</button>
|
||||||
</div>
|
</div>
|
||||||
<button className={classNames('btn', {
|
<button
|
||||||
loading,
|
type='button'
|
||||||
})} onClick={() => setValue(oldValue)}>{t('重置')}</button>
|
className={clsx('btn', {
|
||||||
|
loading,
|
||||||
|
})}
|
||||||
|
onClick={() => setValue(oldValue)}
|
||||||
|
>{t('重置')}</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Settings: React.FC = () => {
|
const SettingsLoader: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
let { loading, error, retry, result } = composeLoadable({
|
const { data: config } = useService('config')
|
||||||
config: usePromise(getConfig),
|
const { data: profile } = useService('profile', 0)
|
||||||
profile: usePromise(() => getProfile(0)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) {
|
if (!config || !profile) {
|
||||||
return <Page>
|
return <>
|
||||||
<div className='h-full flex items-center justify-center'><Loading /></div>
|
Error
|
||||||
</Page>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
return <>
|
||||||
return <Page>
|
<Form oldValue={{ config, profile }} onSaved={() => navigate(-1)} />
|
||||||
<ErrorContent error={error} retry={retry} />
|
</>
|
||||||
</Page>
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
export const Settings: React.FC = () => {
|
||||||
return <Page>
|
return <Page>
|
||||||
{result && <Form oldValue={result} onSaved={() => navigate(-1)} />}
|
<ErrorBoundary FallbackComponent={FallbackComponent}>
|
||||||
|
<Suspense fallback={<div className='h-full flex items-center justify-center'><Loading /></div>}>
|
||||||
|
<SettingsLoader />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
</Page>
|
</Page>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { fs } from "@tauri-apps/api"
|
import { fs } from "@tauri-apps/api"
|
||||||
import { appConfigDir, join } from '@tauri-apps/api/path'
|
import { appConfigDir, join } from '@tauri-apps/api/path'
|
||||||
import { State } from '../../../src/state';
|
import type { State } from '../../../src/state';
|
||||||
|
|
||||||
const configFile = appConfigDir().then(c => join(c, 'config.json'));
|
const configFile = appConfigDir().then(c => join(c, 'config.json'));
|
||||||
const profileDir = appConfigDir().then(c => join(c, 'profile'));
|
const profileDir = appConfigDir().then(c => join(c, 'profile'));
|
||||||
|
|
@ -9,8 +9,7 @@ export type Profile = {
|
||||||
state: State,
|
state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Config = {
|
export type Config = Record<string, never>
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: import from state.ts.
|
// TODO: import from state.ts.
|
||||||
const DEFAULT_STATE: State = {
|
const DEFAULT_STATE: State = {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const client = new JSONRPCClient<S3SIService>({
|
||||||
const LOG_SUB = new Set<(logs: Log[]) => void>();
|
const LOG_SUB = new Set<(logs: Log[]) => void>();
|
||||||
|
|
||||||
async function getLogs() {
|
async function getLogs() {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
const r = await client.getLogs()
|
const r = await client.getLogs()
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ export const useLog = () => {
|
||||||
return useContext(LOG_CONTEXT);
|
return useContext(LOG_CONTEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMsg(i: any) {
|
function renderMsg(i: unknown) {
|
||||||
if (i instanceof Error) {
|
if (i instanceof Error) {
|
||||||
return i.message
|
return i.message
|
||||||
}
|
}
|
||||||
|
|
@ -91,14 +92,15 @@ export const LogProvider: React.FC<{ limit?: number, children?: React.ReactNode
|
||||||
LOG_SUB.delete(cb);
|
LOG_SUB.delete(cb);
|
||||||
}
|
}
|
||||||
}, [limit])
|
}, [limit])
|
||||||
|
const value = useMemo(() => {
|
||||||
|
const renderedLogs = logs.map(renderLog)
|
||||||
|
return {
|
||||||
|
logs,
|
||||||
|
renderedLogs,
|
||||||
|
}
|
||||||
|
}, [logs])
|
||||||
|
|
||||||
|
return <LOG_CONTEXT.Provider value={value}>
|
||||||
const renderedLogs = useMemo(() => logs.map(renderLog), [logs])
|
|
||||||
|
|
||||||
return <LOG_CONTEXT.Provider value={{
|
|
||||||
logs,
|
|
||||||
renderedLogs,
|
|
||||||
}}>
|
|
||||||
{children}
|
{children}
|
||||||
</LOG_CONTEXT.Provider>
|
</LOG_CONTEXT.Provider>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import useSWR, { Key, SWRResponse } from 'swr'
|
||||||
|
import useSWRMutation, { SWRMutationResponse } from 'swr/mutation'
|
||||||
|
import { getConfig, getProfile, setConfig, setProfile } from './config'
|
||||||
|
|
||||||
|
const SERVICES = {
|
||||||
|
profile: {
|
||||||
|
fetcher: getProfile,
|
||||||
|
updater: setProfile,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
fetcher: getConfig,
|
||||||
|
updater: setConfig,
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type Services = keyof typeof SERVICES
|
||||||
|
|
||||||
|
export const useService = <S extends Services>(service: S, ...args: Parameters<(typeof SERVICES)[S]['fetcher']>): SWRResponse<
|
||||||
|
Awaited<ReturnType<(typeof SERVICES)[S]['fetcher']>>
|
||||||
|
> => {
|
||||||
|
// @ts-expect-error TypeScript can not infer type here
|
||||||
|
return useSWR(['service', service, ...args], () => SERVICES[service].fetcher(...args), { suspense: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type RemoveLastParamters<T extends (...args: any) => any> = T extends (...args: [...infer P, any]) => any ? P : never;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type LastParamter<T extends (...args: any) => any> = T extends (...args: [...infer _, infer P]) => any ? P : never;
|
||||||
|
export const useServiceMutation = <S extends Services>(service: S, ...args: RemoveLastParamters<(typeof SERVICES)[S]['updater']>): SWRMutationResponse<
|
||||||
|
Awaited<ReturnType<(typeof SERVICES)[S]['updater']>>,
|
||||||
|
Error,
|
||||||
|
Key,
|
||||||
|
LastParamter<(typeof SERVICES)[S]['updater']>
|
||||||
|
> => {
|
||||||
|
// @ts-expect-error TypeScript can not infer type here
|
||||||
|
return useSWRMutation(['service', service, ...args], (_, { arg }) => SERVICES[service].updater(...args, arg))
|
||||||
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
export type Loadable<T> = {
|
|
||||||
loading: boolean;
|
|
||||||
result?: T;
|
|
||||||
error?: any;
|
|
||||||
retry?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function composeLoadable<T extends Record<string, Loadable<any>>>(map: T): Loadable<{
|
|
||||||
[P in keyof T]: T[P] extends Loadable<infer R> ? R : never
|
|
||||||
}> {
|
|
||||||
const values = Object.values(map)
|
|
||||||
|
|
||||||
const loading = values.some(v => v.loading);
|
|
||||||
const error = values.find(v => v.error)?.error;
|
|
||||||
const result = loading || error ? undefined : Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.result])) as any;
|
|
||||||
const retry = values.some(i => !!i.retry) ? () => Object.values(map).forEach(v => v.retry?.()) : undefined;
|
|
||||||
|
|
||||||
return { loading, result, error, retry };
|
|
||||||
}
|
|
||||||
21
s3si.ts
21
s3si.ts
|
|
@ -4,7 +4,13 @@ import { flags } from "./deps.ts";
|
||||||
|
|
||||||
const parseArgs = (args: string[]) => {
|
const parseArgs = (args: string[]) => {
|
||||||
const parsed = flags.parse(args, {
|
const parsed = flags.parse(args, {
|
||||||
string: ["profilePath", "exporter", "skipMode"],
|
string: [
|
||||||
|
"profilePath",
|
||||||
|
"exporter",
|
||||||
|
"skipMode",
|
||||||
|
"listMethod",
|
||||||
|
"nxapiPresenceUrl",
|
||||||
|
],
|
||||||
boolean: ["help", "noProgress", "monitor", "withSummary"],
|
boolean: ["help", "noProgress", "monitor", "withSummary"],
|
||||||
alias: {
|
alias: {
|
||||||
"help": "h",
|
"help": "h",
|
||||||
|
|
@ -14,7 +20,8 @@ const parseArgs = (args: string[]) => {
|
||||||
"monitor": ["m"],
|
"monitor": ["m"],
|
||||||
"skipMode": ["s", "skip-mode"],
|
"skipMode": ["s", "skip-mode"],
|
||||||
"withSummary": "with-summary",
|
"withSummary": "with-summary",
|
||||||
"withStages": "with-stages",
|
"listMethod": "list-method",
|
||||||
|
"nxapiPresenceUrl": ["nxapi-presence"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return parsed;
|
return parsed;
|
||||||
|
|
@ -29,14 +36,18 @@ Options:
|
||||||
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
||||||
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
|
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
|
||||||
Multiple exporters can be separated by commas
|
Multiple exporters can be separated by commas
|
||||||
(e.g. "stat.ink,file,mongodb")
|
(e.g. "stat.ink,file,splashcat")
|
||||||
|
--list-method When set to "latest", the latest 50 matches will be obtained.
|
||||||
|
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
|
||||||
|
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
|
||||||
|
"auto" is the default setting.
|
||||||
--no-progress, -n Disable progress bar
|
--no-progress, -n Disable progress bar
|
||||||
--monitor, -m Monitor mode
|
--monitor, -m Monitor mode
|
||||||
--skip-mode <mode>, -s Skip mode (default: null)
|
--skip-mode <mode>, -s Skip mode (default: null)
|
||||||
("vs", "coop")
|
("vs", "coop")
|
||||||
--with-summary Include summary in the output
|
--with-summary Include summary in the output
|
||||||
--with-stages Include stage records in the output
|
--help Show this help message and exit
|
||||||
--help Show this help message and exit`,
|
--nxapi-presence Extends monitoring mode to use Nintendo Switch presence from nxapi`,
|
||||||
);
|
);
|
||||||
Deno.exit(0);
|
Deno.exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import * as path from "https://deno.land/std@0.178.0/path/mod.ts";
|
import * as path from "https://deno.land/std@0.213.0/path/mod.ts";
|
||||||
import { assertEquals } from "../dev_deps.ts";
|
import { assertEquals } from "../dev_deps.ts";
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
|
|
@ -39,6 +39,8 @@ if (import.meta.main) {
|
||||||
target,
|
target,
|
||||||
"code:",
|
"code:",
|
||||||
status.code,
|
status.code,
|
||||||
|
"stderr:",
|
||||||
|
new TextDecoder().decode(status.stderr),
|
||||||
);
|
);
|
||||||
Deno.exit(status.code);
|
Deno.exit(status.code);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import { MongoDB } from "../deps.ts";
|
|
||||||
import { DEFAULT_ENV } from "../src/env.ts";
|
|
||||||
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
|
|
||||||
import { FileStateBackend, Profile } from "../src/state.ts";
|
|
||||||
|
|
||||||
const OLD_BATTLES_END_DATE = new Date("2023-02-28T03:42:47.000+00:00");
|
|
||||||
|
|
||||||
const env = DEFAULT_ENV;
|
|
||||||
const stateBackend = new FileStateBackend("./profile.json");
|
|
||||||
const profile = new Profile({ stateBackend, env });
|
|
||||||
await profile.readState();
|
|
||||||
|
|
||||||
if (!profile.state.mongoDbUri) {
|
|
||||||
console.error("MongoDB URI not set");
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
|
|
||||||
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
|
|
||||||
|
|
||||||
const filter = {
|
|
||||||
"exportDate": {
|
|
||||||
"$lte": OLD_BATTLES_END_DATE,
|
|
||||||
},
|
|
||||||
"gameId": undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cursor = battlesCollection.find(filter);
|
|
||||||
|
|
||||||
const oldDocuments = await battlesCollection.countDocuments(filter);
|
|
||||||
|
|
||||||
console.log(`Found ${oldDocuments} old battles to update...`);
|
|
||||||
|
|
||||||
for await (const doc of cursor) {
|
|
||||||
const { splatNetData, _id } = doc;
|
|
||||||
|
|
||||||
const splatNetId = splatNetData.id;
|
|
||||||
const uniqueId = MongoDBExporter.getGameId(splatNetId);
|
|
||||||
|
|
||||||
await battlesCollection.updateOne({ _id }, {
|
|
||||||
"$set": {
|
|
||||||
gameId: uniqueId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Updated ${splatNetId} to ${uniqueId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Done!");
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
{
|
|
||||||
"version": "2",
|
|
||||||
"remote": {
|
|
||||||
"https://deno.land/std@0.141.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
|
||||||
"https://deno.land/std@0.141.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
|
|
||||||
"https://deno.land/std@0.141.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
|
|
||||||
"https://deno.land/std@0.141.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
|
|
||||||
"https://deno.land/std@0.141.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
|
|
||||||
"https://deno.land/std@0.141.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
|
|
||||||
"https://deno.land/std@0.141.0/io/types.d.ts": "01f60ae7ec02675b5dbed150d258fc184a78dfe5c209ef53ba4422b46b58822c",
|
|
||||||
"https://deno.land/std@0.141.0/streams/conversion.ts": "8268f3f1a43324953dd8e9e4e31adb42e3caddb4502433bde03c279e43d70a3b",
|
|
||||||
"https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
|
||||||
"https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934",
|
|
||||||
"https://deno.land/std@0.160.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4",
|
|
||||||
"https://deno.land/std@0.160.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a",
|
|
||||||
"https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179",
|
|
||||||
"https://deno.land/std@0.160.0/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2",
|
|
||||||
"https://deno.land/std@0.160.0/flags/mod.ts": "686b6b36e14b00f11c9e26cecf439021158436a6e34f60eeb0d927f0b169ae20",
|
|
||||||
"https://deno.land/std@0.160.0/fmt/colors.ts": "9e36a716611dcd2e4865adea9c4bec916b5c60caad4cdcdc630d4974e6bb8bd4",
|
|
||||||
"https://deno.land/std@0.160.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289",
|
|
||||||
"https://deno.land/std@0.160.0/io/mod.ts": "6e781ebafd5cdccf9ab4afa1f499b08c513602d023cb08ceebc58758501f78bd",
|
|
||||||
"https://deno.land/std@0.160.0/io/readers.ts": "45847ad404afd2f605eae1cff193f223462bc55eeb9ae313c2f3db28aada0fd6",
|
|
||||||
"https://deno.land/std@0.160.0/io/types.d.ts": "107e1e64834c5ba917c783f446b407d33432c5d612c4b3430df64fc2b4ecf091",
|
|
||||||
"https://deno.land/std@0.160.0/io/util.ts": "23e706b4b6a3ebb34af00ad74d7549d906f3211fc98c1fba1185a36e017fb727",
|
|
||||||
"https://deno.land/std@0.160.0/io/writers.ts": "2e1c63ffd0cfba411b1fd8374609abff9ea86187c9d4d885d42e6fc20325ef0e",
|
|
||||||
"https://deno.land/std@0.160.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
|
|
||||||
"https://deno.land/std@0.160.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
|
|
||||||
"https://deno.land/std@0.160.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677",
|
|
||||||
"https://deno.land/std@0.160.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
|
|
||||||
"https://deno.land/std@0.160.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
|
|
||||||
"https://deno.land/std@0.160.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac",
|
|
||||||
"https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24",
|
|
||||||
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
|
|
||||||
"https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
|
|
||||||
"https://deno.land/std@0.160.0/streams/conversion.ts": "328afbedee0a7e0c330ac4c7b4c1af569ee53974f970230f6a78f545b93abb9b",
|
|
||||||
"https://deno.land/std@0.160.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c",
|
|
||||||
"https://deno.land/std@0.160.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
|
|
||||||
"https://deno.land/std@0.160.0/testing/asserts.ts": "1e340c589853e82e0807629ba31a43c84ebdcdeca910c4a9705715dfdb0f5ce8",
|
|
||||||
"https://deno.land/std@0.160.0/uuid/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
|
|
||||||
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5",
|
|
||||||
"https://deno.land/std@0.160.0/uuid/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
|
|
||||||
"https://deno.land/std@0.160.0/uuid/v4.ts": "3e983c6ac895ea2a7ba03da927a2438fe1c26ac43fb38dc44f2f8aa50c23cb53",
|
|
||||||
"https://deno.land/std@0.160.0/uuid/v5.ts": "43973aeda44ad212f2ec9b8d6c042b74d5cef4ce583d6aa6fc4cdb339344c74c",
|
|
||||||
"https://deno.land/std@0.178.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
|
|
||||||
"https://deno.land/std@0.178.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
|
|
||||||
"https://deno.land/std@0.178.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
|
|
||||||
"https://deno.land/std@0.178.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
|
|
||||||
"https://deno.land/std@0.178.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
|
|
||||||
"https://deno.land/std@0.178.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
|
|
||||||
"https://deno.land/std@0.178.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
|
|
||||||
"https://deno.land/std@0.178.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232",
|
|
||||||
"https://deno.land/std@0.178.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
|
|
||||||
"https://deno.land/std@0.178.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
|
|
||||||
"https://deno.land/std@0.178.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
|
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/cookie.ts": "72d6a6633ea13dd2f13b53d9726735b194996353a958024072c4d6b077c97baf",
|
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/cookie_jar.ts": "9accd36e76929f2f06fa710d2165fb544703617245fa36ac63560b9fa2a22a25",
|
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/fetch_wrapper.ts": "d8918c0776413b2d4a675415727973390b4401a026f6dfdcffedce3296b5e0dc",
|
|
||||||
"https://deno.land/x/another_cookiejar@v4.1.4/mod.ts": "eff949014965771f2cd447fe78625a1ad28b59333afa40640f02c0922534d89a",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/CachedKeyDecoder.ts": "c39b6f1572902ae08c0e4971f639e81031ac59403957fc43c6fb3c7fe69d99a1",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/Decoder.ts": "bdb68309cd51da2b9a897f269784c6d636796258838a97f25b0e1b399c6f369b",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/Encoder.ts": "4852bbacb30cd66eb2bd61a9e20476802458b991e13aacb5eb984d0348247ffe",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/ExtData.ts": "8d97fe43568e119a1eeb93e1ef1c431e0a24e392fb0c6ffed775aac1e579f244",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/ExtensionCodec.ts": "e8a24eb1786156239f589cc3058c8ff3d79ed393f420c40fdf7a93df943c91f2",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/context.ts": "6228de10854dbadf6aef096960af0115214078ec3784eca4565587769fde3d1c",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/decode.ts": "c808aeec46f6d0e5b28d0bbacd40e78d0a3614b229368c70db2e53c03f7555ca",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/decodeAsync.ts": "19e4f33ba0cc8d200b857deb9721bace863c0e89f7bff73e2b04379e4ee85bad",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/encode.ts": "c5598f8eec9efcbd0ef07f246ade049a8f4906703cdb601baf03b2774b293916",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/mod.ts": "c28290db26b1ea027e1798085fd6c8055685ea086f1418d54a33542b285633c9",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/timestamp.ts": "5169949efe39bc24f58cd5dcaae682cdf5353c762a54abf9ae6e18c8d9feb648",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/int.ts": "b08743982f954d2dd7f4f11d868019576b63cb8147d8acc1bce3843f39398188",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/prettyByte.ts": "35c8104d57ba2a727056beaf1063bbe941d512cdd23ce6b04d7c5b44dafcd46e",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/stream.ts": "1315e29af5c1a40d97bfa6f1c4f7f73d26067b912236f56851981f2f049500b8",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/typedArrays.ts": "bb819c2f28cf7f85ed50b2e57f108462715555cc61ce315e8134cf1eef2ae662",
|
|
||||||
"https://deno.land/x/msgpack@v1.4/utils/utf8.ts": "93183055a7a41986080eeb711e83d553e7c8b121642da4379a5adf253b7beefd",
|
|
||||||
"https://deno.land/x/murmurhash@v1.0.0/mod.ts": "13fd2c5534dfd22ffbfcd4255ea13e47a2f2b99e9c90a83dc43e814a0e278829",
|
|
||||||
"https://deno.land/x/progress@v1.2.8/deps.ts": "e0abdc972a0c152508b28ced5ae9c4be26a5773f0aa4a3caa72371c84d2e28a2",
|
|
||||||
"https://deno.land/x/progress@v1.2.8/mod.ts": "5ef7c7ff079d71effed5055666af81cc58a566bc98e2df8473526bd6457976c5",
|
|
||||||
"https://deno.land/x/progress@v1.2.8/multi.ts": "392553552243204539d83ee53cadda990db20b1b421520411318ff9bd0320646",
|
|
||||||
"https://deno.land/x/semaphore@v1.1.1/mod.ts": "431abb51927a16c537cec1cfb05bf2de6a8f3916331f1ec3f9f13ad7ad6a4ea5",
|
|
||||||
"https://deno.land/x/semaphore@v1.1.1/mutex.ts": "2cc6490481f0fdfe97c6b326a2073819d76b76eac3877864a8ada6a2127492f2",
|
|
||||||
"https://deno.land/x/semaphore@v1.1.1/semaphore.ts": "0acf1159d635fa3b9198a4ad4acac9e877d79196601aa80544ac0db5a71c646d",
|
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/functions.ts": "20681c98ce82d503dba56f5ef9313c196f18a2317ce7c0c331cc3fdea0d56688",
|
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/literal-types/mod.ts": "c1b9e16a7e49814e9509bed8a5dec25b717761a37d0ef1589d411bd6130dd2e5",
|
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b",
|
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603",
|
|
||||||
"https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345"
|
|
||||||
},
|
|
||||||
"npm": {
|
|
||||||
"specifiers": {
|
|
||||||
"mongodb": "mongodb@5.1.0",
|
|
||||||
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"@types/node@18.14.2": {
|
|
||||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"@types/webidl-conversions@7.0.0": {
|
|
||||||
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"@types/whatwg-url@8.2.2": {
|
|
||||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "@types/node@18.14.2",
|
|
||||||
"@types/webidl-conversions": "@types/webidl-conversions@7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bson@5.0.1": {
|
|
||||||
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"ip@2.0.0": {
|
|
||||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"memory-pager@1.5.0": {
|
|
||||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"mongodb-connection-string-url@2.6.0": {
|
|
||||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/whatwg-url": "@types/whatwg-url@8.2.2",
|
|
||||||
"whatwg-url": "whatwg-url@11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mongodb@5.1.0": {
|
|
||||||
"integrity": "sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw==",
|
|
||||||
"dependencies": {
|
|
||||||
"bson": "bson@5.0.1",
|
|
||||||
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
|
|
||||||
"saslprep": "saslprep@1.0.3",
|
|
||||||
"socks": "socks@2.7.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"punycode@2.3.0": {
|
|
||||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"saslprep@1.0.3": {
|
|
||||||
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
|
||||||
"dependencies": {
|
|
||||||
"sparse-bitfield": "sparse-bitfield@3.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"smart-buffer@4.2.0": {
|
|
||||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"socks@2.7.1": {
|
|
||||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ip": "ip@2.0.0",
|
|
||||||
"smart-buffer": "smart-buffer@4.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sparse-bitfield@3.0.3": {
|
|
||||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"memory-pager": "memory-pager@1.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"splatnet3-types@0.2.20230227204004": {
|
|
||||||
"integrity": "sha512-FAY6pbUcrp5O8c49BNXSKxoyM3UlCrRx2AtA9Y3qlvqOLdHqwxtzcdzbk1b1hRam8ZcrxRzE/ii6ESRiPIAnZw==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"tr46@3.0.0": {
|
|
||||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "punycode@2.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webidl-conversions@7.0.0": {
|
|
||||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"whatwg-url@11.0.0": {
|
|
||||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "tr46@3.0.0",
|
|
||||||
"webidl-conversions": "webidl-conversions@7.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,7 @@ function encryptKey(uid: string) {
|
||||||
hasher.hash(uid);
|
hasher.hash(uid);
|
||||||
const hash = hasher.result();
|
const hash = hasher.result();
|
||||||
const key = hash & 0xff;
|
const key = hash & 0xff;
|
||||||
const encrypted = base64.encode(
|
const encrypted = base64.encodeBase64(
|
||||||
new TextEncoder().encode(uid).map((i) => i ^ key),
|
new TextEncoder().encode(uid).map((i) => i ^ key),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ for (const file of files) {
|
||||||
const content: FileExporterType = JSON.parse(await Deno.readTextFile(file));
|
const content: FileExporterType = JSON.parse(await Deno.readTextFile(file));
|
||||||
if (content.type === "SUMMARY") continue;
|
if (content.type === "SUMMARY") continue;
|
||||||
const id = content.data.detail.id;
|
const id = content.data.detail.id;
|
||||||
const rawId = base64.decode(id);
|
const rawId = base64.decodeBase64(id);
|
||||||
const uuid = new TextDecoder().decode(rawId.slice(rawId.length - 36));
|
const uuid = new TextDecoder().decode(rawId.slice(rawId.length - 36));
|
||||||
if (ids.has(uuid)) {
|
if (ids.has(uuid)) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import { MongoDB } from "../deps.ts";
|
|
||||||
import { DEFAULT_ENV } from "../src/env.ts";
|
|
||||||
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
|
|
||||||
import { FileStateBackend, Profile } from "../src/state.ts";
|
|
||||||
|
|
||||||
const env = DEFAULT_ENV;
|
|
||||||
const stateBackend = new FileStateBackend("./profile.json");
|
|
||||||
const profile = new Profile({ stateBackend, env });
|
|
||||||
await profile.readState();
|
|
||||||
|
|
||||||
if (!profile.state.mongoDbUri) {
|
|
||||||
console.error("MongoDB URI not set");
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
|
|
||||||
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
|
|
||||||
|
|
||||||
const filter = {
|
|
||||||
"splatNetData.playedTime": {
|
|
||||||
$type: "string",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const cursor = battlesCollection.find(filter);
|
|
||||||
|
|
||||||
const oldDocuments = await battlesCollection.countDocuments(filter);
|
|
||||||
|
|
||||||
console.log(`Found ${oldDocuments} old battles to update...`);
|
|
||||||
|
|
||||||
for await (const doc of cursor) {
|
|
||||||
const { splatNetData, _id } = doc;
|
|
||||||
|
|
||||||
await battlesCollection.updateOne({ _id }, {
|
|
||||||
"$set": {
|
|
||||||
"splatNetData.playedTime": new Date(splatNetData.playedTime),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Updated ${splatNetData.playedTime} to ${new Date(
|
|
||||||
splatNetData.playedTime,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Done!");
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import { MongoDB } from "../deps.ts";
|
|
||||||
import { DEFAULT_ENV } from "../src/env.ts";
|
|
||||||
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
|
|
||||||
import { FileStateBackend, Profile } from "../src/state.ts";
|
|
||||||
|
|
||||||
const env = DEFAULT_ENV;
|
|
||||||
const stateBackend = new FileStateBackend("./profile.json");
|
|
||||||
const profile = new Profile({ stateBackend, env });
|
|
||||||
await profile.readState();
|
|
||||||
|
|
||||||
if (!profile.state.mongoDbUri) {
|
|
||||||
console.error("MongoDB URI not set");
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
|
|
||||||
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
|
|
||||||
|
|
||||||
const cursor = battlesCollection.find();
|
|
||||||
|
|
||||||
const oldDocuments = await battlesCollection.countDocuments();
|
|
||||||
|
|
||||||
console.log(`Found ${oldDocuments} old battles to upload...`);
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
const erroredBattles = [];
|
|
||||||
|
|
||||||
for await (const doc of cursor) {
|
|
||||||
const { splatNetData, _id } = doc;
|
|
||||||
|
|
||||||
// start time for performance tracking, needs to be very accurate
|
|
||||||
const startTime = new Date();
|
|
||||||
|
|
||||||
splatNetData.playedTime = splatNetData.playedTime.toISOString();
|
|
||||||
|
|
||||||
const response = await fetch("http://127.0.0.1:8000/battles/api/upload/", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${profile.state.splashcatApiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
"data_type": "splatnet3",
|
|
||||||
"battle": splatNetData,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Failed to upload ${splatNetData.id}`);
|
|
||||||
erroredBattles.push({
|
|
||||||
id: doc.gameId,
|
|
||||||
error: await response.text(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// end time for performance tracking, needs to be very accurate
|
|
||||||
const endTime = new Date();
|
|
||||||
const timeTaken = endTime.getTime() - startTime.getTime();
|
|
||||||
|
|
||||||
console.log(`Uploaded ${splatNetData.id} (${timeTaken}ms)`);
|
|
||||||
count++;
|
|
||||||
console.log(`Uploaded ${count}/${oldDocuments} battles`)
|
|
||||||
|
|
||||||
if (count % 100 === 0) {
|
|
||||||
console.log("Updating error logs...");
|
|
||||||
if (erroredBattles.length > 0) {
|
|
||||||
await Deno.writeFile("./errored-battles.json", new TextEncoder().encode(JSON.stringify(erroredBattles, null, "\t")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Done!");
|
|
||||||
|
|
||||||
if (erroredBattles.length > 0) {
|
|
||||||
await Deno.writeFile("./errored-battles.json", new TextEncoder().encode(JSON.stringify(erroredBattles, null, 2)));
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,7 @@ class TestRankTracker extends RankTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
function genId(id: number, date = "20220101"): string {
|
function genId(id: number, date = "20220101"): string {
|
||||||
return base64.encode(
|
return base64.encodeBase64(
|
||||||
`VsHistoryDetail-asdf:asdf:${date}T${
|
`VsHistoryDetail-asdf:asdf:${date}T${
|
||||||
id.toString().padStart(6, "0")
|
id.toString().padStart(6, "0")
|
||||||
}_------------------------------------`,
|
}_------------------------------------`,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
HistoryGroups,
|
HistoryGroups,
|
||||||
RankParam,
|
RankParam,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
import { gameId, parseHistoryDetailId } from "./utils.ts";
|
import { battleTime, gameId } from "./utils.ts";
|
||||||
import { getSeason } from "./VersionData.ts";
|
import { getSeason } from "./VersionData.ts";
|
||||||
|
|
||||||
const splusParams = () => {
|
const splusParams = () => {
|
||||||
|
|
@ -193,17 +193,6 @@ function addRank(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const battleTime = (id: string) => {
|
|
||||||
const { timestamp } = parseHistoryDetailId(id);
|
|
||||||
|
|
||||||
const dateStr = timestamp.replace(
|
|
||||||
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
|
|
||||||
"$1-$2-$3T$4:$5:$6Z",
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Date(dateStr);
|
|
||||||
};
|
|
||||||
|
|
||||||
type FlattenItem = {
|
type FlattenItem = {
|
||||||
id: string;
|
id: string;
|
||||||
gameId: string;
|
gameId: string;
|
||||||
|
|
@ -358,6 +347,10 @@ export class RankTracker {
|
||||||
async updateState(
|
async updateState(
|
||||||
history: HistoryGroups<BattleListNode>["nodes"],
|
history: HistoryGroups<BattleListNode>["nodes"],
|
||||||
) {
|
) {
|
||||||
|
if (history.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// history order by time. 0 is the oldest.
|
// history order by time. 0 is the oldest.
|
||||||
const flatten: FlattenItem[] = await Promise.all(
|
const flatten: FlattenItem[] = await Promise.all(
|
||||||
history
|
history
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,66 @@ export const SEASONS: Season[] = [
|
||||||
start: new Date("2023-06-01T00:00:00+00:00"),
|
start: new Date("2023-06-01T00:00:00+00:00"),
|
||||||
end: new Date("2023-09-01T00:00:00+00:00"),
|
end: new Date("2023-09-01T00:00:00+00:00"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "season202309",
|
||||||
|
name: "Drizzle Season 2023",
|
||||||
|
start: new Date("2023-09-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2023-12-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202312",
|
||||||
|
name: "Chill Season 2023",
|
||||||
|
start: new Date("2023-12-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2024-03-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202403",
|
||||||
|
name: "Fresh Season 2024",
|
||||||
|
start: new Date("2024-03-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2024-06-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202406",
|
||||||
|
name: "Sizzle Season 2024",
|
||||||
|
start: new Date("2024-06-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2024-09-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202409",
|
||||||
|
name: "Drizzle Season 2024",
|
||||||
|
start: new Date("2024-09-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2024-12-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202412",
|
||||||
|
name: "Chill Season 2024",
|
||||||
|
start: new Date("2024-12-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2025-03-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202503",
|
||||||
|
name: "Fresh Season 2025",
|
||||||
|
start: new Date("2025-03-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2025-06-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202506",
|
||||||
|
name: "Sizzle Season 2025",
|
||||||
|
start: new Date("2025-06-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2025-09-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202509",
|
||||||
|
name: "Drizzle Season 2025",
|
||||||
|
start: new Date("2025-09-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2025-12-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season202512",
|
||||||
|
name: "Chill Season 2025",
|
||||||
|
start: new Date("2025-12-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2026-03-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getSeason = (date: Date): Season | undefined => {
|
export const getSeason = (date: Date): Season | undefined => {
|
||||||
|
|
|
||||||
265
src/app.ts
265
src/app.ts
|
|
@ -1,15 +1,17 @@
|
||||||
import { loginManually } from "./iksm.ts";
|
import { loginManually } from "./iksm.ts";
|
||||||
import { MultiProgressBar } from "../deps.ts";
|
import { MultiProgressBar, Mutex } from "../deps.ts";
|
||||||
import { FileStateBackend, Profile, StateBackend } from "./state.ts";
|
import { FileStateBackend, Profile, StateBackend } from "./state.ts";
|
||||||
import { Splatnet3 } from "./splatnet3.ts";
|
import { Splatnet3 } from "./splatnet3.ts";
|
||||||
import { BattleListType, Game, GameExporter } from "./types.ts";
|
import { BattleListType, Game, GameExporter, ListMethod } from "./types.ts";
|
||||||
import { Cache, FileCache } from "./cache.ts";
|
import { Cache, FileCache } from "./cache.ts";
|
||||||
import { StatInkExporter } from "./exporters/stat.ink.ts";
|
import { StatInkExporter } from "./exporters/stat.ink.ts";
|
||||||
import { FileExporter } from "./exporters/file.ts";
|
import { FileExporter } from "./exporters/file.ts";
|
||||||
import { delay, showError } from "./utils.ts";
|
import { delay, showError } from "./utils.ts";
|
||||||
import { GameFetcher } from "./GameFetcher.ts";
|
import { GameFetcher } from "./GameFetcher.ts";
|
||||||
import { DEFAULT_ENV, Env } from "./env.ts";
|
import { DEFAULT_ENV, Env } from "./env.ts";
|
||||||
import { MongoDBExporter } from "./exporters/mongodb.ts";
|
import { SplashcatExporter } from "./exporters/splashcat.ts";
|
||||||
|
import { SPLATOON3_TITLE_ID } from "./constant.ts";
|
||||||
|
import { USERAGENT } from "./constant.ts";
|
||||||
|
|
||||||
export type Opts = {
|
export type Opts = {
|
||||||
profilePath: string;
|
profilePath: string;
|
||||||
|
|
@ -17,11 +19,12 @@ export type Opts = {
|
||||||
noProgress: boolean;
|
noProgress: boolean;
|
||||||
monitor: boolean;
|
monitor: boolean;
|
||||||
withSummary: boolean;
|
withSummary: boolean;
|
||||||
withStages: boolean;
|
|
||||||
skipMode?: string;
|
skipMode?: string;
|
||||||
|
listMethod?: string;
|
||||||
cache?: Cache;
|
cache?: Cache;
|
||||||
stateBackend?: StateBackend;
|
stateBackend?: StateBackend;
|
||||||
env: Env;
|
env: Env;
|
||||||
|
nxapiPresenceUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_OPTS: Opts = {
|
export const DEFAULT_OPTS: Opts = {
|
||||||
|
|
@ -30,7 +33,7 @@ export const DEFAULT_OPTS: Opts = {
|
||||||
noProgress: false,
|
noProgress: false,
|
||||||
monitor: false,
|
monitor: false,
|
||||||
withSummary: false,
|
withSummary: false,
|
||||||
withStages: true,
|
listMethod: "auto",
|
||||||
env: DEFAULT_ENV,
|
env: DEFAULT_ENV,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,6 +58,103 @@ class StepProgress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GameListFetcher {
|
||||||
|
/**
|
||||||
|
* Return not exported game list.
|
||||||
|
* [0] is the latest game.
|
||||||
|
* @param exporter GameExporter
|
||||||
|
*/
|
||||||
|
fetch(exporter: GameExporter): Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BattleListFetcher implements GameListFetcher {
|
||||||
|
protected listMethod: ListMethod;
|
||||||
|
protected allBattleList?: string[];
|
||||||
|
protected latestBattleList?: string[];
|
||||||
|
protected allLock = new Mutex();
|
||||||
|
protected latestLock = new Mutex();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
listMethod: string,
|
||||||
|
protected splatnet: Splatnet3,
|
||||||
|
) {
|
||||||
|
if (listMethod === "all") {
|
||||||
|
this.listMethod = "all";
|
||||||
|
} else if (listMethod === "latest") {
|
||||||
|
this.listMethod = "latest";
|
||||||
|
} else {
|
||||||
|
this.listMethod = "auto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getAllBattleList() {
|
||||||
|
return this.allLock.use(async () => {
|
||||||
|
if (!this.allBattleList) {
|
||||||
|
this.allBattleList = await this.splatnet.getAllBattleList();
|
||||||
|
}
|
||||||
|
return this.allBattleList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getLatestBattleList() {
|
||||||
|
return this.latestLock.use(async () => {
|
||||||
|
if (!this.latestBattleList) {
|
||||||
|
this.latestBattleList = await this.splatnet.getBattleList();
|
||||||
|
}
|
||||||
|
return this.latestBattleList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async innerFetch(exporter: GameExporter) {
|
||||||
|
if (this.listMethod === "latest") {
|
||||||
|
return await exporter.notExported({
|
||||||
|
type: "VsInfo",
|
||||||
|
list: await this.getLatestBattleList(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.listMethod === "all") {
|
||||||
|
return await exporter.notExported({
|
||||||
|
type: "VsInfo",
|
||||||
|
list: await this.getAllBattleList(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.listMethod === "auto") {
|
||||||
|
const latestList = await exporter.notExported({
|
||||||
|
type: "VsInfo",
|
||||||
|
list: await this.getLatestBattleList(),
|
||||||
|
});
|
||||||
|
if (latestList.length === 50) {
|
||||||
|
return await exporter.notExported({
|
||||||
|
type: "VsInfo",
|
||||||
|
list: await this.getAllBattleList(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return latestList;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`Unknown listMethod: ${this.listMethod}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(exporter: GameExporter) {
|
||||||
|
return [...await this.innerFetch(exporter)].reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoopListFetcher implements GameListFetcher {
|
||||||
|
constructor(
|
||||||
|
protected splatnet: Splatnet3,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async fetch(exporter: GameExporter) {
|
||||||
|
return [
|
||||||
|
...await exporter.notExported({
|
||||||
|
type: "CoopInfo",
|
||||||
|
list: await this.splatnet.getBattleList(BattleListType.Coop),
|
||||||
|
}),
|
||||||
|
].reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function progress({ total, currentUrl, done }: StepProgress): Progress {
|
function progress({ total, currentUrl, done }: StepProgress): Progress {
|
||||||
return {
|
return {
|
||||||
total,
|
total,
|
||||||
|
|
@ -66,6 +166,7 @@ function progress({ total, currentUrl, done }: StepProgress): Progress {
|
||||||
export class App {
|
export class App {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
env: Env;
|
env: Env;
|
||||||
|
splatoon3PreviouslyActive = false;
|
||||||
|
|
||||||
constructor(public opts: Opts) {
|
constructor(public opts: Opts) {
|
||||||
const stateBackend = opts.stateBackend ??
|
const stateBackend = opts.stateBackend ??
|
||||||
|
|
@ -75,6 +176,12 @@ export class App {
|
||||||
env: opts.env,
|
env: opts.env,
|
||||||
});
|
});
|
||||||
this.env = opts.env;
|
this.env = opts.env;
|
||||||
|
|
||||||
|
if (
|
||||||
|
opts.listMethod && !["all", "auto", "latest"].includes(opts.listMethod)
|
||||||
|
) {
|
||||||
|
throw new TypeError(`Unknown listMethod: ${opts.listMethod}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkipMode(): ("vs" | "coop")[] {
|
getSkipMode(): ("vs" | "coop")[] {
|
||||||
|
|
@ -118,22 +225,26 @@ export class App {
|
||||||
out.push(new FileExporter(state.fileExportPath));
|
out.push(new FileExporter(state.fileExportPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exporters.includes("mongodb")) {
|
if (exporters.includes("splashcat")) {
|
||||||
if (!state.mongoDbUri) {
|
if (!state.splashcatApiKey) {
|
||||||
const uri = (await this.env.prompts.prompt(
|
const key = (await this.env.prompts.prompt(
|
||||||
"MongoDB URI is not set. Please enter below.",
|
"Splashcat API key is not set. Please enter below.",
|
||||||
)).trim();
|
)).trim();
|
||||||
if (!uri) {
|
if (!key) {
|
||||||
this.env.logger.error("MongoDB URI is required.");
|
this.env.logger.error("API key is required.");
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
await this.profile.writeState({
|
await this.profile.writeState({
|
||||||
...state,
|
...state,
|
||||||
mongoDbUri: uri,
|
splashcatApiKey: key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out.push(
|
out.push(
|
||||||
new MongoDBExporter(this.profile.state.mongoDbUri!),
|
new SplashcatExporter({
|
||||||
|
splashcatApiKey: this.profile.state.splashcatApiKey!,
|
||||||
|
uploadMode: this.opts.monitor ? "Monitoring" : "Manual",
|
||||||
|
env: this.env,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,11 +275,13 @@ export class App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const endBar = () => {
|
const end = () => bar?.end();
|
||||||
bar?.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
return { redraw, endBar };
|
return {
|
||||||
|
redraw,
|
||||||
|
end,
|
||||||
|
[Symbol.dispose]: end,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
private async exportOnce() {
|
private async exportOnce() {
|
||||||
const splatnet = new Splatnet3({ profile: this.profile, env: this.env });
|
const splatnet = new Splatnet3({ profile: this.profile, env: this.env });
|
||||||
|
|
@ -184,10 +297,12 @@ export class App {
|
||||||
if (skipMode.includes("vs") || exporters.length === 0) {
|
if (skipMode.includes("vs") || exporters.length === 0) {
|
||||||
this.env.logger.log("Skip exporting VS games.");
|
this.env.logger.log("Skip exporting VS games.");
|
||||||
} else {
|
} else {
|
||||||
this.env.logger.log("Fetching battle list...");
|
const gameListFetcher = new BattleListFetcher(
|
||||||
const gameList = await splatnet.getBattleList();
|
this.opts.listMethod ?? "auto",
|
||||||
|
splatnet,
|
||||||
|
);
|
||||||
|
|
||||||
const { redraw, endBar } = this.exporterProgress("Export vs games");
|
using bar1 = this.exporterProgress("Export vs games");
|
||||||
const fetcher = new GameFetcher({
|
const fetcher = new GameFetcher({
|
||||||
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
|
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
|
||||||
state: this.profile.state,
|
state: this.profile.state,
|
||||||
|
|
@ -204,10 +319,10 @@ export class App {
|
||||||
type: "VsInfo",
|
type: "VsInfo",
|
||||||
fetcher,
|
fetcher,
|
||||||
exporter: e,
|
exporter: e,
|
||||||
gameList,
|
gameListFetcher,
|
||||||
stepProgress: stats[e.name],
|
stepProgress: stats[e.name],
|
||||||
onStep: () => {
|
onStep: () => {
|
||||||
redraw(e.name, progress(stats[e.name]));
|
bar1.redraw(e.name, progress(stats[e.name]));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -218,7 +333,7 @@ export class App {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
endBar();
|
await bar1.end();
|
||||||
|
|
||||||
this.printStats(stats);
|
this.printStats(stats);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
|
@ -238,12 +353,9 @@ export class App {
|
||||||
if (skipMode.includes("coop") || exporters.length === 0) {
|
if (skipMode.includes("coop") || exporters.length === 0) {
|
||||||
this.env.logger.log("Skip exporting coop games.");
|
this.env.logger.log("Skip exporting coop games.");
|
||||||
} else {
|
} else {
|
||||||
this.env.logger.log("Fetching coop battle list...");
|
const gameListFetcher = new CoopListFetcher(splatnet);
|
||||||
const coopBattleList = await splatnet.getBattleList(
|
|
||||||
BattleListType.Coop,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { redraw, endBar } = this.exporterProgress("Export coop games");
|
using bar2 = this.exporterProgress("Export coop games");
|
||||||
const fetcher = new GameFetcher({
|
const fetcher = new GameFetcher({
|
||||||
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
|
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
|
||||||
state: this.profile.state,
|
state: this.profile.state,
|
||||||
|
|
@ -258,10 +370,10 @@ export class App {
|
||||||
type: "CoopInfo",
|
type: "CoopInfo",
|
||||||
fetcher,
|
fetcher,
|
||||||
exporter: e,
|
exporter: e,
|
||||||
gameList: coopBattleList,
|
gameListFetcher,
|
||||||
stepProgress: stats[e.name],
|
stepProgress: stats[e.name],
|
||||||
onStep: () => {
|
onStep: () => {
|
||||||
redraw(e.name, progress(stats[e.name]));
|
bar2.redraw(e.name, progress(stats[e.name]));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -272,7 +384,7 @@ export class App {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
endBar();
|
await bar2.end();
|
||||||
|
|
||||||
this.printStats(stats);
|
this.printStats(stats);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
|
@ -312,33 +424,35 @@ export class App {
|
||||||
throw errors[0];
|
throw errors[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
async monitorWithNxapi() {
|
||||||
|
this.env.logger.debug("Monitoring with nxapi presence");
|
||||||
|
const fetcher = this.env.newFetcher();
|
||||||
|
await this.exportOnce();
|
||||||
|
|
||||||
const stageExporters = exporters.filter((e) => e.exportStages);
|
while (true) {
|
||||||
if (!this.opts.withStages || stageExporters.length === 0) {
|
await this.countDown(this.profile.state.monitorInterval);
|
||||||
this.env.logger.log("Skip exporting stages.");
|
const nxapiResponse = await fetcher.get({
|
||||||
} else {
|
url: this.opts.nxapiPresenceUrl!,
|
||||||
const stageRecords = await splatnet.getStageRecords();
|
headers: {
|
||||||
|
"User-Agent": USERAGENT,
|
||||||
await Promise.all(
|
},
|
||||||
stageExporters.map((e) =>
|
});
|
||||||
showError(
|
const nxapiData = await nxapiResponse.json();
|
||||||
this.env,
|
const isSplatoon3Active = nxapiData.title?.id === SPLATOON3_TITLE_ID;
|
||||||
e.exportStages!(stageRecords.stageRecords.nodes),
|
if (isSplatoon3Active || this.splatoon3PreviouslyActive) {
|
||||||
).then((result) => {
|
this.env.logger.log("Splatoon 3 is active, exporting data");
|
||||||
if (result.status === "success") {
|
await this.exportOnce();
|
||||||
this.env.logger.log(`Exported stages to ${result.url}`);
|
}
|
||||||
} else if (result.status === "skip") {
|
if (isSplatoon3Active !== this.splatoon3PreviouslyActive) {
|
||||||
this.env.logger.log(`Skipped exporting stages to ${e.name}`);
|
this.env.logger.debug(
|
||||||
} else {
|
"Splatoon 3 status has changed from",
|
||||||
const _never: never = result;
|
this.splatoon3PreviouslyActive,
|
||||||
}
|
"to",
|
||||||
})
|
isSplatoon3Active,
|
||||||
.catch((err) => {
|
);
|
||||||
errors.push(err);
|
}
|
||||||
this.env.logger.error(`\nFailed to export to ${e.name}:`, err);
|
this.splatoon3PreviouslyActive = isSplatoon3Active;
|
||||||
})
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async monitor() {
|
async monitor() {
|
||||||
|
|
@ -354,14 +468,17 @@ export class App {
|
||||||
display: "[:bar] :completed/:total",
|
display: "[:bar] :completed/:total",
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
for (const i of Array(sec).keys()) {
|
try {
|
||||||
bar?.render([{
|
for (const i of Array(sec).keys()) {
|
||||||
completed: i,
|
bar?.render([{
|
||||||
total: sec,
|
completed: i,
|
||||||
}]);
|
total: sec,
|
||||||
await delay(1000);
|
}]);
|
||||||
|
await delay(1000);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await bar?.end();
|
||||||
}
|
}
|
||||||
bar?.end();
|
|
||||||
}
|
}
|
||||||
async run() {
|
async run() {
|
||||||
await this.profile.readState();
|
await this.profile.readState();
|
||||||
|
|
@ -378,7 +495,9 @@ export class App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.opts.monitor) {
|
if (this.opts.nxapiPresenceUrl) {
|
||||||
|
await this.monitorWithNxapi();
|
||||||
|
} else if (this.opts.monitor) {
|
||||||
await this.monitor();
|
await this.monitor();
|
||||||
} else {
|
} else {
|
||||||
await this.exportOnce();
|
await this.exportOnce();
|
||||||
|
|
@ -392,30 +511,24 @@ export class App {
|
||||||
* @param gameList ID list of games, sorted by date, newest first
|
* @param gameList ID list of games, sorted by date, newest first
|
||||||
* @param onStep Callback function called when a game is exported
|
* @param onStep Callback function called when a game is exported
|
||||||
*/
|
*/
|
||||||
async exportGameList({
|
private async exportGameList({
|
||||||
type,
|
type,
|
||||||
fetcher,
|
fetcher,
|
||||||
exporter,
|
exporter,
|
||||||
gameList,
|
gameListFetcher,
|
||||||
stepProgress,
|
stepProgress,
|
||||||
onStep,
|
onStep,
|
||||||
}: {
|
}: {
|
||||||
type: Game["type"];
|
type: Game["type"];
|
||||||
exporter: GameExporter;
|
exporter: GameExporter;
|
||||||
fetcher: GameFetcher;
|
fetcher: GameFetcher;
|
||||||
gameList: string[];
|
gameListFetcher: GameListFetcher;
|
||||||
stepProgress: StepProgress;
|
stepProgress: StepProgress;
|
||||||
onStep: () => void;
|
onStep: () => void;
|
||||||
}): Promise<StepProgress> {
|
}): Promise<StepProgress> {
|
||||||
onStep?.();
|
onStep?.();
|
||||||
|
|
||||||
const workQueue = [
|
const workQueue = await gameListFetcher.fetch(exporter);
|
||||||
...await exporter.notExported({
|
|
||||||
type,
|
|
||||||
list: gameList,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
.reverse();
|
|
||||||
|
|
||||||
const step = async (id: string) => {
|
const step = async (id: string) => {
|
||||||
const detail = await fetcher.fetch(type, id);
|
const detail = await fetcher.fetch(type, id);
|
||||||
|
|
@ -452,7 +565,7 @@ export class App {
|
||||||
}
|
}
|
||||||
printStats(stats: Record<string, StepProgress>) {
|
printStats(stats: Record<string, StepProgress>) {
|
||||||
this.env.logger.log(
|
this.env.logger.log(
|
||||||
`Exported ${
|
`\nExported ${
|
||||||
Object.entries(stats)
|
Object.entries(stats)
|
||||||
.map(([name, { exported }]) => `${name}: ${exported}`)
|
.map(([name, { exported }]) => `${name}: ${exported}`)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,46 @@
|
||||||
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
||||||
|
|
||||||
export const AGENT_NAME = "splashcat / s3si.ts";
|
export const AGENT_NAME = "s3si.ts";
|
||||||
export const AGENT_VERSION = "1.1.1";
|
export const S3SI_VERSION = "0.4.20";
|
||||||
export const S3SI_VERSION = "0.4.1";
|
export const NSOAPP_VERSION = "2.10.1";
|
||||||
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
|
export const WEB_VIEW_VERSION = "6.0.0-9f87c815";
|
||||||
export const NSOAPP_VERSION = "2.5.1";
|
|
||||||
export const WEB_VIEW_VERSION = "4.0.0-d5178440";
|
|
||||||
export enum Queries {
|
export enum Queries {
|
||||||
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871",
|
HomeQuery =
|
||||||
LatestBattleHistoriesQuery = "0d90c7576f1916469b2ae69f64292c02",
|
"51fc56bbf006caf37728914aa8bc0e2c86a80cf195b4d4027d6822a3623098a8",
|
||||||
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
|
LatestBattleHistoriesQuery =
|
||||||
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
|
"b24d22fd6cb251c515c2b90044039698aa27bc1fab15801d83014d919cd45780",
|
||||||
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
|
RegularBattleHistoriesQuery =
|
||||||
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
|
"2fe6ea7a2de1d6a888b7bd3dbeb6acc8e3246f055ca39b80c4531bbcd0727bba",
|
||||||
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd",
|
BankaraBattleHistoriesQuery =
|
||||||
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",
|
"9863ea4744730743268e2940396e21b891104ed40e2286789f05100b45a0b0fd",
|
||||||
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
|
XBattleHistoriesQuery =
|
||||||
|
"eb5996a12705c2e94813a62e05c0dc419aad2811b8d49d53e5732290105559cb",
|
||||||
|
EventBattleHistoriesQuery =
|
||||||
|
"e47f9aac5599f75c842335ef0ab8f4c640e8bf2afe588a3b1d4b480ee79198ac",
|
||||||
|
PrivateBattleHistoriesQuery =
|
||||||
|
"fef94f39b9eeac6b2fac4de43bc0442c16a9f2df95f4d367dd8a79d7c5ed5ce7",
|
||||||
|
VsHistoryDetailQuery =
|
||||||
|
"94faa2ff992222d11ced55e0f349920a82ac50f414ae33c83d1d1c9d8161c5dd",
|
||||||
|
CoopHistoryQuery =
|
||||||
|
"e11a8cf2c3de7348495dea5cdcaa25e0c153541c4ed63f044b6c174bc5b703df",
|
||||||
|
CoopHistoryDetailQuery =
|
||||||
|
"f2d55873a9281213ae27edc171e2b19131b3021a2ae263757543cdd3bf015cc8",
|
||||||
myOutfitCommonDataFilteringConditionQuery =
|
myOutfitCommonDataFilteringConditionQuery =
|
||||||
"d02ab22c9dccc440076055c8baa0fa7a",
|
"ac20c44a952131cb0c9d00eda7bc1a84c1a99546f0f1fc170212d5a6bb51a426",
|
||||||
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",
|
myOutfitCommonDataEquipmentsQuery =
|
||||||
HistoryRecordQuery = "d9246baf077b2a29b5f7aac321810a77",
|
"45a4c343d973864f7bb9e9efac404182be1d48cf2181619505e9b7cd3b56a6e8",
|
||||||
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd",
|
HistoryRecordQuery =
|
||||||
StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3",
|
"a654ecc80161a7ca5c38761c1d9e502d405eae764e2d343618b9c74b1dc0a80f",
|
||||||
|
ConfigureAnalyticsQuery =
|
||||||
|
"2a9302bdd09a13f8b344642d4ed483b9464f20889ac17401e993dfa5c2bb3607",
|
||||||
}
|
}
|
||||||
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
|
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
|
||||||
|
|
||||||
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`;
|
export const USERAGENT = `${AGENT_NAME}/${S3SI_VERSION} (${S3SI_LINK})`;
|
||||||
export const DEFAULT_APP_USER_AGENT =
|
export const DEFAULT_APP_USER_AGENT =
|
||||||
"Mozilla/5.0 (Linux; Android 11; Pixel 5) " +
|
"Mozilla/5.0 (Linux; Android 14; Pixel 7a) " +
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||||
"Chrome/94.0.4606.61 Mobile Safari/537.36";
|
"Chrome/120.0.6099.230 Mobile Safari/537.36";
|
||||||
export const SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net";
|
export const SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net";
|
||||||
export const SPLATNET3_ENDPOINT =
|
export const SPLATNET3_ENDPOINT =
|
||||||
"https://api.lp1.av5ja.srv.nintendo.net/api/graphql";
|
"https://api.lp1.av5ja.srv.nintendo.net/api/graphql";
|
||||||
|
|
@ -94,6 +105,10 @@ export const SPLATNET3_STATINK_MAP: {
|
||||||
"sameride",
|
"sameride",
|
||||||
"380e541b5bc5e49d77ff1a616f1343aeba01d500fee36aaddf8f09d74bd3d3bc":
|
"380e541b5bc5e49d77ff1a616f1343aeba01d500fee36aaddf8f09d74bd3d3bc":
|
||||||
"tripletornado",
|
"tripletornado",
|
||||||
|
"8a7ee88a06407f4be1595ef8af4d2d2ac22bbf213a622cd19bbfaf4d0f36bcd7":
|
||||||
|
"teioika",
|
||||||
|
"a75eac34675bc0d4bd9ca9977cf22472848f89e28e08ee986b4461a3f2af28fc":
|
||||||
|
"ultra_chakuchi",
|
||||||
},
|
},
|
||||||
WATER_LEVEL_MAP: {
|
WATER_LEVEL_MAP: {
|
||||||
0: "low",
|
0: "low",
|
||||||
|
|
@ -101,3 +116,5 @@ export const SPLATNET3_STATINK_MAP: {
|
||||||
2: "high",
|
2: "high",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SPLATOON3_TITLE_ID = "0100c2500fc20000";
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,8 @@ if (import.meta.main) {
|
||||||
const service = new S3SIServiceImplement();
|
const service = new S3SIServiceImplement();
|
||||||
const server = new JSONRPCServer({
|
const server = new JSONRPCServer({
|
||||||
transport: new DenoIO({
|
transport: new DenoIO({
|
||||||
reader: Deno.stdin,
|
reader: Deno.stdin.readable,
|
||||||
writer: Deno.stdout,
|
writer: Deno.stdout.writable,
|
||||||
}),
|
}),
|
||||||
service,
|
service,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import { MongoDB } from "../../deps.ts";
|
|
||||||
import { AGENT_VERSION, NSOAPP_VERSION, S3SI_VERSION } from "../constant.ts";
|
|
||||||
import {
|
|
||||||
CoopHistoryDetail,
|
|
||||||
ExportResult,
|
|
||||||
Game,
|
|
||||||
GameExporter,
|
|
||||||
Queries,
|
|
||||||
RespMap,
|
|
||||||
Summary,
|
|
||||||
VsHistoryDetail,
|
|
||||||
} from "../types.ts";
|
|
||||||
import { parseHistoryDetailId } from "../utils.ts";
|
|
||||||
|
|
||||||
export class MongoDBExporter implements GameExporter {
|
|
||||||
name = "mongodb";
|
|
||||||
mongoDbClient: MongoDB.MongoClient;
|
|
||||||
mongoDb: MongoDB.Db;
|
|
||||||
battlesCollection: MongoDB.Collection;
|
|
||||||
jobsCollection: MongoDB.Collection;
|
|
||||||
summariesCollection: MongoDB.Collection;
|
|
||||||
constructor(private mongoDbUri: string) {
|
|
||||||
this.mongoDbClient = new MongoDB.MongoClient(mongoDbUri);
|
|
||||||
this.mongoDb = this.mongoDbClient.db("splashcat");
|
|
||||||
this.battlesCollection = this.mongoDb.collection("battles");
|
|
||||||
this.jobsCollection = this.mongoDb.collection("jobs");
|
|
||||||
this.summariesCollection = this.mongoDb.collection("summaries");
|
|
||||||
}
|
|
||||||
|
|
||||||
static getGameId(id: string) { // very similar to the file exporter
|
|
||||||
const { uid, timestamp } = parseHistoryDetailId(id);
|
|
||||||
|
|
||||||
return `${uid}_${timestamp}Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async notExported(
|
|
||||||
{ type, list }: { type: Game["type"]; list: string[] },
|
|
||||||
): Promise<string[]> {
|
|
||||||
const out: string[] = [];
|
|
||||||
|
|
||||||
const collection = type === "CoopInfo"
|
|
||||||
? this.jobsCollection
|
|
||||||
: this.battlesCollection;
|
|
||||||
|
|
||||||
for (const id of list) {
|
|
||||||
const uniqueId = MongoDBExporter.getGameId(id);
|
|
||||||
const countNewStorage = await collection.countDocuments({
|
|
||||||
gameId: uniqueId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (countNewStorage === 0) {
|
|
||||||
out.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportGame(game: Game): Promise<ExportResult> {
|
|
||||||
const uniqueId = MongoDBExporter.getGameId(game.detail.id);
|
|
||||||
|
|
||||||
const common = {
|
|
||||||
// this seems like useful data to store...
|
|
||||||
// loosely modeled after FileExporterTypeCommon
|
|
||||||
nsoVersion: NSOAPP_VERSION,
|
|
||||||
agentVersion: AGENT_VERSION,
|
|
||||||
s3siVersion: S3SI_VERSION,
|
|
||||||
exportDate: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const splatNetData = {
|
|
||||||
...game.detail,
|
|
||||||
playedTime: new Date(game.detail.playedTime),
|
|
||||||
};
|
|
||||||
|
|
||||||
const body: {
|
|
||||||
data: Game;
|
|
||||||
splatNetData:
|
|
||||||
& Omit<(VsHistoryDetail | CoopHistoryDetail), "playedTime">
|
|
||||||
& { playedTime: Date };
|
|
||||||
gameId: string;
|
|
||||||
} & typeof common = {
|
|
||||||
...common,
|
|
||||||
data: game,
|
|
||||||
splatNetData,
|
|
||||||
gameId: uniqueId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const isJob = game.type === "CoopInfo";
|
|
||||||
|
|
||||||
const collection = isJob ? this.jobsCollection : this.battlesCollection;
|
|
||||||
|
|
||||||
const result = await collection.insertOne(body);
|
|
||||||
|
|
||||||
const objectId = result.insertedId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: "success",
|
|
||||||
url: `https://new.splatoon.catgirlin.space/battle/${objectId.toString()}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportSummary(summary: Summary): Promise<ExportResult> {
|
|
||||||
const id = summary.uid;
|
|
||||||
|
|
||||||
await this.summariesCollection.insertOne({
|
|
||||||
summaryId: id,
|
|
||||||
...summary,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: "success",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportStages(
|
|
||||||
stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"],
|
|
||||||
): Promise<ExportResult> {
|
|
||||||
for (const stage of stages) {
|
|
||||||
await this.mongoDb.collection("stages").updateOne({
|
|
||||||
"stage.id": stage.id,
|
|
||||||
}, {
|
|
||||||
$set: stage,
|
|
||||||
}, {
|
|
||||||
upsert: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: "success",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
export interface SplashcatUpload {
|
||||||
|
battle: SplashcatBattle;
|
||||||
|
data_type: "splashcat";
|
||||||
|
uploader_agent: {
|
||||||
|
name: string; // max of 32 characters
|
||||||
|
version: string; // max of 50 characters
|
||||||
|
extra: string; // max of 100 characters. displayed as a string at the bottom of battle details. useful for debug info such as manual/monitoring modes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A battle to be uploaded to Splashcat. Any SplatNet 3 strings should use en-US locale.
|
||||||
|
* Splashcat will translate strings into the user's language.
|
||||||
|
*/
|
||||||
|
export interface SplashcatBattle {
|
||||||
|
anarchy?: Anarchy;
|
||||||
|
/**
|
||||||
|
* The en-US string for the award. Splashcat will translate this into the user's language
|
||||||
|
* and manage the award's rank.
|
||||||
|
*/
|
||||||
|
awards: string[];
|
||||||
|
challenge?: Challenge;
|
||||||
|
duration: number;
|
||||||
|
judgement: SplashcatBattleJudgement;
|
||||||
|
knockout?: Knockout;
|
||||||
|
playedTime: string;
|
||||||
|
splatfest?: Splatfest;
|
||||||
|
/**
|
||||||
|
* base64 decoded and split by `:` to get the last section
|
||||||
|
*/
|
||||||
|
splatnetId: string;
|
||||||
|
teams: Team[];
|
||||||
|
vsMode: VsMode;
|
||||||
|
vsRule: VsRule;
|
||||||
|
vsStageId: number;
|
||||||
|
xBattle?: XBattle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Anarchy {
|
||||||
|
mode?: AnarchyMode;
|
||||||
|
pointChange?: number;
|
||||||
|
points?: number;
|
||||||
|
power?: number;
|
||||||
|
rank?: Rank;
|
||||||
|
sPlusNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnarchyMode = "SERIES" | "OPEN";
|
||||||
|
|
||||||
|
export type Rank =
|
||||||
|
| "C-"
|
||||||
|
| "C"
|
||||||
|
| "C+"
|
||||||
|
| "B-"
|
||||||
|
| "B"
|
||||||
|
| "B+"
|
||||||
|
| "A-"
|
||||||
|
| "A"
|
||||||
|
| "A+"
|
||||||
|
| "S"
|
||||||
|
| "S+";
|
||||||
|
|
||||||
|
export interface Challenge {
|
||||||
|
/**
|
||||||
|
* base64 decoded and split by `-` to get the last section
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
power?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SplashcatBattleJudgement =
|
||||||
|
| "WIN"
|
||||||
|
| "LOSE"
|
||||||
|
| "DRAW"
|
||||||
|
| "EXEMPTED_LOSE"
|
||||||
|
| "DEEMED_LOSE";
|
||||||
|
|
||||||
|
export type Knockout = "NEITHER" | "WIN" | "LOSE";
|
||||||
|
|
||||||
|
export interface Splatfest {
|
||||||
|
cloutMultiplier?: CloutMultiplier;
|
||||||
|
mode?: SplatfestMode;
|
||||||
|
power?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CloutMultiplier = "NONE" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
|
||||||
|
|
||||||
|
export type SplatfestMode = "OPEN" | "PRO";
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
|
color: Color;
|
||||||
|
festStreakWinCount?: number;
|
||||||
|
festTeamName?: string;
|
||||||
|
festUniformBonusRate?: number;
|
||||||
|
festUniformName?: string;
|
||||||
|
isMyTeam: boolean;
|
||||||
|
judgement?: TeamJudgement;
|
||||||
|
noroshi?: number;
|
||||||
|
order: number;
|
||||||
|
paintRatio?: number;
|
||||||
|
players?: Player[];
|
||||||
|
score?: number;
|
||||||
|
tricolorRole?: TricolorRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Color {
|
||||||
|
a: number;
|
||||||
|
b: number;
|
||||||
|
g: number;
|
||||||
|
r: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TeamJudgement = "WIN" | "LOSE" | "DRAW";
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
assists?: number;
|
||||||
|
/**
|
||||||
|
* Array of badge IDs. Use JSON `null` for empty slots.
|
||||||
|
*/
|
||||||
|
badges: Array<number | null>;
|
||||||
|
clothingGear: Gear;
|
||||||
|
deaths?: number;
|
||||||
|
disconnected: boolean;
|
||||||
|
headGear: Gear;
|
||||||
|
isMe: boolean;
|
||||||
|
/**
|
||||||
|
* Should report the same way that SplatNet 3 does (kills + assists)
|
||||||
|
*/
|
||||||
|
kills?: number;
|
||||||
|
name: string;
|
||||||
|
nameId?: string;
|
||||||
|
noroshiTry?: number;
|
||||||
|
nplnId: string;
|
||||||
|
paint: number;
|
||||||
|
shoesGear: Gear;
|
||||||
|
specials?: number;
|
||||||
|
species: Species;
|
||||||
|
splashtagBackgroundId: number;
|
||||||
|
title: string;
|
||||||
|
weaponId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A piece of gear. Use en-US locale for name and all abilities.
|
||||||
|
*/
|
||||||
|
export interface Gear {
|
||||||
|
name?: string;
|
||||||
|
primaryAbility?: string;
|
||||||
|
secondaryAbilities?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Species = "INKLING" | "OCTOLING";
|
||||||
|
|
||||||
|
export type TricolorRole = "ATTACK1" | "ATTACK2" | "DEFENSE";
|
||||||
|
|
||||||
|
export type VsMode =
|
||||||
|
| "BANKARA"
|
||||||
|
| "X_MATCH"
|
||||||
|
| "REGULAR"
|
||||||
|
| "FEST"
|
||||||
|
| "PRIVATE"
|
||||||
|
| "CHALLENGE";
|
||||||
|
|
||||||
|
export type VsRule =
|
||||||
|
| "AREA"
|
||||||
|
| "TURF_WAR"
|
||||||
|
| "TRI_COLOR"
|
||||||
|
| "LOFT"
|
||||||
|
| "CLAM"
|
||||||
|
| "GOAL";
|
||||||
|
|
||||||
|
export interface XBattle {
|
||||||
|
xPower?: number;
|
||||||
|
xRank?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplashcatRecentBattleIds {
|
||||||
|
battle_ids: string[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
import { AGENT_NAME, S3SI_VERSION, USERAGENT } from "../constant.ts";
|
||||||
|
import {
|
||||||
|
Color,
|
||||||
|
ExportResult,
|
||||||
|
Game,
|
||||||
|
GameExporter,
|
||||||
|
Nameplate,
|
||||||
|
PlayerGear,
|
||||||
|
VsInfo,
|
||||||
|
VsPlayer,
|
||||||
|
VsTeam,
|
||||||
|
} from "../types.ts";
|
||||||
|
import { base64, msgpack, Mutex } from "../../deps.ts";
|
||||||
|
import { APIError } from "../APIError.ts";
|
||||||
|
import { Env } from "../env.ts";
|
||||||
|
import {
|
||||||
|
Gear,
|
||||||
|
Player,
|
||||||
|
Rank,
|
||||||
|
SplashcatBattle,
|
||||||
|
SplashcatRecentBattleIds,
|
||||||
|
Team,
|
||||||
|
TeamJudgement,
|
||||||
|
} from "./splashcat-types.ts";
|
||||||
|
import { SplashcatUpload } from "./splashcat-types.ts";
|
||||||
|
|
||||||
|
async function checkResponse(resp: Response) {
|
||||||
|
// 200~299
|
||||||
|
if (Math.floor(resp.status / 100) !== 2) {
|
||||||
|
const json = await resp.json().catch(() => undefined);
|
||||||
|
throw new APIError({
|
||||||
|
response: resp,
|
||||||
|
json,
|
||||||
|
message: "Failed to fetch data from stat.ink",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SplashcatAPI {
|
||||||
|
splashcat = "https://splashcat.ink";
|
||||||
|
FETCH_LOCK = new Mutex();
|
||||||
|
cache: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
constructor(private splashcatApiKey: string, private env: Env) {}
|
||||||
|
|
||||||
|
requestHeaders() {
|
||||||
|
return {
|
||||||
|
"User-Agent": USERAGENT,
|
||||||
|
"Authorization": `Bearer ${this.splashcatApiKey}`,
|
||||||
|
"Fly-Prefer-Region": "iad",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async uuidList(): Promise<string[]> {
|
||||||
|
const fetch = this.env.newFetcher();
|
||||||
|
const response = await fetch.get({
|
||||||
|
url: `${this.splashcat}/battles/api/recent/`,
|
||||||
|
headers: this.requestHeaders(),
|
||||||
|
});
|
||||||
|
await checkResponse(response);
|
||||||
|
|
||||||
|
const recentBattlesData: SplashcatRecentBattleIds = await response.json();
|
||||||
|
const recentBattleIds = recentBattlesData.battle_ids;
|
||||||
|
|
||||||
|
if (!Array.isArray(recentBattleIds)) {
|
||||||
|
throw new APIError({
|
||||||
|
response,
|
||||||
|
json: recentBattlesData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return recentBattleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
async postBattle(body: SplashcatUpload) {
|
||||||
|
const fetch = this.env.newFetcher();
|
||||||
|
const resp = await fetch.post({
|
||||||
|
url: `${this.splashcat}/battles/api/upload/`,
|
||||||
|
headers: {
|
||||||
|
...this.requestHeaders(),
|
||||||
|
"Content-Type": "application/x-msgpack",
|
||||||
|
},
|
||||||
|
body: msgpack.encode(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await resp.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new APIError({
|
||||||
|
response: resp,
|
||||||
|
message: "Failed to export battle",
|
||||||
|
json,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getCached<T>(url: string): Promise<T> {
|
||||||
|
const release = await this.FETCH_LOCK.acquire();
|
||||||
|
try {
|
||||||
|
if (this.cache[url]) {
|
||||||
|
return this.cache[url] as T;
|
||||||
|
}
|
||||||
|
const fetch = this.env.newFetcher();
|
||||||
|
const resp = await fetch.get({
|
||||||
|
url,
|
||||||
|
headers: this.requestHeaders(),
|
||||||
|
});
|
||||||
|
await checkResponse(resp);
|
||||||
|
const json = await resp.json();
|
||||||
|
this.cache[url] = json;
|
||||||
|
return json;
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NameDict = {
|
||||||
|
gearPower: Record<string, number | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporter to Splashcat.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SplashcatExporter implements GameExporter {
|
||||||
|
name = "Splashcat";
|
||||||
|
private api: SplashcatAPI;
|
||||||
|
private uploadMode: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ splashcatApiKey, uploadMode, env }: {
|
||||||
|
splashcatApiKey: string;
|
||||||
|
uploadMode: string;
|
||||||
|
env: Env;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.api = new SplashcatAPI(splashcatApiKey, env);
|
||||||
|
this.uploadMode = uploadMode;
|
||||||
|
}
|
||||||
|
async exportGame(game: Game): Promise<ExportResult> {
|
||||||
|
if (game.type === "VsInfo") {
|
||||||
|
const battle = await this.mapBattle(game);
|
||||||
|
const body: SplashcatUpload = {
|
||||||
|
battle,
|
||||||
|
data_type: "splashcat",
|
||||||
|
uploader_agent: {
|
||||||
|
name: AGENT_NAME,
|
||||||
|
version: S3SI_VERSION,
|
||||||
|
extra: `Upload Mode: ${this.uploadMode}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const resp = await this.api.postBattle(body);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
url: resp.battle_id
|
||||||
|
? `https://splashcat.ink/battles/${resp.battle_id}/`
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: "skip",
|
||||||
|
reason: "Splashcat does not support Salmon Run",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getGameId(id: string) {
|
||||||
|
const plainText = new TextDecoder().decode(base64.decodeBase64(id));
|
||||||
|
|
||||||
|
return plainText.split(":").at(-1);
|
||||||
|
}
|
||||||
|
async notExported(
|
||||||
|
{ type, list }: { list: string[]; type: Game["type"] },
|
||||||
|
): Promise<string[]> {
|
||||||
|
if (type !== "VsInfo") return [];
|
||||||
|
const uuid = await this.api.uuidList();
|
||||||
|
|
||||||
|
const out: string[] = [];
|
||||||
|
|
||||||
|
for (const id of list) {
|
||||||
|
const gameId = SplashcatExporter.getGameId(id)!;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!uuid.includes(gameId)
|
||||||
|
) {
|
||||||
|
out.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
mapPlayer = (
|
||||||
|
player: VsPlayer,
|
||||||
|
_index: number,
|
||||||
|
): Player => {
|
||||||
|
const result: Player = {
|
||||||
|
badges: (player.nameplate as Nameplate).badges.map((i) =>
|
||||||
|
i
|
||||||
|
? Number(
|
||||||
|
new TextDecoder().decode(base64.decodeBase64(i.id)).split("-")[1],
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
),
|
||||||
|
splashtagBackgroundId: Number(
|
||||||
|
new TextDecoder().decode(
|
||||||
|
base64.decodeBase64((player.nameplate as Nameplate).background.id),
|
||||||
|
).split("-")[1],
|
||||||
|
),
|
||||||
|
clothingGear: this.mapGear(player.clothingGear),
|
||||||
|
headGear: this.mapGear(player.headGear),
|
||||||
|
shoesGear: this.mapGear(player.shoesGear),
|
||||||
|
disconnected: player.result ? false : true,
|
||||||
|
isMe: player.isMyself,
|
||||||
|
name: player.name,
|
||||||
|
nameId: player.nameId ?? "",
|
||||||
|
nplnId: new TextDecoder().decode(base64.decodeBase64(player.id)).split(
|
||||||
|
":",
|
||||||
|
).at(
|
||||||
|
-1,
|
||||||
|
)!,
|
||||||
|
paint: player.paint,
|
||||||
|
species: player.species,
|
||||||
|
weaponId: Number(
|
||||||
|
new TextDecoder().decode(base64.decodeBase64(player.weapon.id)).split(
|
||||||
|
"-",
|
||||||
|
)[1],
|
||||||
|
),
|
||||||
|
assists: player.result?.assist,
|
||||||
|
deaths: player.result?.death,
|
||||||
|
kills: player.result?.kill,
|
||||||
|
specials: player.result?.special,
|
||||||
|
noroshiTry: player.result?.noroshiTry ?? undefined,
|
||||||
|
title: player.byname,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
mapBattle(
|
||||||
|
{
|
||||||
|
detail: vsDetail,
|
||||||
|
rankState,
|
||||||
|
}: VsInfo,
|
||||||
|
): SplashcatBattle {
|
||||||
|
const {
|
||||||
|
myTeam,
|
||||||
|
otherTeams,
|
||||||
|
} = vsDetail;
|
||||||
|
|
||||||
|
const self = myTeam.players.find((i) => i.isMyself);
|
||||||
|
if (!self) {
|
||||||
|
throw new Error("Self not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherTeams.length === 0) {
|
||||||
|
throw new Error(`Other teams is empty`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let anarchyMode: "OPEN" | "SERIES" | undefined;
|
||||||
|
if (vsDetail.bankaraMatch?.mode) {
|
||||||
|
anarchyMode = vsDetail.bankaraMatch.mode === "OPEN" ? "OPEN" : "SERIES";
|
||||||
|
}
|
||||||
|
|
||||||
|
const rank = rankState?.rank.substring(0, 2) ?? undefined;
|
||||||
|
const sPlusNumber = rankState?.rank.substring(2) ?? undefined;
|
||||||
|
|
||||||
|
const result: SplashcatBattle = {
|
||||||
|
splatnetId: SplashcatExporter.getGameId(vsDetail.id)!,
|
||||||
|
duration: vsDetail.duration,
|
||||||
|
judgement: vsDetail.judgement,
|
||||||
|
playedTime: new Date(vsDetail.playedTime).toISOString()!,
|
||||||
|
vsMode: vsDetail.vsMode.mode === "LEAGUE"
|
||||||
|
? "CHALLENGE"
|
||||||
|
: vsDetail.vsMode.mode,
|
||||||
|
vsRule: vsDetail.vsRule.rule,
|
||||||
|
vsStageId: Number(
|
||||||
|
new TextDecoder().decode(base64.decodeBase64(vsDetail.vsStage.id))
|
||||||
|
.split(
|
||||||
|
"-",
|
||||||
|
)[1],
|
||||||
|
),
|
||||||
|
anarchy: vsDetail.vsMode.mode === "BANKARA"
|
||||||
|
? {
|
||||||
|
mode: anarchyMode,
|
||||||
|
pointChange: vsDetail.bankaraMatch?.earnedUdemaePoint ?? undefined,
|
||||||
|
power: vsDetail.bankaraMatch?.bankaraPower?.power ?? undefined,
|
||||||
|
points: rankState?.rankPoint ?? undefined,
|
||||||
|
rank: rank as Rank,
|
||||||
|
sPlusNumber: sPlusNumber ? Number(sPlusNumber) : undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
knockout: vsDetail.knockout ?? undefined,
|
||||||
|
splatfest: vsDetail.vsMode.mode === "FEST"
|
||||||
|
? {
|
||||||
|
cloutMultiplier: vsDetail.festMatch?.dragonMatchType === "NORMAL"
|
||||||
|
? "NONE"
|
||||||
|
: (vsDetail.festMatch?.dragonMatchType ?? undefined),
|
||||||
|
power: vsDetail.festMatch?.myFestPower ?? undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
xBattle: vsDetail.vsMode.mode === "X_MATCH"
|
||||||
|
? {
|
||||||
|
xPower: vsDetail.xMatch?.lastXPower ?? undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
challenge: vsDetail.vsMode.mode === "LEAGUE"
|
||||||
|
? {
|
||||||
|
id: new TextDecoder().decode(
|
||||||
|
base64.decodeBase64(vsDetail.leagueMatch?.leagueMatchEvent?.id!),
|
||||||
|
).split("-")[1],
|
||||||
|
power: vsDetail.leagueMatch?.myLeaguePower ?? undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
teams: [],
|
||||||
|
awards: vsDetail.awards.map((i) => i.name),
|
||||||
|
};
|
||||||
|
|
||||||
|
const teams: VsTeam[] = [vsDetail.myTeam, ...vsDetail.otherTeams];
|
||||||
|
|
||||||
|
for (const team of teams) {
|
||||||
|
const players = team.players.map(this.mapPlayer);
|
||||||
|
const teamResult: Team = {
|
||||||
|
players,
|
||||||
|
color: team.color,
|
||||||
|
isMyTeam: team.players.find((i) => i.isMyself) !== undefined,
|
||||||
|
judgement: team.judgement as TeamJudgement,
|
||||||
|
order: team.order,
|
||||||
|
festStreakWinCount: team.festStreakWinCount,
|
||||||
|
festTeamName: team.festTeamName ?? undefined,
|
||||||
|
festUniformBonusRate: team.festUniformBonusRate,
|
||||||
|
festUniformName: team.festUniformName,
|
||||||
|
noroshi: team.result?.noroshi ?? undefined,
|
||||||
|
paintRatio: team.result?.paintRatio ?? undefined,
|
||||||
|
score: team.result?.score ?? undefined,
|
||||||
|
tricolorRole: team.tricolorRole ?? undefined,
|
||||||
|
};
|
||||||
|
result.teams.push(teamResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
mapColor(color: Color): string | undefined {
|
||||||
|
const float2hex = (i: number) =>
|
||||||
|
Math.round(i * 255).toString(16).padStart(2, "0");
|
||||||
|
// rgba
|
||||||
|
const numbers = [color.r, color.g, color.b, color.a];
|
||||||
|
return numbers.map(float2hex).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
mapGear(gear: PlayerGear): Gear {
|
||||||
|
return {
|
||||||
|
name: gear.name,
|
||||||
|
primaryAbility: gear.primaryGearPower.name,
|
||||||
|
secondaryAbilities: gear.additionalGearPowers.map((i) => i.name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
AGENT_NAME,
|
AGENT_NAME,
|
||||||
COMBINED_VERSION,
|
|
||||||
S3SI_VERSION,
|
S3SI_VERSION,
|
||||||
SPLATNET3_STATINK_MAP,
|
SPLATNET3_STATINK_MAP,
|
||||||
USERAGENT,
|
USERAGENT,
|
||||||
|
|
@ -41,7 +40,7 @@ import {
|
||||||
urlSimplify,
|
urlSimplify,
|
||||||
} from "../utils.ts";
|
} from "../utils.ts";
|
||||||
import { Env } from "../env.ts";
|
import { Env } from "../env.ts";
|
||||||
import GEAR_MAP from "../assets/gear-map.json" assert { type: "json" };
|
import GEAR_MAP from "../assets/gear-map.json" with { type: "json" };
|
||||||
|
|
||||||
const COOP_POINT_MAP: Record<number, number | undefined> = {
|
const COOP_POINT_MAP: Record<number, number | undefined> = {
|
||||||
0: -20,
|
0: -20,
|
||||||
|
|
@ -366,7 +365,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
{ primaryGearPower, additionalGearPowers }: PlayerGear,
|
{ primaryGearPower, additionalGearPowers }: PlayerGear,
|
||||||
): StatInkGear => {
|
): StatInkGear => {
|
||||||
const primary = mapAbility(primaryGearPower);
|
const primary = mapAbility(primaryGearPower);
|
||||||
if (!primary) {
|
if (!primary && !this.isRandom(primaryGearPower.image)) {
|
||||||
throw new Error("Unknown ability: " + primaryGearPower.name);
|
throw new Error("Unknown ability: " + primaryGearPower.name);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -394,6 +393,8 @@ export class StatInkExporter implements GameExporter {
|
||||||
inked: player.paint,
|
inked: player.paint,
|
||||||
gears: await this.mapGears(player),
|
gears: await this.mapGears(player),
|
||||||
crown: player.crown ? "yes" : "no",
|
crown: player.crown ? "yes" : "no",
|
||||||
|
crown_type: undefined,
|
||||||
|
species: player.species === "INKLING" ? "inkling" : "octoling",
|
||||||
disconnected: player.result ? "no" : "yes",
|
disconnected: player.result ? "no" : "yes",
|
||||||
};
|
};
|
||||||
if (player.result) {
|
if (player.result) {
|
||||||
|
|
@ -404,6 +405,13 @@ export class StatInkExporter implements GameExporter {
|
||||||
result.signal = player.result.noroshiTry ?? undefined;
|
result.signal = player.result.noroshiTry ?? undefined;
|
||||||
result.special = player.result.special;
|
result.special = player.result.special;
|
||||||
}
|
}
|
||||||
|
if (player.crown) {
|
||||||
|
result.crown_type = "x";
|
||||||
|
} else if (player.festDragonCert === "DRAGON") {
|
||||||
|
result.crown_type = "100x";
|
||||||
|
} else if (player.festDragonCert === "DOUBLE_DRAGON") {
|
||||||
|
result.crown_type = "333x";
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
async mapBattle(
|
async mapBattle(
|
||||||
|
|
@ -459,7 +467,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
),
|
),
|
||||||
|
|
||||||
agent: AGENT_NAME,
|
agent: AGENT_NAME,
|
||||||
agent_version: COMBINED_VERSION,
|
agent_version: S3SI_VERSION,
|
||||||
agent_variables: {
|
agent_variables: {
|
||||||
"Upload Mode": this.uploadMode,
|
"Upload Mode": this.uploadMode,
|
||||||
},
|
},
|
||||||
|
|
@ -588,6 +596,8 @@ export class StatInkExporter implements GameExporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.bankara_power_after = vsDetail.bankaraMatch?.bankaraPower?.power;
|
||||||
|
|
||||||
if (rankBeforeState && rankState) {
|
if (rankBeforeState && rankState) {
|
||||||
result.rank_before_exp = rankBeforeState.rankPoint;
|
result.rank_before_exp = rankBeforeState.rankPoint;
|
||||||
result.rank_after_exp = rankState.rankPoint;
|
result.rank_after_exp = rankState.rankPoint;
|
||||||
|
|
@ -619,16 +629,18 @@ export class StatInkExporter implements GameExporter {
|
||||||
}
|
}
|
||||||
isRandom(image: Image | null): boolean {
|
isRandom(image: Image | null): boolean {
|
||||||
// question mark
|
// question mark
|
||||||
const RANDOM_FILENAME =
|
const RANDOM_FILENAME = [
|
||||||
"473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1";
|
"473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1",
|
||||||
|
"dc937b59892604f5a86ac96936cd7ff09e25f18ae6b758e8014a24c7fa039e91",
|
||||||
|
];
|
||||||
// file exporter will replace url to { pathname: string } | string
|
// file exporter will replace url to { pathname: string } | string
|
||||||
const url = image?.url as ReturnType<typeof urlSimplify> | undefined | null;
|
const url = image?.url as ReturnType<typeof urlSimplify> | undefined | null;
|
||||||
if (typeof url === "string") {
|
if (typeof url === "string") {
|
||||||
return url.includes(RANDOM_FILENAME);
|
return RANDOM_FILENAME.some((i) => url.includes(i));
|
||||||
} else if (url === undefined || url === null) {
|
} else if (url === undefined || url === null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return url.pathname.includes(RANDOM_FILENAME);
|
return RANDOM_FILENAME.some((i) => url.pathname.includes(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async mapCoopWeapon(
|
async mapCoopWeapon(
|
||||||
|
|
@ -698,6 +710,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
rescued: rescuedCount,
|
rescued: rescuedCount,
|
||||||
defeat_boss: defeatEnemyCount,
|
defeat_boss: defeatEnemyCount,
|
||||||
disconnected: disconnected ? "yes" : "no",
|
disconnected: disconnected ? "yes" : "no",
|
||||||
|
species: player.species === "INKLING" ? "inkling" : "octoling",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
mapKing(id?: string) {
|
mapKing(id?: string) {
|
||||||
|
|
|
||||||
21
src/iksm.ts
21
src/iksm.ts
|
|
@ -140,7 +140,8 @@ export async function getGToken(
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Connection": "Keep-Alive",
|
"Connection": "Keep-Alive",
|
||||||
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 7.1.2)",
|
"User-Agent":
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 14; Pixel 7a Build/UQ1A.240105.004)",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"client_id": "71b963c1b7b6d119",
|
"client_id": "71b963c1b7b6d119",
|
||||||
|
|
@ -194,7 +195,7 @@ export async function getGToken(
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
"Connection": "Keep-Alive",
|
"Connection": "Keep-Alive",
|
||||||
"Accept-Encoding": "gzip",
|
"Accept-Encoding": "gzip",
|
||||||
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/7.1.2)`,
|
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/14)`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
parameter: {
|
parameter: {
|
||||||
|
|
@ -211,22 +212,24 @@ export async function getGToken(
|
||||||
);
|
);
|
||||||
const respJson = await resp.json();
|
const respJson = await resp.json();
|
||||||
|
|
||||||
const idToken2: string = respJson?.result?.webApiServerCredential
|
const idToken2: string | undefined = respJson?.result
|
||||||
|
?.webApiServerCredential
|
||||||
?.accessToken;
|
?.accessToken;
|
||||||
const coralUserId: number = respJson?.result?.user?.id;
|
const coralUserId: string | undefined = respJson?.result?.user?.id
|
||||||
|
?.toString();
|
||||||
|
|
||||||
if (!idToken2 || !coralUserId) {
|
if (!idToken2 || !coralUserId) {
|
||||||
throw new APIError({
|
throw new APIError({
|
||||||
response: resp,
|
response: resp,
|
||||||
json: respJson,
|
json: respJson,
|
||||||
message:
|
message:
|
||||||
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`,
|
`No idToken2 or coralUserId found. Please try again later. (${idToken2?.length}, ${coralUserId?.length})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [idToken2, coralUserId] as const;
|
return [idToken2, coralUserId] as const;
|
||||||
};
|
};
|
||||||
const getGToken = async (idToken: string, coralUserId: number) => {
|
const getGToken = async (idToken: string, coralUserId: string) => {
|
||||||
const { f, request_id: requestId, timestamp } = await callImink({
|
const { f, request_id: requestId, timestamp } = await callImink({
|
||||||
step: 2,
|
step: 2,
|
||||||
idToken,
|
idToken,
|
||||||
|
|
@ -244,7 +247,7 @@ export async function getGToken(
|
||||||
"Authorization": `Bearer ${idToken}`,
|
"Authorization": `Bearer ${idToken}`,
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
"Accept-Encoding": "gzip",
|
"Accept-Encoding": "gzip",
|
||||||
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/7.1.2)`,
|
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/14)`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
parameter: {
|
parameter: {
|
||||||
|
|
@ -414,7 +417,7 @@ async function callImink(
|
||||||
step: number;
|
step: number;
|
||||||
idToken: string;
|
idToken: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
coralUserId?: number;
|
coralUserId?: string;
|
||||||
env: Env;
|
env: Env;
|
||||||
},
|
},
|
||||||
): Promise<IminkResponse> {
|
): Promise<IminkResponse> {
|
||||||
|
|
@ -425,6 +428,8 @@ async function callImink(
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": USERAGENT,
|
"User-Agent": USERAGENT,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"X-znca-Platform": "Android",
|
||||||
|
"X-znca-Version": NSOAPP_VERSION,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"token": idToken,
|
"token": idToken,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { IPC } from "./stdio.ts";
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/// <reference lib="deno.ns" />
|
|
||||||
|
|
||||||
import { io, writeAll } from "../../deps.ts";
|
|
||||||
import type { ExtractType } from "./types.ts";
|
|
||||||
|
|
||||||
export class IPC<T extends { type: string }> {
|
|
||||||
lines: AsyncIterableIterator<string>;
|
|
||||||
writer: Deno.Writer;
|
|
||||||
constructor({ reader, writer }: {
|
|
||||||
reader: Deno.Reader;
|
|
||||||
writer: Deno.Writer;
|
|
||||||
}) {
|
|
||||||
this.lines = io.readLines(reader);
|
|
||||||
this.writer = writer;
|
|
||||||
}
|
|
||||||
async recvType<K extends T["type"]>(
|
|
||||||
type: K,
|
|
||||||
): Promise<ExtractType<T, K>> {
|
|
||||||
const data = await this.recv();
|
|
||||||
if (data.type !== type) {
|
|
||||||
throw new Error(`Unexpected type: ${data.type}`);
|
|
||||||
}
|
|
||||||
return data as ExtractType<T, K>;
|
|
||||||
}
|
|
||||||
async recv(): Promise<T> {
|
|
||||||
const result = await this.lines.next();
|
|
||||||
|
|
||||||
if (!result.done) {
|
|
||||||
return JSON.parse(result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("EOF");
|
|
||||||
}
|
|
||||||
async send(data: T) {
|
|
||||||
await writeAll(
|
|
||||||
this.writer,
|
|
||||||
new TextEncoder().encode(JSON.stringify(data) + "\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
export type Command = {
|
|
||||||
type: "hello";
|
|
||||||
data: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExtractType<T extends { type: string }, K extends T["type"]> =
|
|
||||||
Extract<
|
|
||||||
T,
|
|
||||||
{ type: K }
|
|
||||||
>;
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { io, writeAll } from "../../deps.ts";
|
import { readLines } from "../utils.ts";
|
||||||
import { Transport } from "./types.ts";
|
import { Transport } from "./types.ts";
|
||||||
|
|
||||||
export class DenoIO implements Transport {
|
export class DenoIO implements Transport {
|
||||||
lines: AsyncIterableIterator<string>;
|
lines: AsyncIterableIterator<string>;
|
||||||
writer: Deno.Writer & Deno.Closer;
|
writer: WritableStreamDefaultWriter<Uint8Array>;
|
||||||
constructor({ reader, writer }: {
|
constructor({ reader, writer }: {
|
||||||
reader: Deno.Reader;
|
reader: ReadableStream<Uint8Array>;
|
||||||
writer: Deno.Writer & Deno.Closer;
|
writer: WritableStream<Uint8Array>;
|
||||||
}) {
|
}) {
|
||||||
this.lines = io.readLines(reader);
|
this.lines = readLines(reader);
|
||||||
this.writer = writer;
|
this.writer = writer.getWriter();
|
||||||
}
|
}
|
||||||
async recv(): Promise<string | undefined> {
|
async recv(): Promise<string | undefined> {
|
||||||
const result = await this.lines.next();
|
const result = await this.lines.next();
|
||||||
|
|
@ -21,10 +21,8 @@ export class DenoIO implements Transport {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
async send(data: string) {
|
async send(data: string) {
|
||||||
await writeAll(
|
await this.writer.ready;
|
||||||
this.writer,
|
await this.writer.write(new TextEncoder().encode(data + "\n"));
|
||||||
new TextEncoder().encode(data + "\n"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
async close() {
|
async close() {
|
||||||
await this.writer.close();
|
await this.writer.close();
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
import { DEFAULT_ENV, Env } from "./env.ts";
|
import { DEFAULT_ENV, Env } from "./env.ts";
|
||||||
import { getBulletToken, getGToken } from "./iksm.ts";
|
import { getBulletToken, getGToken } from "./iksm.ts";
|
||||||
import { parseHistoryDetailId } from "./utils.ts";
|
import { battleTime, parseHistoryDetailId } from "./utils.ts";
|
||||||
|
|
||||||
export class Splatnet3 {
|
export class Splatnet3 {
|
||||||
protected profile: Profile;
|
protected profile: Profile;
|
||||||
|
|
@ -137,6 +137,12 @@ export class Splatnet3 {
|
||||||
[BattleListType.Bankara]: () =>
|
[BattleListType.Bankara]: () =>
|
||||||
this.request(Queries.BankaraBattleHistoriesQuery)
|
this.request(Queries.BankaraBattleHistoriesQuery)
|
||||||
.then((r) => getIdsFromGroups(r.bankaraBattleHistories)),
|
.then((r) => getIdsFromGroups(r.bankaraBattleHistories)),
|
||||||
|
[BattleListType.XBattle]: () =>
|
||||||
|
this.request(Queries.XBattleHistoriesQuery)
|
||||||
|
.then((r) => getIdsFromGroups(r.xBattleHistories)),
|
||||||
|
[BattleListType.Event]: () =>
|
||||||
|
this.request(Queries.EventBattleHistoriesQuery)
|
||||||
|
.then((r) => getIdsFromGroups(r.eventBattleHistories)),
|
||||||
[BattleListType.Private]: () =>
|
[BattleListType.Private]: () =>
|
||||||
this.request(Queries.PrivateBattleHistoriesQuery)
|
this.request(Queries.PrivateBattleHistoriesQuery)
|
||||||
.then((r) => getIdsFromGroups(r.privateBattleHistories)),
|
.then((r) => getIdsFromGroups(r.privateBattleHistories)),
|
||||||
|
|
@ -168,6 +174,29 @@ export class Splatnet3 {
|
||||||
return await this.BATTLE_LIST_TYPE_MAP[battleListType]();
|
return await this.BATTLE_LIST_TYPE_MAP[battleListType]();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all id from all battle list, sort by time, [0] is the latest
|
||||||
|
async getAllBattleList() {
|
||||||
|
const ALL_TYPE: BattleListType[] = [
|
||||||
|
BattleListType.Regular,
|
||||||
|
BattleListType.Bankara,
|
||||||
|
BattleListType.XBattle,
|
||||||
|
BattleListType.Event,
|
||||||
|
BattleListType.Private,
|
||||||
|
];
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (const type of ALL_TYPE) {
|
||||||
|
ids.push(...await this.getBattleList(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeMap = new Map<string, Date>(
|
||||||
|
ids.map((id) => [id, battleTime(id)] as const),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ids.sort((a, b) =>
|
||||||
|
timeMap.get(b)!.getTime() - timeMap.get(a)!.getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getBattleDetail(
|
getBattleDetail(
|
||||||
id: string,
|
id: string,
|
||||||
) {
|
) {
|
||||||
|
|
@ -257,12 +286,6 @@ export class Splatnet3 {
|
||||||
CoopHistoryQuery,
|
CoopHistoryQuery,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStageRecords() {
|
|
||||||
const resp = await this.request(Queries.StageRecordQuery);
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIdsFromGroups<T extends { id: string }>(
|
function getIdsFromGroups<T extends { id: string }>(
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ export type State = {
|
||||||
statInkApiKey?: string;
|
statInkApiKey?: string;
|
||||||
fileExportPath: string;
|
fileExportPath: string;
|
||||||
monitorInterval: number;
|
monitorInterval: number;
|
||||||
mongoDbUri?: string;
|
|
||||||
splashcatApiKey?: string;
|
splashcatApiKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
36
src/types.ts
36
src/types.ts
|
|
@ -1,4 +1,3 @@
|
||||||
import { splatNet3Types } from "../deps.ts";
|
|
||||||
import { RankState } from "./state.ts";
|
import { RankState } from "./state.ts";
|
||||||
import { Queries } from "./constant.ts";
|
import { Queries } from "./constant.ts";
|
||||||
export { Queries };
|
export { Queries };
|
||||||
|
|
@ -9,6 +8,7 @@ export type VarsMap = {
|
||||||
[Queries.RegularBattleHistoriesQuery]: [];
|
[Queries.RegularBattleHistoriesQuery]: [];
|
||||||
[Queries.BankaraBattleHistoriesQuery]: [];
|
[Queries.BankaraBattleHistoriesQuery]: [];
|
||||||
[Queries.XBattleHistoriesQuery]: [];
|
[Queries.XBattleHistoriesQuery]: [];
|
||||||
|
[Queries.EventBattleHistoriesQuery]: [];
|
||||||
[Queries.PrivateBattleHistoriesQuery]: [];
|
[Queries.PrivateBattleHistoriesQuery]: [];
|
||||||
[Queries.VsHistoryDetailQuery]: [{
|
[Queries.VsHistoryDetailQuery]: [{
|
||||||
vsResultId: string;
|
vsResultId: string;
|
||||||
|
|
@ -21,7 +21,6 @@ export type VarsMap = {
|
||||||
[Queries.myOutfitCommonDataEquipmentsQuery]: [];
|
[Queries.myOutfitCommonDataEquipmentsQuery]: [];
|
||||||
[Queries.HistoryRecordQuery]: [];
|
[Queries.HistoryRecordQuery]: [];
|
||||||
[Queries.ConfigureAnalyticsQuery]: [];
|
[Queries.ConfigureAnalyticsQuery]: [];
|
||||||
[Queries.StageRecordQuery]: [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Image = {
|
export type Image = {
|
||||||
|
|
@ -130,6 +129,7 @@ export type PlayerWeapon = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type VsPlayer = {
|
export type VsPlayer = {
|
||||||
|
nameplate: Nameplate;
|
||||||
id: string;
|
id: string;
|
||||||
nameId: string | null;
|
nameId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -146,6 +146,7 @@ export type VsPlayer = {
|
||||||
} | null;
|
} | null;
|
||||||
paint: number;
|
paint: number;
|
||||||
crown: boolean;
|
crown: boolean;
|
||||||
|
festDragonCert: "NONE" | "DRAGON" | "DOUBLE_DRAGON";
|
||||||
|
|
||||||
headGear: PlayerGear;
|
headGear: PlayerGear;
|
||||||
clothingGear: PlayerGear;
|
clothingGear: PlayerGear;
|
||||||
|
|
@ -158,6 +159,11 @@ export type Color = {
|
||||||
r: number;
|
r: number;
|
||||||
};
|
};
|
||||||
export type VsTeam = {
|
export type VsTeam = {
|
||||||
|
festUniformName?: string;
|
||||||
|
festStreakWinCount?: number;
|
||||||
|
festUniformBonusRate?: number;
|
||||||
|
order: number;
|
||||||
|
judgement: string;
|
||||||
players: VsPlayer[];
|
players: VsPlayer[];
|
||||||
color: Color;
|
color: Color;
|
||||||
tricolorRole: null | "DEFENSE" | "ATTACK1" | "ATTACK2";
|
tricolorRole: null | "DEFENSE" | "ATTACK1" | "ATTACK2";
|
||||||
|
|
@ -165,6 +171,7 @@ export type VsTeam = {
|
||||||
result: null | {
|
result: null | {
|
||||||
paintRatio: null | number;
|
paintRatio: null | number;
|
||||||
score: null | number;
|
score: null | number;
|
||||||
|
noroshi: null | number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type VsRule =
|
export type VsRule =
|
||||||
|
|
@ -237,6 +244,9 @@ export type VsHistoryDetail = {
|
||||||
bankaraMatch: {
|
bankaraMatch: {
|
||||||
earnedUdemaePoint: null | number;
|
earnedUdemaePoint: null | number;
|
||||||
mode: "OPEN" | "CHALLENGE";
|
mode: "OPEN" | "CHALLENGE";
|
||||||
|
bankaraPower?: null | {
|
||||||
|
power?: null | number;
|
||||||
|
};
|
||||||
} | null;
|
} | null;
|
||||||
festMatch: {
|
festMatch: {
|
||||||
dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
|
dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
|
||||||
|
|
@ -268,6 +278,8 @@ export type CoopHistoryPlayerResult = {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
isMyself: boolean;
|
||||||
|
species: "INKLING" | "OCTOLING";
|
||||||
};
|
};
|
||||||
weapons: { name: string; image: Image | null }[];
|
weapons: { name: string; image: Image | null }[];
|
||||||
specialWeapon: null | {
|
specialWeapon: null | {
|
||||||
|
|
@ -372,9 +384,6 @@ export type GameExporter = {
|
||||||
) => Promise<string[]>;
|
) => Promise<string[]>;
|
||||||
exportGame: (game: Game) => Promise<ExportResult>;
|
exportGame: (game: Game) => Promise<ExportResult>;
|
||||||
exportSummary?: (summary: Summary) => Promise<ExportResult>;
|
exportSummary?: (summary: Summary) => Promise<ExportResult>;
|
||||||
exportStages?: (
|
|
||||||
stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"],
|
|
||||||
) => Promise<ExportResult>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BankaraBattleHistories = {
|
export type BankaraBattleHistories = {
|
||||||
|
|
@ -420,6 +429,11 @@ export type RespMap = {
|
||||||
};
|
};
|
||||||
[Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories;
|
[Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories;
|
||||||
[Queries.XBattleHistoriesQuery]: XBattleHistories;
|
[Queries.XBattleHistoriesQuery]: XBattleHistories;
|
||||||
|
[Queries.EventBattleHistoriesQuery]: {
|
||||||
|
eventBattleHistories: {
|
||||||
|
historyGroups: HistoryGroups<BattleListNode>;
|
||||||
|
};
|
||||||
|
};
|
||||||
[Queries.PrivateBattleHistoriesQuery]: {
|
[Queries.PrivateBattleHistoriesQuery]: {
|
||||||
privateBattleHistories: {
|
privateBattleHistories: {
|
||||||
historyGroups: HistoryGroups<BattleListNode>;
|
historyGroups: HistoryGroups<BattleListNode>;
|
||||||
|
|
@ -554,7 +568,6 @@ export type RespMap = {
|
||||||
xMatchMaxLf: SimpleXRank;
|
xMatchMaxLf: SimpleXRank;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
[Queries.StageRecordQuery]: splatNet3Types.StageRecordResult;
|
|
||||||
};
|
};
|
||||||
export type WeaponWithRatio = {
|
export type WeaponWithRatio = {
|
||||||
weapon: {
|
weapon: {
|
||||||
|
|
@ -607,10 +620,14 @@ export enum BattleListType {
|
||||||
Latest,
|
Latest,
|
||||||
Regular,
|
Regular,
|
||||||
Bankara,
|
Bankara,
|
||||||
|
Event,
|
||||||
|
XBattle,
|
||||||
Private,
|
Private,
|
||||||
Coop,
|
Coop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ListMethod = "latest" | "all" | "auto";
|
||||||
|
|
||||||
export type StatInkUuidList = {
|
export type StatInkUuidList = {
|
||||||
status: number;
|
status: number;
|
||||||
code: number;
|
code: number;
|
||||||
|
|
@ -630,7 +647,7 @@ export type StatInkWeapon = {
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export type StatInkGear = {
|
export type StatInkGear = {
|
||||||
primary_ability: string;
|
primary_ability: string | null;
|
||||||
secondary_abilities: (string | null)[];
|
secondary_abilities: (string | null)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -656,7 +673,9 @@ export type StatInkPlayer = {
|
||||||
special?: number;
|
special?: number;
|
||||||
gears?: StatInkGears;
|
gears?: StatInkGears;
|
||||||
crown?: "yes" | "no";
|
crown?: "yes" | "no";
|
||||||
|
crown_type?: "x" | "100x" | "333x";
|
||||||
disconnected: "yes" | "no";
|
disconnected: "yes" | "no";
|
||||||
|
species: "inkling" | "octoling";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatInkStage = {
|
export type StatInkStage = {
|
||||||
|
|
@ -706,6 +725,7 @@ export type StatInkCoopPlayer = {
|
||||||
rescued: number;
|
rescued: number;
|
||||||
defeat_boss: number;
|
defeat_boss: number;
|
||||||
disconnected: "yes" | "no";
|
disconnected: "yes" | "no";
|
||||||
|
species: "inkling" | "octoling";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatInkCoopBoss = {
|
export type StatInkCoopBoss = {
|
||||||
|
|
@ -815,6 +835,8 @@ export type StatInkPostBody = {
|
||||||
challenge_lose?: number;
|
challenge_lose?: number;
|
||||||
x_power_before?: number | null;
|
x_power_before?: number | null;
|
||||||
x_power_after?: number | null;
|
x_power_after?: number | null;
|
||||||
|
bankara_power_before?: number | null;
|
||||||
|
bankara_power_after?: number | null;
|
||||||
fest_power?: number; // Splatfest Power (Pro)
|
fest_power?: number; // Splatfest Power (Pro)
|
||||||
fest_dragon?:
|
fest_dragon?:
|
||||||
| "10x"
|
| "10x"
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ const COOP_ID =
|
||||||
|
|
||||||
Deno.test("gameId", async () => {
|
Deno.test("gameId", async () => {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await gameId(base64.encode(VS_ID)),
|
await gameId(base64.encodeBase64(VS_ID)),
|
||||||
"042bcac9-6b25-5d2e-a5ea-800939a6dea1",
|
"042bcac9-6b25-5d2e-a5ea-800939a6dea1",
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await gameId(base64.encode(COOP_ID)),
|
await gameId(base64.encodeBase64(COOP_ID)),
|
||||||
"58329d62-737d-5b43-ac22-e35e6e44b077",
|
"58329d62-737d-5b43-ac22-e35e6e44b077",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -22,7 +22,7 @@ Deno.test("gameId", async () => {
|
||||||
Deno.test("s3sCoopGameId", async () => {
|
Deno.test("s3sCoopGameId", async () => {
|
||||||
const S3S_COOP_UUID = "be4435b1-0ac5-577b-81bb-766585bec028";
|
const S3S_COOP_UUID = "be4435b1-0ac5-577b-81bb-766585bec028";
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await s3sCoopGameId(base64.encode(COOP_ID)),
|
await s3sCoopGameId(base64.encodeBase64(COOP_ID)),
|
||||||
S3S_COOP_UUID,
|
S3S_COOP_UUID,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
57
src/utils.ts
57
src/utils.ts
|
|
@ -6,29 +6,53 @@ import {
|
||||||
} from "./constant.ts";
|
} from "./constant.ts";
|
||||||
import { base64, uuid } from "../deps.ts";
|
import { base64, uuid } from "../deps.ts";
|
||||||
import { Env } from "./env.ts";
|
import { Env } from "./env.ts";
|
||||||
import { io } from "../deps.ts";
|
|
||||||
|
|
||||||
const stdinLines = io.readLines(Deno.stdin);
|
export async function* readLines(readable: ReadableStream<Uint8Array>) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
for await (const chunk of readable) {
|
||||||
|
buffer += decoder.decode(chunk, { stream: true });
|
||||||
|
let lineEndIndex;
|
||||||
|
|
||||||
|
while ((lineEndIndex = buffer.indexOf("\n")) !== -1) {
|
||||||
|
const line = buffer.slice(0, lineEndIndex).trim();
|
||||||
|
buffer = buffer.slice(lineEndIndex + 1);
|
||||||
|
|
||||||
|
yield line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.length > 0) {
|
||||||
|
yield buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdinLines = readLines(Deno.stdin.readable);
|
||||||
export async function readline(
|
export async function readline(
|
||||||
{ skipEmpty = true }: { skipEmpty?: boolean } = {},
|
{ skipEmpty = true }: { skipEmpty?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
for await (const line of stdinLines) {
|
while (true) {
|
||||||
|
const result = await stdinLines.next();
|
||||||
|
if (result.done) {
|
||||||
|
throw new Error("EOF");
|
||||||
|
}
|
||||||
|
const line = result.value;
|
||||||
if (!skipEmpty || line !== "") {
|
if (!skipEmpty || line !== "") {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("EOF");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function urlBase64Encode(data: ArrayBuffer) {
|
export function urlBase64Encode(data: ArrayBuffer) {
|
||||||
return base64.encode(data)
|
return base64.encodeBase64(data)
|
||||||
.replaceAll("+", "-")
|
.replaceAll("+", "-")
|
||||||
.replaceAll("/", "_")
|
.replaceAll("/", "_")
|
||||||
.replaceAll("=", "");
|
.replaceAll("=", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function urlBase64Decode(data: string) {
|
export function urlBase64Decode(data: string) {
|
||||||
return base64.decode(
|
return base64.encodeBase64(
|
||||||
data
|
data
|
||||||
.replaceAll("_", "/")
|
.replaceAll("_", "/")
|
||||||
.replaceAll("-", "+"),
|
.replaceAll("-", "+"),
|
||||||
|
|
@ -103,14 +127,14 @@ export function gameId(
|
||||||
);
|
);
|
||||||
return uuid.v5.generate(BATTLE_NAMESPACE, content);
|
return uuid.v5.generate(BATTLE_NAMESPACE, content);
|
||||||
} else if (parsed.type === "CoopHistoryDetail") {
|
} else if (parsed.type === "CoopHistoryDetail") {
|
||||||
return uuid.v5.generate(COOP_NAMESPACE, base64.decode(id));
|
return uuid.v5.generate(COOP_NAMESPACE, base64.decodeBase64(id));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown type");
|
throw new Error("Unknown type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function s3siGameId(id: string) {
|
export function s3siGameId(id: string) {
|
||||||
const fullId = base64.decode(id);
|
const fullId = base64.decodeBase64(id);
|
||||||
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
||||||
return uuid.v5.generate(S3SI_NAMESPACE, tsUuid);
|
return uuid.v5.generate(S3SI_NAMESPACE, tsUuid);
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +146,7 @@ export function s3siGameId(id: string) {
|
||||||
* @returns uuid used in stat.ink
|
* @returns uuid used in stat.ink
|
||||||
*/
|
*/
|
||||||
export function s3sCoopGameId(id: string) {
|
export function s3sCoopGameId(id: string) {
|
||||||
const fullId = base64.decode(id);
|
const fullId = base64.decodeBase64(id);
|
||||||
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
||||||
return uuid.v5.generate(COOP_NAMESPACE, tsUuid);
|
return uuid.v5.generate(COOP_NAMESPACE, tsUuid);
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +155,7 @@ export function s3sCoopGameId(id: string) {
|
||||||
* @param id VsHistoryDetail id or CoopHistoryDetail id
|
* @param id VsHistoryDetail id or CoopHistoryDetail id
|
||||||
*/
|
*/
|
||||||
export function parseHistoryDetailId(id: string) {
|
export function parseHistoryDetailId(id: string) {
|
||||||
const plainText = new TextDecoder().decode(base64.decode(id));
|
const plainText = new TextDecoder().decode(base64.decodeBase64(id));
|
||||||
|
|
||||||
const vsRE =
|
const vsRE =
|
||||||
/VsHistoryDetail-([a-z0-9-]+):(\w+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
|
/VsHistoryDetail-([a-z0-9-]+):(\w+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
|
||||||
|
|
@ -167,7 +191,7 @@ export const delay = (ms: number) =>
|
||||||
* Decode ID and get number after '-'
|
* Decode ID and get number after '-'
|
||||||
*/
|
*/
|
||||||
export function b64Number(id: string): number {
|
export function b64Number(id: string): number {
|
||||||
const text = new TextDecoder().decode(base64.decode(id));
|
const text = new TextDecoder().decode(base64.decodeBase64(id));
|
||||||
const [_, num] = text.split("-");
|
const [_, num] = text.split("-");
|
||||||
return parseInt(num);
|
return parseInt(num);
|
||||||
}
|
}
|
||||||
|
|
@ -188,3 +212,14 @@ export function urlSimplify(url: string): { pathname: string } | string {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const battleTime = (id: string) => {
|
||||||
|
const { timestamp } = parseHistoryDetailId(id);
|
||||||
|
|
||||||
|
const dateStr = timestamp.replace(
|
||||||
|
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
|
||||||
|
"$1-$2-$3T$4:$5:$6Z",
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Date(dateStr);
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue