Compare commits

..

28 Commits

Author SHA1 Message Date
Rosalina d5a31fdf85
add stage record query 2023-06-03 15:01:14 -04:00
Rosalina fcfa346969
Merge remote-tracking branch 'upstream/main' 2023-06-03 14:57:37 -04:00
Rosalina 5fd0dbbd14
Merge remote-tracking branch 'upstream/main' 2023-05-24 23:41:56 -04:00
Rosalina f04f88a02a
commit splashcat stuff 2023-05-24 23:41:09 -04:00
Rosalina 32d4586cce
fmt 2023-04-16 23:46:01 -04:00
Rosalina 261b19c40b
Merge remote-tracking branch 'upstream/main' 2023-04-16 15:06:48 -04:00
Rosalina f3085af9aa
Merge remote-tracking branch 'upstream/main' 2023-04-12 20:05:15 -04:00
Rosalina 61728e6838
bump version 2023-03-24 19:30:04 -04:00
Rosalina f66bba53d4
Merge from upstream and bump version. 2023-03-24 19:29:35 -04:00
Rosalina 009e87d4ab
add stage exporter 2023-03-07 13:33:59 -05:00
Rosalina 3f31bc7ea9
add stage exporting 2023-03-07 10:09:32 -05:00
Rosalina 93b360d5b2
write fixer script 2023-03-04 19:19:22 -05:00
Rosalina a7fa1541b2
fix types 2023-03-04 19:17:19 -05:00
Rosalina 0d647254e0
fix date field in splatnet data 2023-03-04 19:09:06 -05:00
Rosalina 499a9f8650
cleanup 2023-03-02 21:01:50 -05:00
Rosalina cdffe0278d
remove old check 2023-03-02 11:39:10 -05:00
Rosalina 169526974b
switch name to better reflect its for splashcat 2023-03-01 19:14:43 -05:00
Rosalina 7aa98e1905
Merge remote-tracking branch 'upstream/main' 2023-03-01 09:27:59 -05:00
Rosalina 51f9f80cc2
update dependencies 2023-03-01 09:08:18 -05:00
Rosalina 7e46187d75
add mongodb to help message 2023-03-01 09:08:02 -05:00
Rosalina c2fe3007e5
add mongodb migrations for older battles 2023-03-01 09:07:45 -05:00
Rosalina 3e1a90dc45
add summary exporter and fix notExported check 2023-03-01 09:06:48 -05:00
Rosalina eb91b8a171
use combined version for stat.ink 2023-03-01 09:06:21 -05:00
Rosalina c1aa9e397a
add export game function 2023-02-28 11:34:07 -05:00
Rosalina 96776b20c8
Merge remote-tracking branch 'upstream/main' 2023-02-28 11:08:27 -05:00
Rosalina 3064abd454
add early MongoDB exporter
mongodb exporter can now identify what already exists and is compatible with the previous format of splashcat's db.
2023-02-27 23:52:40 -05:00
Rosalina da92cb9382
Merge remote-tracking branch 'origin/main' 2023-02-27 23:08:34 -05:00
Rosalina 2e782ccaa2
add mongodb exporter and include extra details in the useragent
i am unsure if/how the agent is used by stat.ink for statistics. because of this, i am using a custom agent here to clearly show that this is a fork from s3s, intended for uploading to mongodb for my website.
2023-02-27 23:06:04 -05:00
64 changed files with 4752 additions and 3432 deletions

View File

@ -7,7 +7,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
deno: [1.x, "1.37.x", canary]
deno: [1.x, "1.31.x", canary]
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
@ -15,10 +15,8 @@ jobs:
deno-version: ${{ matrix.deno }}
- name: Check fmt
run: deno fmt --check
if: ${{ matrix.deno != '1.31.x' }}
- name: Run lint
run: deno lint
if: ${{ matrix.deno != '1.31.x' }}
- name: All entries
uses: tj-actions/glob@v16
id: entries

22
.github/workflows/constant-check.yaml vendored Normal file
View File

@ -0,0 +1,22 @@
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

View File

@ -27,9 +27,9 @@ jobs:
with:
deno-version: 1.x
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v2
with:
version: 8.11.0
version: 7.29.1
- name: Sync node version and setup cache
uses: actions/setup-node@v3

View File

@ -1,92 +1,3 @@
## 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
feat: add support for Challenges

View File

@ -3,7 +3,7 @@
[![Build status](https://github.com/spacemeowx2/s3si.ts/workflows/Build/badge.svg)](https://github.com/spacemeowx2/s3si.ts/actions/workflows/ci.yaml)
[![Constant check status](https://github.com/spacemeowx2/s3si.ts/workflows/Constant%20Check/badge.svg)](https://github.com/spacemeowx2/s3si.ts/actions/workflows/constant-check.yaml)
Export your battles from SplatNet to stat.ink and Splashcat.
Export your battles from SplatNet to stat.ink.
If you have used s3s, please see [here](#migrate-from-s3s).
@ -19,18 +19,13 @@ Options:
--profile-path <path>, -p Path to config file (default: ./profile.json)
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
Multiple exporters can be separated by commas
(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.
(e.g. "stat.ink,file")
--no-progress, -n Disable progress bar
--monitor, -m Monitor mode
--skip-mode <mode>, -s Skip mode (default: null)
("vs", "coop")
--with-summary Include summary in the output
--help Show this help message and exit
--nxapi-presence Extends monitoring mode to use Nintendo Switch presence from nxapi
--help Show this help message and exit`,
```
3. If it's your first time running this, follow the instructions to login to
@ -39,12 +34,6 @@ Options:
- If you want to use a different profile, use `-p` to specify the path to the
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
- Run
@ -78,8 +67,7 @@ results into the user's language when displayed.
// userLang will effect the language of the exported games to stat.ink
"userLang": "zh-CN",
"userCountry": "JP",
"statInkApiKey": "...",
"splashcatApiKey": "..."
"statInkApiKey": "..."
}
```

329
deno.lock
View File

@ -1,167 +1,46 @@
{
"version": "3",
"version": "2",
"remote": {
"https://deno.land/std@0.210.0/fmt/colors.ts": "2685c524bef9b16b3059a417daf6860c754eb755e19e812762ef5dff62f24481",
"https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
"https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
"https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
"https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
"https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
"https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
"https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
"https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
"https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
"https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
"https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
"https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
"https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
"https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
"https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c",
"https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
"https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
"https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
"https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
"https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
"https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
"https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
"https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
"https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
"https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
"https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
"https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
"https://deno.land/std@0.213.0/bytes/concat.ts": "9cac3b4376afbef98ff03588eb3cf948e0d1eb6c27cfe81a7651ab6dd3adc54a",
"https://deno.land/std@0.213.0/bytes/copy.ts": "f29c03168853720dfe82eaa57793d0b9e3543ebfe5306684182f0f1e3bfd422a",
"https://deno.land/std@0.213.0/crypto/_fnv/fnv32.ts": "ba2c5ef976b9f047d7ce2d33dfe18671afc75154bcf20ef89d932b2fe8820535",
"https://deno.land/std@0.213.0/crypto/_fnv/fnv64.ts": "580cadfe2ff333fe253d15df450f927c8ac7e408b704547be26aab41b5772558",
"https://deno.land/std@0.213.0/crypto/_fnv/mod.ts": "8dbb60f062a6e77b82f7a62ac11fabfba52c3cd408c21916b130d8f57a880f96",
"https://deno.land/std@0.213.0/crypto/_fnv/util.ts": "27b36ce3440d0a180af6bf1cfc2c326f68823288540a354dc1d636b781b9b75f",
"https://deno.land/std@0.213.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "76c727912539737def4549bb62a96897f37eb334b979f49c57b8af7a1617635e",
"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/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/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/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/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",
@ -178,18 +57,128 @@
"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.4.5/deps.ts": "f2886f3f87af20b397ffcf9723a0fabc5893491ce9ce7615a37b1d7a38539247",
"https://deno.land/x/progress@v1.4.5/mod.ts": "e26996fb5f23863c3402133896a9739ea4059b155a3d89ba207cad10b50524ea",
"https://deno.land/x/progress@v1.4.5/multi.ts": "bf50eff76d4c1b1b1a3118e73a58631b19b0b30e0dd4166ae4ef0886efae88a5",
"https://deno.land/x/progress@v1.4.5/time.ts": "001198ff9fe2a452830515fc944665c4369990102978b325e1c9094486cfd8ab",
"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/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.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
View File

@ -2,14 +2,17 @@ export {
Cookie,
CookieJar,
wrapFetch,
} from "https://deno.land/x/another_cookiejar@v5.0.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.213.0/encoding/base64.ts";
export * as flags from "https://deno.land/std@0.213.0/flags/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.213.0/uuid/mod.ts";
} from "https://deno.land/x/another_cookiejar@v4.1.4/mod.ts";
export type { CookieOptions } from "https://deno.land/x/another_cookiejar@v4.1.4/mod.ts";
export * as base64 from "https://deno.land/std@0.160.0/encoding/base64.ts";
export * as flags from "https://deno.land/std@0.160.0/flags/mod.ts";
export * as io from "https://deno.land/std@0.160.0/io/mod.ts";
export * as uuid from "https://deno.land/std@0.160.0/uuid/mod.ts";
export * as msgpack from "https://deno.land/x/msgpack@v1.4/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.4.5/mod.ts";
export { Mutex } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";
export * as path from "https://deno.land/std@0.160.0/path/mod.ts";
export { MultiProgressBar } from "https://deno.land/x/progress@v1.2.8/mod.ts";
export { Mutex } from "https://deno.land/x/semaphore@v1.1.1/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";

View File

@ -1 +1 @@
export { assertEquals } from "https://deno.land/std@0.213.0/assert/mod.ts";
export { assertEquals } from "https://deno.land/std@0.160.0/testing/asserts.ts";

View File

@ -1,12 +0,0 @@
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'],
}

View File

@ -1,7 +1,7 @@
# s3si.ts GUI
# Tauri + React + Typescript
## Development
This template should help get you started developing with Tauri, React and Typescript in Vite.
```
pnpm tauri dev
```
## Recommended IDE Setup
- [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)

View File

@ -11,39 +11,50 @@
"lint": "eslint --max-warnings=0 src"
},
"dependencies": {
"@tauri-apps/api": "^1.5.3",
"clsx": "^2.1.0",
"daisyui": "^4.6.1",
"i18next": "^23.8.1",
"i18next-browser-languagedetector": "^7.2.0",
"@tauri-apps/api": "^1.3.0",
"classnames": "^2.3.2",
"daisyui": "^2.52.0",
"i18next": "^22.5.0",
"i18next-browser-languagedetector": "^7.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
"react-i18next": "^14.0.1",
"react-icons": "^5.0.1",
"react-router-dom": "^6.21.3",
"react-use": "^17.5.0",
"swr": "^2.2.4"
"react-i18next": "^12.3.1",
"react-icons": "^4.9.0",
"react-router-dom": "^6.11.2",
"react-use": "^17.4.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.9",
"@types/node": "^20.11.10",
"@tauri-apps/cli": "^1.3.1",
"@types/node": "^20.2.5",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@typescript-eslint/typescript-estree": "^6.20.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"i18next-http-backend": "^2.4.2",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.41.0",
"eslint-config-react-app": "^7.0.1",
"i18next-http-backend": "^2.2.1",
"postcss": "^8.4.24",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.4",
"vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.3.1"
"vite-tsconfig-paths": "^4.2.0"
},
"eslintConfig": {
"extends": "react-app"
},
"pnpm": {
"packageExtensions": {
"eslint-plugin-flowtype": {
"peerDependenciesMeta": {
"@babel/plugin-syntax-flow": {
"optional": true
},
"@babel/plugin-transform-react-jsx": {
"optional": true
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

802
gui/src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,22 +10,14 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.1", features = [] }
tauri-build = { version = "1.2", features = [] }
[dependencies]
tauri = { version = "1.5.4", features = [
"fs-all",
"path-all",
"process-relaunch",
"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"
tauri = { version = "1.2", features = ["fs-all", "path-all", "process-relaunch", "shell-execute", "shell-open", "shell-sidecar", "window-all"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["time"] }
urlencoding = "2.1.2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem

View File

@ -15,12 +15,14 @@ function onSelectUserClick(e) {
}
e.preventDefault();
// a little official way...
window.__TAURI_INVOKE__({
// very hacky way...
window.ipc.postMessage(JSON.stringify({
"cmd":"tauri",
"callback":0,
"error":0,
"__tauriModule":"Event",
"cmd": "tauri",
"message":{"cmd":"emit","event":"login","payload":{"url":element.href}}
})
}))
}
function detectAndInject() {
const element = document.getElementById('authorize-switch-approval-link');
@ -72,7 +74,12 @@ document.addEventListener("DOMContentLoaded", () => {{
#[tauri::command]
async fn open_login_window(app: tauri::AppHandle, url: String) -> Option<String> {
let window = WindowBuilder::new(&app, "login", tauri::WindowUrl::App(url.into()))
let encoded = urlencoding::encode(&url);
let window = WindowBuilder::new(
&app,
"login",
tauri::WindowUrl::App(format!("/redirect?url={encoded}").into()),
)
.title("Login")
.center()
.inner_size(1040.0, 960.0)

View File

@ -1,5 +1,4 @@
{
"$schema": "https://github.com/tauri-apps/tauri/raw/tauri-v1.4.1/core/tauri-config-schema/schema.json",
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
@ -9,7 +8,7 @@
},
"package": {
"productName": "s3si-ts",
"version": "0.4.20"
"version": "0.4.1"
},
"tauri": {
"allowlist": {
@ -70,16 +69,7 @@
]
},
"security": {
"csp": null,
"dangerousRemoteDomainIpcAccess": [
{
"windows": [
"login"
],
"domain": "accounts.nintendo.com",
"enableTauriAPI": true
}
]
"csp": null
},
"updater": {
"active": false,

View File

@ -4,21 +4,20 @@ import { Layout } from "components/Layout";
import { Home } from "pages/Home";
import { Settings } from "pages/Settings";
import { Guide } from 'pages/Guide';
import { RedirectLogin } from 'pages/RedirectLogin';
import { useShowWindow } from 'hooks/useShowWindow';
import { AppContextProvider } from 'context/app';
function App() {
useShowWindow();
return (
<AppContextProvider>
<Routes>
<Route path='/' element={<Layout />}>
<Route index element={<Home />} />
<Route path='/settings' element={<Settings />} />
<Route path='/guide' element={<Guide />} />
<Route path='/redirect' element={<RedirectLogin />} />
</Route>
</Routes>
</AppContextProvider>
);
}

View File

@ -11,6 +11,6 @@ export const CheckUpdate: React.FC<{ className?: string, children?: ReactNode }>
}
return <>
<button type='button' className={className} onClick={onClick}>{children}</button>
<button className={className} onClick={onClick}>{children}</button>
</>;
}

View File

@ -1,10 +1,9 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AiOutlineWarning } from 'react-icons/ai'
import { FallbackProps } from 'react-error-boundary'
type ErrorContentProps = {
error: unknown
error: any
retry?: () => void
}
@ -19,14 +18,9 @@ export const ErrorContent: React.FC<ErrorContentProps> = ({ error, retry }) => {
<span className='inline-flex items-center'>
<AiOutlineWarning className='inline-block scale-[2] mr-4 justify-end flex-none' />
<div className='max-w-full break-all'>
<div>{t('发生了错误')}{retry && <button type='button' className='link link-info ml-1'>{t('重试')}</button>}</div>
<div>{t('发生了错误')}{retry && <button className='link link-info ml-1'>{t('重试')}</button>}</div>
{String(error)}
</div>
</span>
</div>
}
export const FallbackComponent: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
console.error('FallbackComponent', error)
return <ErrorContent error={error} retry={resetErrorBoundary} />
}

View File

@ -9,6 +9,6 @@ type HeaderProps = {
export const Header: React.FC<HeaderProps> = ({ title }) => {
const navigate = useNavigate();
return <>
<h2 className="card-title" data-tauri-drag-region><button type='button' onClick={() => navigate(-1)}><AiOutlineLeft /></button>{title}</h2>
<h2 className="card-title" data-tauri-drag-region><button onClick={() => navigate(-1)}><AiOutlineLeft /></button>{title}</h2>
</>
}

View File

@ -1,8 +1,10 @@
import { invoke } from '@tauri-apps/api';
import clsx from 'clsx';
import { useService, useServiceMutation } from 'services/useService';
import classNames from 'classnames';
import { usePromise } from 'hooks/usePromise';
import React, { useState } from 'react'
import { getConfig, getProfile, setProfile } from 'services/config';
import { ensureTokenValid } from 'services/s3si';
import { composeLoadable } from 'utils/composeLoadable';
import { ErrorContent } from './ErrorContent';
type OpenSplatnetProps = {
@ -10,29 +12,30 @@ type OpenSplatnetProps = {
}
export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
const profileResult = useService('profile', 0)
const { trigger: setProfile } = useServiceMutation('profile', 0)
let { loading, error, retry, result } = composeLoadable({
config: usePromise(getConfig),
profile: usePromise(() => getProfile(0)),
});
const [doing, setDoing] = useState(false);
const [err, setError] = useState<unknown>();
const [err, setError] = useState<any>();
const onClick = async () => {
setDoing(true);
try {
if (!profileResult.data) {
if (!result) {
return;
}
const state = profileResult.data.state;
const state = result.profile.state;
const newState = await ensureTokenValid(state);
await setProfile({
...profileResult.data,
await setProfile(0, {
...result.profile,
state: newState,
});
retry?.();
const gtoken = newState.loginState?.gToken;
await invoke('open_splatnet', {
gtoken,
lang: profileResult.data.state.userLang,
lang: result.profile.state.userLang,
});
} catch (e) {
setError(e);
@ -42,21 +45,16 @@ export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
};
if (err) {
if (error || err) {
return <>
<ErrorContent error={err} />
<ErrorContent error={error || err} retry={retry} />
</>
}
const btnLoading = profileResult.isLoading || doing;
return <>
<button
type='button'
className={clsx('btn w-full', {
'btn-disabled': !profileResult.data?.state?.loginState?.sessionToken,
})}
onClick={onClick}
disabled={btnLoading}
>{btnLoading ? <span className='loading' /> : children}</button>
<button className={classNames('btn', {
'btn-disabled': !result?.profile.state.loginState?.sessionToken,
'loading': loading || doing,
})} onClick={onClick}>{children}</button>
</>
}

View File

@ -1,39 +1,71 @@
import clsx from 'clsx';
import classNames from 'classnames';
import { usePromise } from 'hooks/usePromise';
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next';
import { useLog } from 'services/s3si';
import { canExport, getProfile, setProfile } from 'services/config';
import { addLog, run, useLog } from 'services/s3si';
import { Checkbox } from './Checkbox';
import { Loading } from './Loading';
import { useService } from 'services/useService';
import { useAppContext } from 'context/app'
type RunPanelProps = Record<string, never>
type RunPanelProps = {
}
export const RunPanel: React.FC<RunPanelProps> = () => {
const { t } = useTranslation();
const { data: result } = useService('profile', 0)
const { result } = usePromise(() => getProfile(0));
const [exportBattle, setExportBattle] = useState(true);
const [exportCoop, setExportCoop] = useState(true);
const { exports } = useAppContext()
const disabled = !exports
const isExporting = exports?.isExporting ?? false
const [loading, setLoading] = useState(false);
if (!result) {
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 <>
<div className="tooltip" data-tip={disabled ? t('请先在设置中完成Nintendo Account登录和stat.ink的API密钥') : undefined}>
<Checkbox disabled={disabled || isExporting} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
<Checkbox disabled={disabled || isExporting} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
<Checkbox disabled={disabled || loading} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
<button
type='button'
onClick={() => exports?.trigger({ exportBattle, exportCoop })}
className={clsx('btn btn-primary w-full', {
onClick={onClick}
className={classNames('btn w-full', {
'btn-disabled': disabled || (!exportBattle && !exportCoop),
'loading': loading,
})}
disabled={isExporting}
>{isExporting ? <span className='loading' /> : t('导出')}</button>
>{t('导出')}</button>
</div>
</>
}

View File

@ -1,81 +0,0 @@
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>
}

View File

@ -0,0 +1,84 @@
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;
}

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
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 DotField<T extends Maybe<Record<string, any>>, K = KeyOf<NonNullable<T>>> = K extends string

View File

@ -21,7 +21,7 @@ export class JSONRPCClient<S extends Service> {
protected transport: Transport;
protected requestMap: Map<
ID,
(result: RPCResult<unknown, ResponseError>) => void
(result: RPCResult<any, ResponseError>) => void
> = new Map();
protected fatal: unknown = undefined;
protected task: Promise<void>;
@ -55,7 +55,6 @@ export class JSONRPCClient<S extends Service> {
// receive response from server
protected async run() {
try {
// eslint-disable-next-line no-constant-condition
while (true) {
const data = await this.transport.recv();
if (data === undefined) {
@ -112,7 +111,7 @@ export class JSONRPCClient<S extends Service> {
if (result.error) {
rej(new JSONRPCError(result.error));
} else {
res(result.result as R);
res(result.result);
}
});
});
@ -121,7 +120,6 @@ export class JSONRPCClient<S extends Service> {
getProxy(): S {
const proxy = new Proxy({}, {
get: (_, method: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (...params: unknown[]) => this.call(method, ...params as any);
},
});

View File

@ -2,17 +2,40 @@
@tailwind components;
@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 {
width: 100vw;
height: 100vh;
}
#root {
width: 100vw;
height: 100vh;
overflow: hidden
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
}
/* custom classes */
.flex-auto-all > * {

View File

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'classnames';
import { Header } from 'components/Header';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -30,16 +30,14 @@ const Steps: React.FC<{ steps: Step[], className?: string }> = ({ className, ste
{Content && <Content onChange={setState} />}
<div className='mt-4 flex gap-2'>
<button
type='button'
onClick={() => setStep(s => s - 1)}
className={clsx('btn', {
className={classNames('btn', {
'btn-disabled': !hasPrev || !state.prev,
})}
>{t('上一步')}</button>
<button
type='button'
onClick={() => setStep(s => s + 1)}
className={clsx('btn', {
className={classNames('btn', {
'btn-disabled': !hasNext || !state.next,
})}
>{t('下一步')}</button>
@ -51,11 +49,7 @@ const LoginNintendoAccount: React.FC<{ onChange: (v: StepState) => void }> = ({
const { t } = useTranslation();
return <div className='my-3'>
<button
type='button'
className='btn'
onClick={() => onChange({ next: true, prev: true })}
>{t('点击登录')}</button>
<button className='btn' onClick={() => onChange({ next: true, prev: true })}>{t('点击登录')}</button>
</div>
}

View File

@ -1,19 +1,14 @@
import { OpenSplatnet } from 'components/OpenSplatnet';
import { LogPanel, RunPanel } from 'components/RunPanel';
import { STAT_INK } from 'constant';
import React, { Suspense } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { FallbackComponent } from 'components/ErrorContent';
import { Loading } from 'components/Loading';
import { Link } from "react-router-dom";
export const Home: React.FC = () => {
const { t } = useTranslation();
return <ErrorBoundary FallbackComponent={FallbackComponent}>
<Suspense fallback={<Loading />}>
<div className='flex p-2 w-full h-full gap-2'>
return <div className='flex p-2 w-full h-full gap-2'>
<div className='max-w-full h-full md:max-w-sm flex-auto'>
<div className='flex flex-col gap-2 h-full'>
<LogPanel className='sm:hidden flex-auto' />
@ -21,12 +16,10 @@ export const Home: React.FC = () => {
<Link to='/settings' className='btn'>{t('设置')}</Link>
<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>
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
</div>
</div>
</div>
<LogPanel className='hidden sm:block flex-1' />
</div>
</Suspense>
</ErrorBoundary>
}

View File

@ -0,0 +1,25 @@
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>
}

View File

@ -1,17 +1,16 @@
import { ErrorContent, FallbackComponent } from 'components/ErrorContent';
import { ErrorContent } from 'components/ErrorContent';
import { Loading } from 'components/Loading';
import React, { Suspense, useState } from 'react'
import { usePromise, usePromiseLazy } from 'hooks/usePromise';
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next';
import { Config, Profile } from 'services/config';
import clsx from 'clsx';
import { Config, getConfig, getProfile, Profile, setConfig, setProfile } from 'services/config';
import { composeLoadable } from 'utils/composeLoadable';
import classNames from 'classnames';
import { useLogin } from 'services/s3si';
import { STAT_INK } from 'constant';
import { Header } from 'components/Header';
import { useSubField } from 'hooks/useSubField';
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;
@ -58,8 +57,6 @@ const Form: React.FC<{
const { t, i18n } = useTranslation();
const [value, setValue] = useState(oldValue);
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);
@ -67,12 +64,12 @@ const Form: React.FC<{
const statInkApiKey = subField('profile.state.statInkApiKey')
const splatnet3Lang = subField('profile.state.userLang')
const { trigger: onSave, isMutating: loading, error } = useSWRMutation('saveSettings', async () => {
await setProfile(value.profile);
const [onSave, { loading, error }] = usePromiseLazy(async () => {
await setProfile(0, value.profile);
await setConfig(value.config);
onSaved?.();
})
const loginState = useSWRMutation('login', async () => {
const [onLogin, loginState] = usePromiseLazy(async () => {
const result = await login();
if (!result) {
return;
@ -88,12 +85,11 @@ const Form: React.FC<{
<label className="label">
<span className="label-text">{t('Nintendo Account 会话令牌')}</span>
<span className="label-text-alt"><button
type='button'
className={clsx('link', {
loading: loginState.isMutating,
className={classNames('link', {
loading: loginState.loading,
})}
onClick={() => loginState.trigger()}
disabled={loginState.isMutating}
onClick={onLogin}
disabled={loginState.loading}
>{t('网页登录')}</button></span>
</label>
<input
@ -117,7 +113,7 @@ const Form: React.FC<{
</label>
<div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}>
<input
className={clsx("input input-bordered w-full", {
className={classNames("input input-bordered w-full", {
'input-error': statInkKeyError,
})}
type="text"
@ -152,49 +148,38 @@ const Form: React.FC<{
</div>
<ErrorContent error={error} />
<div className='flex gap-4 max-w-md justify-between flex-auto-all'>
<div className='tooltip' data-tip={changed ? undefined : t('没有更改')}>
<button
type='button'
className={clsx('btn btn-primary w-full', {
<div className="tooltip" data-tip={changed ? undefined : t('没有更改')}>
<button className={classNames('btn btn-primary w-full', {
loading,
})}
onClick={() => onSave()}
disabled={!changed || statInkKeyError}
>{t('保存')}</button>
})} onClick={onSave} disabled={!changed || statInkKeyError}>{t('保存')}</button>
</div>
<button
type='button'
className={clsx('btn', {
<button className={classNames('btn', {
loading,
})}
onClick={() => setValue(oldValue)}
>{t('重置')}</button>
})} onClick={() => setValue(oldValue)}>{t('重置')}</button>
</div>
</>
}
const SettingsLoader: React.FC = () => {
const navigate = useNavigate();
const { data: config } = useService('config')
const { data: profile } = useService('profile', 0)
if (!config || !profile) {
return <>
Error
</>
}
return <>
<Form oldValue={{ config, profile }} onSaved={() => navigate(-1)} />
</>
}
export const Settings: React.FC = () => {
const navigate = useNavigate();
let { loading, error, retry, result } = composeLoadable({
config: usePromise(getConfig),
profile: usePromise(() => getProfile(0)),
});
if (loading) {
return <Page>
<ErrorBoundary FallbackComponent={FallbackComponent}>
<Suspense fallback={<div className='h-full flex items-center justify-center'><Loading /></div>}>
<SettingsLoader />
</Suspense>
</ErrorBoundary>
<div className='h-full flex items-center justify-center'><Loading /></div>
</Page>
}
if (error) {
return <Page>
<ErrorContent error={error} retry={retry} />
</Page>
}
return <Page>
{result && <Form oldValue={result} onSaved={() => navigate(-1)} />}
</Page>
}

View File

@ -1,6 +1,6 @@
import { fs } from "@tauri-apps/api"
import { appConfigDir, join } from '@tauri-apps/api/path'
import type { State } from '../../../src/state';
import { State } from '../../../src/state';
const configFile = appConfigDir().then(c => join(c, 'config.json'));
const profileDir = appConfigDir().then(c => join(c, 'profile'));
@ -9,7 +9,8 @@ export type Profile = {
state: State,
}
export type Config = Record<string, never>
export type Config = {
}
// TODO: import from state.ts.
const DEFAULT_STATE: State = {

View File

@ -9,7 +9,6 @@ const client = new JSONRPCClient<S3SIService>({
const LOG_SUB = new Set<(logs: Log[]) => void>();
async function getLogs() {
// eslint-disable-next-line no-constant-condition
while (true) {
const r = await client.getLogs()
@ -58,7 +57,7 @@ export const useLog = () => {
return useContext(LOG_CONTEXT);
}
function renderMsg(i: unknown) {
function renderMsg(i: any) {
if (i instanceof Error) {
return i.message
}
@ -92,15 +91,14 @@ export const LogProvider: React.FC<{ limit?: number, children?: React.ReactNode
LOG_SUB.delete(cb);
}
}, [limit])
const value = useMemo(() => {
const renderedLogs = logs.map(renderLog)
return {
const renderedLogs = useMemo(() => logs.map(renderLog), [logs])
return <LOG_CONTEXT.Provider value={{
logs,
renderedLogs,
}
}, [logs])
return <LOG_CONTEXT.Provider value={value}>
}}>
{children}
</LOG_CONTEXT.Provider>
}

View File

@ -1,37 +0,0 @@
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))
}

View File

@ -0,0 +1,19 @@
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
View File

@ -4,13 +4,7 @@ import { flags } from "./deps.ts";
const parseArgs = (args: string[]) => {
const parsed = flags.parse(args, {
string: [
"profilePath",
"exporter",
"skipMode",
"listMethod",
"nxapiPresenceUrl",
],
string: ["profilePath", "exporter", "skipMode"],
boolean: ["help", "noProgress", "monitor", "withSummary"],
alias: {
"help": "h",
@ -20,8 +14,7 @@ const parseArgs = (args: string[]) => {
"monitor": ["m"],
"skipMode": ["s", "skip-mode"],
"withSummary": "with-summary",
"listMethod": "list-method",
"nxapiPresenceUrl": ["nxapi-presence"],
"withStages": "with-stages",
},
});
return parsed;
@ -36,18 +29,14 @@ Options:
--profile-path <path>, -p Path to config file (default: ./profile.json)
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
Multiple exporters can be separated by commas
(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.
(e.g. "stat.ink,file,mongodb")
--no-progress, -n Disable progress bar
--monitor, -m Monitor mode
--skip-mode <mode>, -s Skip mode (default: null)
("vs", "coop")
--with-summary Include summary in the output
--help Show this help message and exit
--nxapi-presence Extends monitoring mode to use Nintendo Switch presence from nxapi`,
--with-stages Include stage records in the output
--help Show this help message and exit`,
);
Deno.exit(0);
}

View File

@ -1,4 +1,4 @@
import * as path from "https://deno.land/std@0.213.0/path/mod.ts";
import * as path from "https://deno.land/std@0.178.0/path/mod.ts";
import { assertEquals } from "../dev_deps.ts";
if (import.meta.main) {
@ -39,8 +39,6 @@ if (import.meta.main) {
target,
"code:",
status.code,
"stderr:",
new TextDecoder().decode(status.stderr),
);
Deno.exit(status.code);
}

View File

@ -0,0 +1,49 @@
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
scripts/deno.json Normal file
View File

@ -0,0 +1 @@
{}

187
scripts/deno.lock Normal file
View File

@ -0,0 +1,187 @@
{
"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"
}
}
}
}
}

View File

@ -23,7 +23,7 @@ function encryptKey(uid: string) {
hasher.hash(uid);
const hash = hasher.result();
const key = hash & 0xff;
const encrypted = base64.encodeBase64(
const encrypted = base64.encode(
new TextEncoder().encode(uid).map((i) => i ^ key),
);
return {

View File

@ -20,7 +20,7 @@ for (const file of files) {
const content: FileExporterType = JSON.parse(await Deno.readTextFile(file));
if (content.type === "SUMMARY") continue;
const id = content.data.detail.id;
const rawId = base64.decodeBase64(id);
const rawId = base64.decode(id);
const uuid = new TextDecoder().decode(rawId.slice(rawId.length - 36));
if (ids.has(uuid)) {
console.log(

View File

@ -0,0 +1,47 @@
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!");

View File

@ -0,0 +1,77 @@
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)));
}

View File

@ -22,7 +22,7 @@ class TestRankTracker extends RankTracker {
}
function genId(id: number, date = "20220101"): string {
return base64.encodeBase64(
return base64.encode(
`VsHistoryDetail-asdf:asdf:${date}T${
id.toString().padStart(6, "0")
}_------------------------------------`,

View File

@ -5,7 +5,7 @@ import {
HistoryGroups,
RankParam,
} from "./types.ts";
import { battleTime, gameId } from "./utils.ts";
import { gameId, parseHistoryDetailId } from "./utils.ts";
import { getSeason } from "./VersionData.ts";
const splusParams = () => {
@ -193,6 +193,17 @@ 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 = {
id: string;
gameId: string;
@ -347,10 +358,6 @@ export class RankTracker {
async updateState(
history: HistoryGroups<BattleListNode>["nodes"],
) {
if (history.length === 0) {
return;
}
// history order by time. 0 is the oldest.
const flatten: FlattenItem[] = await Promise.all(
history

View File

@ -30,66 +30,6 @@ export const SEASONS: Season[] = [
start: new Date("2023-06-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 => {

View File

@ -1,17 +1,15 @@
import { loginManually } from "./iksm.ts";
import { MultiProgressBar, Mutex } from "../deps.ts";
import { MultiProgressBar } from "../deps.ts";
import { FileStateBackend, Profile, StateBackend } from "./state.ts";
import { Splatnet3 } from "./splatnet3.ts";
import { BattleListType, Game, GameExporter, ListMethod } from "./types.ts";
import { BattleListType, Game, GameExporter } from "./types.ts";
import { Cache, FileCache } from "./cache.ts";
import { StatInkExporter } from "./exporters/stat.ink.ts";
import { FileExporter } from "./exporters/file.ts";
import { delay, showError } from "./utils.ts";
import { GameFetcher } from "./GameFetcher.ts";
import { DEFAULT_ENV, Env } from "./env.ts";
import { SplashcatExporter } from "./exporters/splashcat.ts";
import { SPLATOON3_TITLE_ID } from "./constant.ts";
import { USERAGENT } from "./constant.ts";
import { MongoDBExporter } from "./exporters/mongodb.ts";
export type Opts = {
profilePath: string;
@ -19,12 +17,11 @@ export type Opts = {
noProgress: boolean;
monitor: boolean;
withSummary: boolean;
withStages: boolean;
skipMode?: string;
listMethod?: string;
cache?: Cache;
stateBackend?: StateBackend;
env: Env;
nxapiPresenceUrl?: string;
};
export const DEFAULT_OPTS: Opts = {
@ -33,7 +30,7 @@ export const DEFAULT_OPTS: Opts = {
noProgress: false,
monitor: false,
withSummary: false,
listMethod: "auto",
withStages: true,
env: DEFAULT_ENV,
};
@ -58,103 +55,6 @@ 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 {
return {
total,
@ -166,7 +66,6 @@ function progress({ total, currentUrl, done }: StepProgress): Progress {
export class App {
profile: Profile;
env: Env;
splatoon3PreviouslyActive = false;
constructor(public opts: Opts) {
const stateBackend = opts.stateBackend ??
@ -176,12 +75,6 @@ export class App {
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")[] {
@ -225,26 +118,22 @@ export class App {
out.push(new FileExporter(state.fileExportPath));
}
if (exporters.includes("splashcat")) {
if (!state.splashcatApiKey) {
const key = (await this.env.prompts.prompt(
"Splashcat API key is not set. Please enter below.",
if (exporters.includes("mongodb")) {
if (!state.mongoDbUri) {
const uri = (await this.env.prompts.prompt(
"MongoDB URI is not set. Please enter below.",
)).trim();
if (!key) {
this.env.logger.error("API key is required.");
if (!uri) {
this.env.logger.error("MongoDB URI is required.");
Deno.exit(1);
}
await this.profile.writeState({
...state,
splashcatApiKey: key,
mongoDbUri: uri,
});
}
out.push(
new SplashcatExporter({
splashcatApiKey: this.profile.state.splashcatApiKey!,
uploadMode: this.opts.monitor ? "Monitoring" : "Manual",
env: this.env,
}),
new MongoDBExporter(this.profile.state.mongoDbUri!),
);
}
@ -275,13 +164,11 @@ export class App {
);
}
};
const end = () => bar?.end();
return {
redraw,
end,
[Symbol.dispose]: end,
const endBar = () => {
bar?.end();
};
return { redraw, endBar };
}
private async exportOnce() {
const splatnet = new Splatnet3({ profile: this.profile, env: this.env });
@ -297,12 +184,10 @@ export class App {
if (skipMode.includes("vs") || exporters.length === 0) {
this.env.logger.log("Skip exporting VS games.");
} else {
const gameListFetcher = new BattleListFetcher(
this.opts.listMethod ?? "auto",
splatnet,
);
this.env.logger.log("Fetching battle list...");
const gameList = await splatnet.getBattleList();
using bar1 = this.exporterProgress("Export vs games");
const { redraw, endBar } = this.exporterProgress("Export vs games");
const fetcher = new GameFetcher({
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
state: this.profile.state,
@ -319,10 +204,10 @@ export class App {
type: "VsInfo",
fetcher,
exporter: e,
gameListFetcher,
gameList,
stepProgress: stats[e.name],
onStep: () => {
bar1.redraw(e.name, progress(stats[e.name]));
redraw(e.name, progress(stats[e.name]));
},
}),
)
@ -333,7 +218,7 @@ export class App {
),
);
await bar1.end();
endBar();
this.printStats(stats);
if (errors.length > 0) {
@ -353,9 +238,12 @@ export class App {
if (skipMode.includes("coop") || exporters.length === 0) {
this.env.logger.log("Skip exporting coop games.");
} else {
const gameListFetcher = new CoopListFetcher(splatnet);
this.env.logger.log("Fetching coop battle list...");
const coopBattleList = await splatnet.getBattleList(
BattleListType.Coop,
);
using bar2 = this.exporterProgress("Export coop games");
const { redraw, endBar } = this.exporterProgress("Export coop games");
const fetcher = new GameFetcher({
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
state: this.profile.state,
@ -370,10 +258,10 @@ export class App {
type: "CoopInfo",
fetcher,
exporter: e,
gameListFetcher,
gameList: coopBattleList,
stepProgress: stats[e.name],
onStep: () => {
bar2.redraw(e.name, progress(stats[e.name]));
redraw(e.name, progress(stats[e.name]));
},
}),
)
@ -384,7 +272,7 @@ export class App {
),
);
await bar2.end();
endBar();
this.printStats(stats);
if (errors.length > 0) {
@ -424,36 +312,34 @@ export class App {
throw errors[0];
}
}
}
async monitorWithNxapi() {
this.env.logger.debug("Monitoring with nxapi presence");
const fetcher = this.env.newFetcher();
await this.exportOnce();
while (true) {
await this.countDown(this.profile.state.monitorInterval);
const nxapiResponse = await fetcher.get({
url: this.opts.nxapiPresenceUrl!,
headers: {
"User-Agent": USERAGENT,
},
});
const nxapiData = await nxapiResponse.json();
const isSplatoon3Active = nxapiData.title?.id === SPLATOON3_TITLE_ID;
if (isSplatoon3Active || this.splatoon3PreviouslyActive) {
this.env.logger.log("Splatoon 3 is active, exporting data");
await this.exportOnce();
const stageExporters = exporters.filter((e) => e.exportStages);
if (!this.opts.withStages || stageExporters.length === 0) {
this.env.logger.log("Skip exporting stages.");
} else {
const stageRecords = await splatnet.getStageRecords();
await Promise.all(
stageExporters.map((e) =>
showError(
this.env,
e.exportStages!(stageRecords.stageRecords.nodes),
).then((result) => {
if (result.status === "success") {
this.env.logger.log(`Exported stages to ${result.url}`);
} else if (result.status === "skip") {
this.env.logger.log(`Skipped exporting stages to ${e.name}`);
} else {
const _never: never = result;
}
if (isSplatoon3Active !== this.splatoon3PreviouslyActive) {
this.env.logger.debug(
"Splatoon 3 status has changed from",
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() {
while (true) {
@ -468,7 +354,6 @@ export class App {
display: "[:bar] :completed/:total",
})
: undefined;
try {
for (const i of Array(sec).keys()) {
bar?.render([{
completed: i,
@ -476,9 +361,7 @@ export class App {
}]);
await delay(1000);
}
} finally {
await bar?.end();
}
bar?.end();
}
async run() {
await this.profile.readState();
@ -495,9 +378,7 @@ export class App {
});
}
if (this.opts.nxapiPresenceUrl) {
await this.monitorWithNxapi();
} else if (this.opts.monitor) {
if (this.opts.monitor) {
await this.monitor();
} else {
await this.exportOnce();
@ -511,24 +392,30 @@ export class App {
* @param gameList ID list of games, sorted by date, newest first
* @param onStep Callback function called when a game is exported
*/
private async exportGameList({
async exportGameList({
type,
fetcher,
exporter,
gameListFetcher,
gameList,
stepProgress,
onStep,
}: {
type: Game["type"];
exporter: GameExporter;
fetcher: GameFetcher;
gameListFetcher: GameListFetcher;
gameList: string[];
stepProgress: StepProgress;
onStep: () => void;
}): Promise<StepProgress> {
onStep?.();
const workQueue = await gameListFetcher.fetch(exporter);
const workQueue = [
...await exporter.notExported({
type,
list: gameList,
}),
]
.reverse();
const step = async (id: string) => {
const detail = await fetcher.fetch(type, id);
@ -565,7 +452,7 @@ export class App {
}
printStats(stats: Record<string, StepProgress>) {
this.env.logger.log(
`\nExported ${
`Exported ${
Object.entries(stats)
.map(([name, { exported }]) => `${name}: ${exported}`)
.join(", ")

View File

@ -1,46 +1,35 @@
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "s3si.ts";
export const S3SI_VERSION = "0.4.20";
export const NSOAPP_VERSION = "2.10.1";
export const WEB_VIEW_VERSION = "6.0.0-9f87c815";
export const AGENT_NAME = "splashcat / s3si.ts";
export const AGENT_VERSION = "1.1.1";
export const S3SI_VERSION = "0.4.1";
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
export const NSOAPP_VERSION = "2.5.1";
export const WEB_VIEW_VERSION = "4.0.0-d5178440";
export enum Queries {
HomeQuery =
"51fc56bbf006caf37728914aa8bc0e2c86a80cf195b4d4027d6822a3623098a8",
LatestBattleHistoriesQuery =
"b24d22fd6cb251c515c2b90044039698aa27bc1fab15801d83014d919cd45780",
RegularBattleHistoriesQuery =
"2fe6ea7a2de1d6a888b7bd3dbeb6acc8e3246f055ca39b80c4531bbcd0727bba",
BankaraBattleHistoriesQuery =
"9863ea4744730743268e2940396e21b891104ed40e2286789f05100b45a0b0fd",
XBattleHistoriesQuery =
"eb5996a12705c2e94813a62e05c0dc419aad2811b8d49d53e5732290105559cb",
EventBattleHistoriesQuery =
"e47f9aac5599f75c842335ef0ab8f4c640e8bf2afe588a3b1d4b480ee79198ac",
PrivateBattleHistoriesQuery =
"fef94f39b9eeac6b2fac4de43bc0442c16a9f2df95f4d367dd8a79d7c5ed5ce7",
VsHistoryDetailQuery =
"94faa2ff992222d11ced55e0f349920a82ac50f414ae33c83d1d1c9d8161c5dd",
CoopHistoryQuery =
"e11a8cf2c3de7348495dea5cdcaa25e0c153541c4ed63f044b6c174bc5b703df",
CoopHistoryDetailQuery =
"f2d55873a9281213ae27edc171e2b19131b3021a2ae263757543cdd3bf015cc8",
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871",
LatestBattleHistoriesQuery = "0d90c7576f1916469b2ae69f64292c02",
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd",
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
myOutfitCommonDataFilteringConditionQuery =
"ac20c44a952131cb0c9d00eda7bc1a84c1a99546f0f1fc170212d5a6bb51a426",
myOutfitCommonDataEquipmentsQuery =
"45a4c343d973864f7bb9e9efac404182be1d48cf2181619505e9b7cd3b56a6e8",
HistoryRecordQuery =
"a654ecc80161a7ca5c38761c1d9e502d405eae764e2d343618b9c74b1dc0a80f",
ConfigureAnalyticsQuery =
"2a9302bdd09a13f8b344642d4ed483b9464f20889ac17401e993dfa5c2bb3607",
"d02ab22c9dccc440076055c8baa0fa7a",
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",
HistoryRecordQuery = "d9246baf077b2a29b5f7aac321810a77",
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd",
StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3",
}
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
export const USERAGENT = `${AGENT_NAME}/${S3SI_VERSION} (${S3SI_LINK})`;
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`;
export const DEFAULT_APP_USER_AGENT =
"Mozilla/5.0 (Linux; Android 14; Pixel 7a) " +
"Mozilla/5.0 (Linux; Android 11; Pixel 5) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/120.0.6099.230 Mobile Safari/537.36";
"Chrome/94.0.4606.61 Mobile Safari/537.36";
export const SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net";
export const SPLATNET3_ENDPOINT =
"https://api.lp1.av5ja.srv.nintendo.net/api/graphql";
@ -105,10 +94,6 @@ export const SPLATNET3_STATINK_MAP: {
"sameride",
"380e541b5bc5e49d77ff1a616f1343aeba01d500fee36aaddf8f09d74bd3d3bc":
"tripletornado",
"8a7ee88a06407f4be1595ef8af4d2d2ac22bbf213a622cd19bbfaf4d0f36bcd7":
"teioika",
"a75eac34675bc0d4bd9ca9977cf22472848f89e28e08ee986b4461a3f2af28fc":
"ultra_chakuchi",
},
WATER_LEVEL_MAP: {
0: "low",
@ -116,5 +101,3 @@ export const SPLATNET3_STATINK_MAP: {
2: "high",
},
};
export const SPLATOON3_TITLE_ID = "0100c2500fc20000";

View File

@ -126,8 +126,8 @@ if (import.meta.main) {
const service = new S3SIServiceImplement();
const server = new JSONRPCServer({
transport: new DenoIO({
reader: Deno.stdin.readable,
writer: Deno.stdout.writable,
reader: Deno.stdin,
writer: Deno.stdout,
}),
service,
});

133
src/exporters/mongodb.ts Normal file
View File

@ -0,0 +1,133 @@
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",
};
}
}

View File

@ -1,178 +0,0 @@
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[];
}

View File

@ -1,359 +0,0 @@
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),
};
}
}

View File

@ -1,5 +1,6 @@
import {
AGENT_NAME,
COMBINED_VERSION,
S3SI_VERSION,
SPLATNET3_STATINK_MAP,
USERAGENT,
@ -40,7 +41,7 @@ import {
urlSimplify,
} from "../utils.ts";
import { Env } from "../env.ts";
import GEAR_MAP from "../assets/gear-map.json" with { type: "json" };
import GEAR_MAP from "../assets/gear-map.json" assert { type: "json" };
const COOP_POINT_MAP: Record<number, number | undefined> = {
0: -20,
@ -365,7 +366,7 @@ export class StatInkExporter implements GameExporter {
{ primaryGearPower, additionalGearPowers }: PlayerGear,
): StatInkGear => {
const primary = mapAbility(primaryGearPower);
if (!primary && !this.isRandom(primaryGearPower.image)) {
if (!primary) {
throw new Error("Unknown ability: " + primaryGearPower.name);
}
return {
@ -393,8 +394,6 @@ export class StatInkExporter implements GameExporter {
inked: player.paint,
gears: await this.mapGears(player),
crown: player.crown ? "yes" : "no",
crown_type: undefined,
species: player.species === "INKLING" ? "inkling" : "octoling",
disconnected: player.result ? "no" : "yes",
};
if (player.result) {
@ -405,13 +404,6 @@ export class StatInkExporter implements GameExporter {
result.signal = player.result.noroshiTry ?? undefined;
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;
};
async mapBattle(
@ -467,7 +459,7 @@ export class StatInkExporter implements GameExporter {
),
agent: AGENT_NAME,
agent_version: S3SI_VERSION,
agent_version: COMBINED_VERSION,
agent_variables: {
"Upload Mode": this.uploadMode,
},
@ -596,8 +588,6 @@ export class StatInkExporter implements GameExporter {
}
}
result.bankara_power_after = vsDetail.bankaraMatch?.bankaraPower?.power;
if (rankBeforeState && rankState) {
result.rank_before_exp = rankBeforeState.rankPoint;
result.rank_after_exp = rankState.rankPoint;
@ -629,18 +619,16 @@ export class StatInkExporter implements GameExporter {
}
isRandom(image: Image | null): boolean {
// question mark
const RANDOM_FILENAME = [
"473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1",
"dc937b59892604f5a86ac96936cd7ff09e25f18ae6b758e8014a24c7fa039e91",
];
const RANDOM_FILENAME =
"473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1";
// file exporter will replace url to { pathname: string } | string
const url = image?.url as ReturnType<typeof urlSimplify> | undefined | null;
if (typeof url === "string") {
return RANDOM_FILENAME.some((i) => url.includes(i));
return url.includes(RANDOM_FILENAME);
} else if (url === undefined || url === null) {
return false;
} else {
return RANDOM_FILENAME.some((i) => url.pathname.includes(i));
return url.pathname.includes(RANDOM_FILENAME);
}
}
async mapCoopWeapon(
@ -710,7 +698,6 @@ export class StatInkExporter implements GameExporter {
rescued: rescuedCount,
defeat_boss: defeatEnemyCount,
disconnected: disconnected ? "yes" : "no",
species: player.species === "INKLING" ? "inkling" : "octoling",
};
}
mapKing(id?: string) {

View File

@ -140,8 +140,7 @@ export async function getGToken(
"Content-Type": "application/json",
"Accept": "application/json",
"Connection": "Keep-Alive",
"User-Agent":
"Dalvik/2.1.0 (Linux; U; Android 14; Pixel 7a Build/UQ1A.240105.004)",
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 7.1.2)",
},
body: JSON.stringify({
"client_id": "71b963c1b7b6d119",
@ -195,7 +194,7 @@ export async function getGToken(
"Content-Type": "application/json; charset=utf-8",
"Connection": "Keep-Alive",
"Accept-Encoding": "gzip",
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/14)`,
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/7.1.2)`,
},
body: JSON.stringify({
parameter: {
@ -212,24 +211,22 @@ export async function getGToken(
);
const respJson = await resp.json();
const idToken2: string | undefined = respJson?.result
?.webApiServerCredential
const idToken2: string = respJson?.result?.webApiServerCredential
?.accessToken;
const coralUserId: string | undefined = respJson?.result?.user?.id
?.toString();
const coralUserId: number = respJson?.result?.user?.id;
if (!idToken2 || !coralUserId) {
throw new APIError({
response: resp,
json: respJson,
message:
`No idToken2 or coralUserId found. Please try again later. (${idToken2?.length}, ${coralUserId?.length})`,
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`,
});
}
return [idToken2, coralUserId] as const;
};
const getGToken = async (idToken: string, coralUserId: string) => {
const getGToken = async (idToken: string, coralUserId: number) => {
const { f, request_id: requestId, timestamp } = await callImink({
step: 2,
idToken,
@ -247,7 +244,7 @@ export async function getGToken(
"Authorization": `Bearer ${idToken}`,
"Content-Type": "application/json; charset=utf-8",
"Accept-Encoding": "gzip",
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/14)`,
"User-Agent": `com.nintendo.znca/${NSOAPP_VERSION}(Android/7.1.2)`,
},
body: JSON.stringify({
parameter: {
@ -417,7 +414,7 @@ async function callImink(
step: number;
idToken: string;
userId: string;
coralUserId?: string;
coralUserId?: number;
env: Env;
},
): Promise<IminkResponse> {
@ -428,8 +425,6 @@ async function callImink(
headers: {
"User-Agent": USERAGENT,
"Content-Type": "application/json",
"X-znca-Platform": "Android",
"X-znca-Version": NSOAPP_VERSION,
},
body: JSON.stringify({
"token": idToken,

1
src/ipc/mod.ts Normal file
View File

@ -0,0 +1 @@
export { IPC } from "./stdio.ts";

40
src/ipc/stdio.ts Normal file
View File

@ -0,0 +1,40 @@
/// <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"),
);
}
}

10
src/ipc/types.ts Normal file
View File

@ -0,0 +1,10 @@
export type Command = {
type: "hello";
data: string;
};
export type ExtractType<T extends { type: string }, K extends T["type"]> =
Extract<
T,
{ type: K }
>;

View File

@ -1,15 +1,15 @@
import { readLines } from "../utils.ts";
import { io, writeAll } from "../../deps.ts";
import { Transport } from "./types.ts";
export class DenoIO implements Transport {
lines: AsyncIterableIterator<string>;
writer: WritableStreamDefaultWriter<Uint8Array>;
writer: Deno.Writer & Deno.Closer;
constructor({ reader, writer }: {
reader: ReadableStream<Uint8Array>;
writer: WritableStream<Uint8Array>;
reader: Deno.Reader;
writer: Deno.Writer & Deno.Closer;
}) {
this.lines = readLines(reader);
this.writer = writer.getWriter();
this.lines = io.readLines(reader);
this.writer = writer;
}
async recv(): Promise<string | undefined> {
const result = await this.lines.next();
@ -21,8 +21,10 @@ export class DenoIO implements Transport {
return undefined;
}
async send(data: string) {
await this.writer.ready;
await this.writer.write(new TextEncoder().encode(data + "\n"));
await writeAll(
this.writer,
new TextEncoder().encode(data + "\n"),
);
}
async close() {
await this.writer.close();

View File

@ -15,7 +15,7 @@ import {
} from "./types.ts";
import { DEFAULT_ENV, Env } from "./env.ts";
import { getBulletToken, getGToken } from "./iksm.ts";
import { battleTime, parseHistoryDetailId } from "./utils.ts";
import { parseHistoryDetailId } from "./utils.ts";
export class Splatnet3 {
protected profile: Profile;
@ -137,12 +137,6 @@ export class Splatnet3 {
[BattleListType.Bankara]: () =>
this.request(Queries.BankaraBattleHistoriesQuery)
.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]: () =>
this.request(Queries.PrivateBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.privateBattleHistories)),
@ -174,29 +168,6 @@ export class Splatnet3 {
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(
id: string,
) {
@ -286,6 +257,12 @@ export class Splatnet3 {
CoopHistoryQuery,
};
}
async getStageRecords() {
const resp = await this.request(Queries.StageRecordQuery);
return resp;
}
}
function getIdsFromGroups<T extends { id: string }>(

View File

@ -30,6 +30,7 @@ export type State = {
statInkApiKey?: string;
fileExportPath: string;
monitorInterval: number;
mongoDbUri?: string;
splashcatApiKey?: string;
};

View File

@ -1,3 +1,4 @@
import { splatNet3Types } from "../deps.ts";
import { RankState } from "./state.ts";
import { Queries } from "./constant.ts";
export { Queries };
@ -8,7 +9,6 @@ export type VarsMap = {
[Queries.RegularBattleHistoriesQuery]: [];
[Queries.BankaraBattleHistoriesQuery]: [];
[Queries.XBattleHistoriesQuery]: [];
[Queries.EventBattleHistoriesQuery]: [];
[Queries.PrivateBattleHistoriesQuery]: [];
[Queries.VsHistoryDetailQuery]: [{
vsResultId: string;
@ -21,6 +21,7 @@ export type VarsMap = {
[Queries.myOutfitCommonDataEquipmentsQuery]: [];
[Queries.HistoryRecordQuery]: [];
[Queries.ConfigureAnalyticsQuery]: [];
[Queries.StageRecordQuery]: [];
};
export type Image = {
@ -129,7 +130,6 @@ export type PlayerWeapon = {
};
};
export type VsPlayer = {
nameplate: Nameplate;
id: string;
nameId: string | null;
name: string;
@ -146,7 +146,6 @@ export type VsPlayer = {
} | null;
paint: number;
crown: boolean;
festDragonCert: "NONE" | "DRAGON" | "DOUBLE_DRAGON";
headGear: PlayerGear;
clothingGear: PlayerGear;
@ -159,11 +158,6 @@ export type Color = {
r: number;
};
export type VsTeam = {
festUniformName?: string;
festStreakWinCount?: number;
festUniformBonusRate?: number;
order: number;
judgement: string;
players: VsPlayer[];
color: Color;
tricolorRole: null | "DEFENSE" | "ATTACK1" | "ATTACK2";
@ -171,7 +165,6 @@ export type VsTeam = {
result: null | {
paintRatio: null | number;
score: null | number;
noroshi: null | number;
};
};
export type VsRule =
@ -244,9 +237,6 @@ export type VsHistoryDetail = {
bankaraMatch: {
earnedUdemaePoint: null | number;
mode: "OPEN" | "CHALLENGE";
bankaraPower?: null | {
power?: null | number;
};
} | null;
festMatch: {
dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
@ -278,8 +268,6 @@ export type CoopHistoryPlayerResult = {
name: string;
id: string;
};
isMyself: boolean;
species: "INKLING" | "OCTOLING";
};
weapons: { name: string; image: Image | null }[];
specialWeapon: null | {
@ -384,6 +372,9 @@ export type GameExporter = {
) => Promise<string[]>;
exportGame: (game: Game) => Promise<ExportResult>;
exportSummary?: (summary: Summary) => Promise<ExportResult>;
exportStages?: (
stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"],
) => Promise<ExportResult>;
};
export type BankaraBattleHistories = {
@ -429,11 +420,6 @@ export type RespMap = {
};
[Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories;
[Queries.XBattleHistoriesQuery]: XBattleHistories;
[Queries.EventBattleHistoriesQuery]: {
eventBattleHistories: {
historyGroups: HistoryGroups<BattleListNode>;
};
};
[Queries.PrivateBattleHistoriesQuery]: {
privateBattleHistories: {
historyGroups: HistoryGroups<BattleListNode>;
@ -568,6 +554,7 @@ export type RespMap = {
xMatchMaxLf: SimpleXRank;
} | null;
};
[Queries.StageRecordQuery]: splatNet3Types.StageRecordResult;
};
export type WeaponWithRatio = {
weapon: {
@ -620,14 +607,10 @@ export enum BattleListType {
Latest,
Regular,
Bankara,
Event,
XBattle,
Private,
Coop,
}
export type ListMethod = "latest" | "all" | "auto";
export type StatInkUuidList = {
status: number;
code: number;
@ -647,7 +630,7 @@ export type StatInkWeapon = {
}[];
export type StatInkGear = {
primary_ability: string | null;
primary_ability: string;
secondary_abilities: (string | null)[];
};
@ -673,9 +656,7 @@ export type StatInkPlayer = {
special?: number;
gears?: StatInkGears;
crown?: "yes" | "no";
crown_type?: "x" | "100x" | "333x";
disconnected: "yes" | "no";
species: "inkling" | "octoling";
};
export type StatInkStage = {
@ -725,7 +706,6 @@ export type StatInkCoopPlayer = {
rescued: number;
defeat_boss: number;
disconnected: "yes" | "no";
species: "inkling" | "octoling";
};
export type StatInkCoopBoss = {
@ -835,8 +815,6 @@ export type StatInkPostBody = {
challenge_lose?: number;
x_power_before?: number | null;
x_power_after?: number | null;
bankara_power_before?: number | null;
bankara_power_after?: number | null;
fest_power?: number; // Splatfest Power (Pro)
fest_dragon?:
| "10x"

View File

@ -9,12 +9,12 @@ const COOP_ID =
Deno.test("gameId", async () => {
assertEquals(
await gameId(base64.encodeBase64(VS_ID)),
await gameId(base64.encode(VS_ID)),
"042bcac9-6b25-5d2e-a5ea-800939a6dea1",
);
assertEquals(
await gameId(base64.encodeBase64(COOP_ID)),
await gameId(base64.encode(COOP_ID)),
"58329d62-737d-5b43-ac22-e35e6e44b077",
);
});
@ -22,7 +22,7 @@ Deno.test("gameId", async () => {
Deno.test("s3sCoopGameId", async () => {
const S3S_COOP_UUID = "be4435b1-0ac5-577b-81bb-766585bec028";
assertEquals(
await s3sCoopGameId(base64.encodeBase64(COOP_ID)),
await s3sCoopGameId(base64.encode(COOP_ID)),
S3S_COOP_UUID,
);
});

View File

@ -6,53 +6,29 @@ import {
} from "./constant.ts";
import { base64, uuid } from "../deps.ts";
import { Env } from "./env.ts";
import { io } from "../deps.ts";
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);
const stdinLines = io.readLines(Deno.stdin);
export async function readline(
{ skipEmpty = true }: { skipEmpty?: boolean } = {},
) {
while (true) {
const result = await stdinLines.next();
if (result.done) {
throw new Error("EOF");
}
const line = result.value;
for await (const line of stdinLines) {
if (!skipEmpty || line !== "") {
return line;
}
}
throw new Error("EOF");
}
export function urlBase64Encode(data: ArrayBuffer) {
return base64.encodeBase64(data)
return base64.encode(data)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
}
export function urlBase64Decode(data: string) {
return base64.encodeBase64(
return base64.decode(
data
.replaceAll("_", "/")
.replaceAll("-", "+"),
@ -127,14 +103,14 @@ export function gameId(
);
return uuid.v5.generate(BATTLE_NAMESPACE, content);
} else if (parsed.type === "CoopHistoryDetail") {
return uuid.v5.generate(COOP_NAMESPACE, base64.decodeBase64(id));
return uuid.v5.generate(COOP_NAMESPACE, base64.decode(id));
} else {
throw new Error("Unknown type");
}
}
export function s3siGameId(id: string) {
const fullId = base64.decodeBase64(id);
const fullId = base64.decode(id);
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
return uuid.v5.generate(S3SI_NAMESPACE, tsUuid);
}
@ -146,7 +122,7 @@ export function s3siGameId(id: string) {
* @returns uuid used in stat.ink
*/
export function s3sCoopGameId(id: string) {
const fullId = base64.decodeBase64(id);
const fullId = base64.decode(id);
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
return uuid.v5.generate(COOP_NAMESPACE, tsUuid);
}
@ -155,7 +131,7 @@ export function s3sCoopGameId(id: string) {
* @param id VsHistoryDetail id or CoopHistoryDetail id
*/
export function parseHistoryDetailId(id: string) {
const plainText = new TextDecoder().decode(base64.decodeBase64(id));
const plainText = new TextDecoder().decode(base64.decode(id));
const vsRE =
/VsHistoryDetail-([a-z0-9-]+):(\w+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
@ -191,7 +167,7 @@ export const delay = (ms: number) =>
* Decode ID and get number after '-'
*/
export function b64Number(id: string): number {
const text = new TextDecoder().decode(base64.decodeBase64(id));
const text = new TextDecoder().decode(base64.decode(id));
const [_, num] = text.split("-");
return parseInt(num);
}
@ -212,14 +188,3 @@ export function urlSimplify(url: string): { pathname: string } | string {
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);
};