Compare commits

..

56 Commits

Author SHA1 Message Date
Rosalina 32200ce9bb
Merge remote-tracking branch 'upstream/main' into nxapi-presence 2024-01-16 08:42:46 -05:00
spacemeowx2 02a01188c6 ci: update oldest deno version 2024-01-16 16:05:39 +08:00
spacemeowx2 54a7ff55fa ci: skip fmt and lint for old version 2024-01-16 16:04:28 +08:00
spacemeowx2 ed5d286ac7 fix: lint 2024-01-16 15:54:36 +08:00
spacemeowx2 f33c03c691 ci: remove constant-check 2024-01-16 15:51:26 +08:00
Rosalina c004e6596c
Merge remote-tracking branch 'upstream/main' into nxapi-presence 2024-01-14 18:16:38 -05:00
Rosalina c336f0e7b7
use s3si.ts fetcher and send a user agent 2024-01-14 18:11:03 -05:00
Rosalina 055b1405df run deno fmt 2024-01-14 15:10:33 +08:00
Rosalina b8e53fc719 simplify cli arguments to just nxapi-presence and add to readme 2024-01-14 15:10:33 +08:00
Rosalina cbe7a5424a switch to polling every monitor interval 2024-01-14 15:10:33 +08:00
Rosalina 21b02fb44d event stream implementation
(committing this right now because event streams seem broke)
2024-01-14 15:10:33 +08:00
Rosalina 6f396d012c
run deno fmt 2024-01-14 02:08:23 -05:00
Rosalina 4d65d0acef
simplify cli arguments to just nxapi-presence and add to readme 2024-01-13 02:34:43 -05:00
Rosalina 72c3b7c9d3
switch to polling every monitor interval 2024-01-13 02:10:38 -05:00
Rosalina 81a228cfb8
event stream implementation
(committing this right now because event streams seem broke)
2024-01-13 01:59:04 -05:00
imspace cc348653d6 chore: update `WEB_VIEW_VERSION` 2023-12-15 21:47:02 +08:00
spacemeowx2 4a0cda32ab feat: update `NSOAPP_VERSION` 2023-12-07 14:47:08 +08:00
imspace 454f294045 feat: add 6.0.0 special 2023-12-02 21:39:22 +08:00
imspace 209e5e75ed Revert "fix: use old NSO app version"
This reverts commit 02c1c92191.
2023-12-02 21:04:03 +08:00
spacemeowx2 02c1c92191 fix: use old NSO app version 2023-12-01 13:46:36 +08:00
spacemeowx2 6a4fd3cceb chore: update VersionData 2023-11-30 13:44:06 +08:00
spacemeowx2 a7b0783f89 chore: update NSOAPP_VERSION 2023-11-30 13:27:55 +08:00
spacemeowx2 bfb7d79609 ci: update pnpm version 2023-11-30 13:19:35 +08:00
spacemeowx2 e4be0f2fe3 chore: update S3SI_VERSION 2023-11-30 12:56:53 +08:00
spacemeowx2 af0ea16ecc chore update `WEB_VIEW_VERSION` 2023-11-30 12:55:31 +08:00
spacemeowx2 addb535d96 chore: update `NSOAPP_VERSION` 2023-11-09 15:57:05 +08:00
spacemeowx2 94c33bae8f feat: support random primary ability 2023-10-23 20:00:44 +08:00
spacemeowx2 f236a523f7 chore: update `WEB_VIEW_VERSION` 2023-10-17 15:15:51 +08:00
imspace b2555783bb chore: bump version 0.4.9 2023-09-15 15:13:05 +08:00
imspace 0cfe618f2f feat: add species and crown_type
https://github.com/fetus-hina/stat.ink/issues/1227
2023-09-15 15:13:05 +08:00
spacemeowx2 5e36f6c33d feat: add stat.ink types 2023-09-15 15:13:05 +08:00
spacemeowx2 41d71073dc chore: update S3SI_VERSION 2023-09-01 15:10:36 +08:00
spacemeowx2 5b7d320267 chore: update `WEB_VIEW_VERSION` and queries (0.4.8) 2023-08-31 17:11:59 +08:00
spacemeowx2 a770901759 feat: update VersionData 2023-08-31 17:11:59 +08:00
imspace 7a2dedfbe5 chore: update VERSION 2023-08-25 13:54:16 +08:00
spacemeowx2 cad2edeaf5 chore: update `WEB_VIEW_VERSION` (0.4.7) 2023-08-22 16:09:19 +08:00
spacemeowx2 6582ab408b chore: bump version (0.4.6) 2023-07-26 16:47:25 +08:00
spacemeowx2 348cf6045a chore: update WEB_VIEW_VERSION and Queries 2023-07-26 16:45:39 +08:00
spacemeowx2 0baad9c04b chore: update NSOAPP_VERSION 2023-07-24 16:11:38 +08:00
spacemeowx2 56c75385fa chore: deno.lock 2023-07-17 15:36:19 +08:00
spacemeowx2 417a52138d fix: skip updateState if history if empty (#81) 2023-07-17 15:33:32 +08:00
spacemeowx2 5867740de3 feat(gui): using latest tauri config, remove hacky way 2023-07-07 01:18:22 +08:00
spacemeowx2 8707feac01 fix(gui): breaking change by daisyui 2023-07-07 01:18:22 +08:00
spacemeowx2 740259e156 chore(gui): update README 2023-07-07 01:18:22 +08:00
spacemeowx2 2702e6cdf3 chore: upgrade deps 2023-07-07 01:18:22 +08:00
imspace 1bc0d3eefc fix: list method is not auto 2023-06-15 19:22:00 +08:00
spacemeowx2 a67bb4814d chore: update `NSOAPP_VERSION` 2023-06-13 21:47:39 +08:00
spacemeowx2 6e5c2e05f3 chore: update README 2023-06-13 01:42:39 +08:00
spacemeowx2 40cfd13e6c feat: send Anarchy (Open) Power 2023-06-12 17:05:46 +08:00
spacemeowx2 6d044a15ae chore: bump version(0.4.3) 2023-06-06 22:40:52 +08:00
spacemeowx2 63ea9347da feat: implement auto list-method 2023-06-06 22:40:52 +08:00
spacemeowx2 a5f35c78c9 style: remove one line 2023-06-06 22:40:52 +08:00
spacemeowx2 91f528a3be feat: add fetch from all modes 2023-06-06 22:40:52 +08:00
spacemeowx2 8a96cb321c feat: add list-method opt and its query 2023-06-06 22:40:52 +08:00
spacemeowx2 0517bda98d fix: don't print token (oops) 2023-06-05 19:15:27 +08:00
spacemeowx2 cabfa8f8c0 fix: `coral_user_id` is string 2023-06-05 17:06:42 +08:00
36 changed files with 1400 additions and 1237 deletions

View File

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

View File

@ -1,22 +0,0 @@
name: Constant Check
on:
pull_request:
branches:
- main
push:
branches:
- main
schedule:
- cron: "0 0 * * *"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: 1.x
- name: Check constant updates
run: deno run -A ./scripts/update-constant.ts
- name: Check if workspace is clean
run: git diff --exit-code

View File

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 7.29.1 version: 8.11.0
- name: Sync node version and setup cache - name: Sync node version and setup cache
uses: actions/setup-node@v3 uses: actions/setup-node@v3

View File

@ -1,3 +1,53 @@
## 0.4.12
feat: add 6.0.0 special
## 0.4.11
chore: update `WEB_VIEW_VERSION` and queries
## 0.4.10
feat: support random primary ability
## 0.4.9
feat: add species and crown_type
## 0.4.8
chore: update `WEB_VIEW_VERSION` and queries
feat: update VersionData
## 0.4.7
chore: update `WEB_VIEW_VERSION`
## 0.4.6
chore: update constants
fix: skip updateState if history if empty
([#81](https://github.com/spacemeowx2/s3si.ts/issues/81))
## 0.4.5
fix: list method is not auto
## 0.4.4
feat: send Anarchy (Open) Power
## 0.4.3
feat: add `list-method` option
([#73](https://github.com/spacemeowx2/s3si.ts/issues/73))
## 0.4.2
fix: `coral_user_id` is string
## 0.4.1 ## 0.4.1
feat: add support for Challenges feat: add support for Challenges

View File

@ -20,12 +20,17 @@ Options:
--exporter <exporter>, -e Exporter list to use (default: stat.ink) --exporter <exporter>, -e Exporter list to use (default: stat.ink)
Multiple exporters can be separated by commas Multiple exporters can be separated by commas
(e.g. "stat.ink,file") (e.g. "stat.ink,file")
--list-method When set to "latest", the latest 50 matches will be obtained.
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
"auto" is the default setting.
--no-progress, -n Disable progress bar --no-progress, -n Disable progress bar
--monitor, -m Monitor mode --monitor, -m Monitor mode
--skip-mode <mode>, -s Skip mode (default: null) --skip-mode <mode>, -s Skip mode (default: null)
("vs", "coop") ("vs", "coop")
--with-summary Include summary in the output --with-summary Include summary in the output
--help Show this help message and exit`, --help Show this help message and exit
--nxapi-presence Extends monitoring mode to use Nintendo Switch presence from nxapi
``` ```
3. If it's your first time running this, follow the instructions to login to 3. If it's your first time running this, follow the instructions to login to

116
deno.lock
View File

@ -16,6 +16,7 @@
"https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179", "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/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2",
"https://deno.land/std@0.160.0/flags/mod.ts": "686b6b36e14b00f11c9e26cecf439021158436a6e34f60eeb0d927f0b169ae20", "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/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289",
"https://deno.land/std@0.160.0/io/mod.ts": "6e781ebafd5cdccf9ab4afa1f499b08c513602d023cb08ceebc58758501f78bd", "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/readers.ts": "45847ad404afd2f605eae1cff193f223462bc55eeb9ae313c2f3db28aada0fd6",
@ -32,6 +33,9 @@
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", "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/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
"https://deno.land/std@0.160.0/streams/conversion.ts": "328afbedee0a7e0c330ac4c7b4c1af569ee53974f970230f6a78f545b93abb9b", "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/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5", "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/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
@ -68,117 +72,5 @@
"https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b", "https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b",
"https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603", "https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603",
"https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345" "https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345"
},
"npm": {
"specifiers": {
"mongodb": "mongodb@5.5.0",
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
},
"packages": {
"@types/node@18.14.2": {
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dependencies": {}
},
"@types/webidl-conversions@7.0.0": {
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==",
"dependencies": {}
},
"@types/whatwg-url@8.2.2": {
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
"dependencies": {
"@types/node": "@types/node@18.14.2",
"@types/webidl-conversions": "@types/webidl-conversions@7.0.0"
}
},
"bson@5.0.1": {
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
"dependencies": {}
},
"bson@5.3.0": {
"integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==",
"dependencies": {}
},
"ip@2.0.0": {
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
"dependencies": {}
},
"memory-pager@1.5.0": {
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"dependencies": {}
},
"mongodb-connection-string-url@2.6.0": {
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
"dependencies": {
"@types/whatwg-url": "@types/whatwg-url@8.2.2",
"whatwg-url": "whatwg-url@11.0.0"
}
},
"mongodb@5.1.0": {
"integrity": "sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw==",
"dependencies": {
"bson": "bson@5.0.1",
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
"saslprep": "saslprep@1.0.3",
"socks": "socks@2.7.1"
}
},
"mongodb@5.5.0": {
"integrity": "sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ==",
"dependencies": {
"bson": "bson@5.3.0",
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
"saslprep": "saslprep@1.0.3",
"socks": "socks@2.7.1"
}
},
"punycode@2.3.0": {
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
"dependencies": {}
},
"saslprep@1.0.3": {
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dependencies": {
"sparse-bitfield": "sparse-bitfield@3.0.3"
}
},
"smart-buffer@4.2.0": {
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dependencies": {}
},
"socks@2.7.1": {
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
"dependencies": {
"ip": "ip@2.0.0",
"smart-buffer": "smart-buffer@4.2.0"
}
},
"sparse-bitfield@3.0.3": {
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"dependencies": {
"memory-pager": "memory-pager@1.5.0"
}
},
"splatnet3-types@0.2.20230227204004": {
"integrity": "sha512-FAY6pbUcrp5O8c49BNXSKxoyM3UlCrRx2AtA9Y3qlvqOLdHqwxtzcdzbk1b1hRam8ZcrxRzE/ii6ESRiPIAnZw==",
"dependencies": {}
},
"tr46@3.0.0": {
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dependencies": {
"punycode": "punycode@2.3.0"
}
},
"webidl-conversions@7.0.0": {
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dependencies": {}
},
"whatwg-url@11.0.0": {
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"dependencies": {
"tr46": "tr46@3.0.0",
"webidl-conversions": "webidl-conversions@7.0.0"
}
}
}
} }
} }

View File

@ -13,6 +13,4 @@ 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 { 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 { 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 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"; export { writeAll } from "https://deno.land/std@0.160.0/streams/conversion.ts";

View File

@ -1,7 +1,7 @@
# Tauri + React + Typescript # s3si.ts GUI
This template should help get you started developing with Tauri, React and Typescript in Vite. ## Development
## Recommended IDE Setup ```
pnpm tauri dev
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) ```

View File

@ -11,31 +11,32 @@
"lint": "eslint --max-warnings=0 src" "lint": "eslint --max-warnings=0 src"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.4.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"daisyui": "^2.52.0", "daisyui": "^3.1.7",
"i18next": "^22.5.0", "i18next": "^23.2.6",
"i18next-browser-languagedetector": "^7.0.2", "i18next-browser-languagedetector": "^7.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^12.3.1", "react-i18next": "^13.0.1",
"react-icons": "^4.9.0", "react-icons": "^4.10.1",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.14.1",
"react-use": "^17.4.0" "react-use": "^17.4.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.3.1", "@tauri-apps/cli": "^1.4.0",
"@types/node": "^20.2.5", "@types/node": "^20.3.3",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^4.0.0", "@typescript-eslint/parser": "^5.61.0",
"@vitejs/plugin-react": "^4.0.1",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.41.0", "eslint": "^8.44.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"i18next-http-backend": "^2.2.1", "i18next-http-backend": "^2.2.1",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.0.4", "typescript": "^5.1.6",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0" "vite-tsconfig-paths": "^4.2.0"

File diff suppressed because it is too large Load Diff

645
gui/src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import { Layout } from "components/Layout";
import { Home } from "pages/Home"; import { Home } from "pages/Home";
import { Settings } from "pages/Settings"; import { Settings } from "pages/Settings";
import { Guide } from 'pages/Guide'; import { Guide } from 'pages/Guide';
import { RedirectLogin } from 'pages/RedirectLogin';
import { useShowWindow } from 'hooks/useShowWindow'; import { useShowWindow } from 'hooks/useShowWindow';
function App() { function App() {
@ -15,7 +14,6 @@ function App() {
<Route index element={<Home />} /> <Route index element={<Home />} />
<Route path='/settings' element={<Settings />} /> <Route path='/settings' element={<Settings />} />
<Route path='/guide' element={<Guide />} /> <Route path='/guide' element={<Guide />} />
<Route path='/redirect' element={<RedirectLogin />} />
</Route> </Route>
</Routes> </Routes>
); );

View File

@ -51,10 +51,10 @@ export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
</> </>
} }
const btnLoading = loading || doing;
return <> return <>
<button className={classNames('btn', { <button className={classNames('btn w-full', {
'btn-disabled': !result?.profile.state.loginState?.sessionToken, 'btn-disabled': !result?.profile.state.loginState?.sessionToken,
'loading': loading || doing, })} onClick={onClick} disabled={btnLoading}>{btnLoading ? <span className='loading' /> : children}</button>
})} onClick={onClick}>{children}</button>
</> </>
} }

View File

@ -61,11 +61,11 @@ export const RunPanel: React.FC<RunPanelProps> = () => {
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox> <Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
<button <button
onClick={onClick} onClick={onClick}
className={classNames('btn w-full', { className={classNames('btn btn-primary w-full', {
'btn-disabled': disabled || (!exportBattle && !exportCoop), 'btn-disabled': disabled || (!exportBattle && !exportCoop),
'loading': loading,
})} })}
>{t('导出')}</button> disabled={loading}
>{loading ? <span className='loading' /> : t('导出')}</button>
</div> </div>
</> </>
} }

View File

@ -2,40 +2,17 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body { body {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
#root { #root {
width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden overflow: hidden
} }
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
}
/* custom classes */ /* custom classes */
.flex-auto-all > * { .flex-auto-all > * {

View File

@ -16,7 +16,7 @@ export const Home: React.FC = () => {
<Link to='/settings' className='btn'>{t('设置')}</Link> <Link to='/settings' className='btn'>{t('设置')}</Link>
<div className='flex gap-2 flex-auto-all'> <div className='flex gap-2 flex-auto-all'>
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet> <OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet>
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a> <a className='btn w-full' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,25 +0,0 @@
import { Loading } from 'components/Loading';
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-use';
export const RedirectLogin: React.FC = () => {
const { t } = useTranslation();
const state = useLocation();
useEffect(() => {
const search = state.search ?? '';
const index = search.indexOf('url=');
if (index === -1) {
return;
}
const url = decodeURIComponent(search.substring(index + 4));
window.location.href = url;
}, [state])
return <div className='h-full flex justify-center items-center'>
<span className='flex justify-center items-center gap-1'><Loading className='align-middle' />{t('正在跳转到登录页面...')}</span>
</div>
}

21
s3si.ts
View File

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

View File

@ -1,49 +0,0 @@
import { MongoDB } from "../deps.ts";
import { DEFAULT_ENV } from "../src/env.ts";
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
import { FileStateBackend, Profile } from "../src/state.ts";
const OLD_BATTLES_END_DATE = new Date("2023-02-28T03:42:47.000+00:00");
const env = DEFAULT_ENV;
const stateBackend = new FileStateBackend("./profile.json");
const profile = new Profile({ stateBackend, env });
await profile.readState();
if (!profile.state.mongoDbUri) {
console.error("MongoDB URI not set");
Deno.exit(1);
}
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
const filter = {
"exportDate": {
"$lte": OLD_BATTLES_END_DATE,
},
"gameId": undefined,
};
const cursor = battlesCollection.find(filter);
const oldDocuments = await battlesCollection.countDocuments(filter);
console.log(`Found ${oldDocuments} old battles to update...`);
for await (const doc of cursor) {
const { splatNetData, _id } = doc;
const splatNetId = splatNetData.id;
const uniqueId = MongoDBExporter.getGameId(splatNetId);
await battlesCollection.updateOne({ _id }, {
"$set": {
gameId: uniqueId,
},
});
console.log(`Updated ${splatNetId} to ${uniqueId}`);
}
console.log("Done!");

View File

@ -84,104 +84,5 @@
"https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b", "https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b",
"https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603", "https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603",
"https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345" "https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345"
},
"npm": {
"specifiers": {
"mongodb": "mongodb@5.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

@ -1,47 +0,0 @@
import { MongoDB } from "../deps.ts";
import { DEFAULT_ENV } from "../src/env.ts";
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
import { FileStateBackend, Profile } from "../src/state.ts";
const env = DEFAULT_ENV;
const stateBackend = new FileStateBackend("./profile.json");
const profile = new Profile({ stateBackend, env });
await profile.readState();
if (!profile.state.mongoDbUri) {
console.error("MongoDB URI not set");
Deno.exit(1);
}
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
const filter = {
"splatNetData.playedTime": {
$type: "string",
},
};
const cursor = battlesCollection.find(filter);
const oldDocuments = await battlesCollection.countDocuments(filter);
console.log(`Found ${oldDocuments} old battles to update...`);
for await (const doc of cursor) {
const { splatNetData, _id } = doc;
await battlesCollection.updateOne({ _id }, {
"$set": {
"splatNetData.playedTime": new Date(splatNetData.playedTime),
},
});
console.log(
`Updated ${splatNetData.playedTime} to ${new Date(
splatNetData.playedTime,
)}`,
);
}
console.log("Done!");

View File

@ -1,77 +0,0 @@
import { MongoDB } from "../deps.ts";
import { DEFAULT_ENV } from "../src/env.ts";
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
import { FileStateBackend, Profile } from "../src/state.ts";
const env = DEFAULT_ENV;
const stateBackend = new FileStateBackend("./profile.json");
const profile = new Profile({ stateBackend, env });
await profile.readState();
if (!profile.state.mongoDbUri) {
console.error("MongoDB URI not set");
Deno.exit(1);
}
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
const cursor = battlesCollection.find();
const oldDocuments = await battlesCollection.countDocuments();
console.log(`Found ${oldDocuments} old battles to upload...`);
let count = 0;
const erroredBattles = [];
for await (const doc of cursor) {
const { splatNetData, _id } = doc;
// start time for performance tracking, needs to be very accurate
const startTime = new Date();
splatNetData.playedTime = splatNetData.playedTime.toISOString();
const response = await fetch("http://127.0.0.1:8000/battles/api/upload/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${profile.state.splashcatApiKey}`,
},
body: JSON.stringify({
"data_type": "splatnet3",
"battle": splatNetData,
})
})
if (!response.ok) {
console.error(`Failed to upload ${splatNetData.id}`);
erroredBattles.push({
id: doc.gameId,
error: await response.text(),
});
}
// end time for performance tracking, needs to be very accurate
const endTime = new Date();
const timeTaken = endTime.getTime() - startTime.getTime();
console.log(`Uploaded ${splatNetData.id} (${timeTaken}ms)`);
count++;
console.log(`Uploaded ${count}/${oldDocuments} battles`)
if (count % 100 === 0) {
console.log("Updating error logs...");
if (erroredBattles.length > 0) {
await Deno.writeFile("./errored-battles.json", new TextEncoder().encode(JSON.stringify(erroredBattles, null, "\t")));
}
}
}
console.log("Done!");
if (erroredBattles.length > 0) {
await Deno.writeFile("./errored-battles.json", new TextEncoder().encode(JSON.stringify(erroredBattles, null, 2)));
}

View File

@ -5,7 +5,7 @@ import {
HistoryGroups, HistoryGroups,
RankParam, RankParam,
} from "./types.ts"; } from "./types.ts";
import { gameId, parseHistoryDetailId } from "./utils.ts"; import { battleTime, gameId } from "./utils.ts";
import { getSeason } from "./VersionData.ts"; import { getSeason } from "./VersionData.ts";
const splusParams = () => { const splusParams = () => {
@ -193,17 +193,6 @@ function addRank(
}; };
} }
const battleTime = (id: string) => {
const { timestamp } = parseHistoryDetailId(id);
const dateStr = timestamp.replace(
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
"$1-$2-$3T$4:$5:$6Z",
);
return new Date(dateStr);
};
type FlattenItem = { type FlattenItem = {
id: string; id: string;
gameId: string; gameId: string;
@ -358,6 +347,10 @@ export class RankTracker {
async updateState( async updateState(
history: HistoryGroups<BattleListNode>["nodes"], history: HistoryGroups<BattleListNode>["nodes"],
) { ) {
if (history.length === 0) {
return;
}
// history order by time. 0 is the oldest. // history order by time. 0 is the oldest.
const flatten: FlattenItem[] = await Promise.all( const flatten: FlattenItem[] = await Promise.all(
history history

View File

@ -30,6 +30,18 @@ export const SEASONS: Season[] = [
start: new Date("2023-06-01T00:00:00+00:00"), start: new Date("2023-06-01T00:00:00+00:00"),
end: new Date("2023-09-01T00:00:00+00:00"), end: new Date("2023-09-01T00:00:00+00:00"),
}, },
{
id: "season202309",
name: "Drizzle Season 2023",
start: new Date("2023-09-01T00:00:00+00:00"),
end: new Date("2023-12-01T00:00:00+00:00"),
},
{
id: "season202312",
name: "Chill Season 2023",
start: new Date("2023-12-01T00:00:00+00:00"),
end: new Date("2024-03-01T00:00:00+00:00"),
},
]; ];
export const getSeason = (date: Date): Season | undefined => { export const getSeason = (date: Date): Season | undefined => {

View File

@ -1,15 +1,16 @@
import { loginManually } from "./iksm.ts"; import { loginManually } from "./iksm.ts";
import { MultiProgressBar } from "../deps.ts"; import { MultiProgressBar, Mutex } from "../deps.ts";
import { FileStateBackend, Profile, StateBackend } from "./state.ts"; import { FileStateBackend, Profile, StateBackend } from "./state.ts";
import { Splatnet3 } from "./splatnet3.ts"; import { Splatnet3 } from "./splatnet3.ts";
import { BattleListType, Game, GameExporter } from "./types.ts"; import { BattleListType, Game, GameExporter, ListMethod } from "./types.ts";
import { Cache, FileCache } from "./cache.ts"; import { Cache, FileCache } from "./cache.ts";
import { StatInkExporter } from "./exporters/stat.ink.ts"; import { StatInkExporter } from "./exporters/stat.ink.ts";
import { FileExporter } from "./exporters/file.ts"; import { FileExporter } from "./exporters/file.ts";
import { delay, showError } from "./utils.ts"; import { delay, showError } from "./utils.ts";
import { GameFetcher } from "./GameFetcher.ts"; import { GameFetcher } from "./GameFetcher.ts";
import { DEFAULT_ENV, Env } from "./env.ts"; import { DEFAULT_ENV, Env } from "./env.ts";
import { MongoDBExporter } from "./exporters/mongodb.ts"; import { SPLATOON3_TITLE_ID } from "./constant.ts";
import { USERAGENT } from "./constant.ts";
export type Opts = { export type Opts = {
profilePath: string; profilePath: string;
@ -17,11 +18,12 @@ export type Opts = {
noProgress: boolean; noProgress: boolean;
monitor: boolean; monitor: boolean;
withSummary: boolean; withSummary: boolean;
withStages: boolean;
skipMode?: string; skipMode?: string;
listMethod?: string;
cache?: Cache; cache?: Cache;
stateBackend?: StateBackend; stateBackend?: StateBackend;
env: Env; env: Env;
nxapiPresenceUrl?: string;
}; };
export const DEFAULT_OPTS: Opts = { export const DEFAULT_OPTS: Opts = {
@ -30,7 +32,7 @@ export const DEFAULT_OPTS: Opts = {
noProgress: false, noProgress: false,
monitor: false, monitor: false,
withSummary: false, withSummary: false,
withStages: true, listMethod: "auto",
env: DEFAULT_ENV, env: DEFAULT_ENV,
}; };
@ -55,6 +57,103 @@ class StepProgress {
} }
} }
interface GameListFetcher {
/**
* Return not exported game list.
* [0] is the latest game.
* @param exporter GameExporter
*/
fetch(exporter: GameExporter): Promise<string[]>;
}
class BattleListFetcher implements GameListFetcher {
protected listMethod: ListMethod;
protected allBattleList?: string[];
protected latestBattleList?: string[];
protected allLock = new Mutex();
protected latestLock = new Mutex();
constructor(
listMethod: string,
protected splatnet: Splatnet3,
) {
if (listMethod === "all") {
this.listMethod = "all";
} else if (listMethod === "latest") {
this.listMethod = "latest";
} else {
this.listMethod = "auto";
}
}
protected getAllBattleList() {
return this.allLock.use(async () => {
if (!this.allBattleList) {
this.allBattleList = await this.splatnet.getAllBattleList();
}
return this.allBattleList;
});
}
protected getLatestBattleList() {
return this.latestLock.use(async () => {
if (!this.latestBattleList) {
this.latestBattleList = await this.splatnet.getBattleList();
}
return this.latestBattleList;
});
}
private async innerFetch(exporter: GameExporter) {
if (this.listMethod === "latest") {
return await exporter.notExported({
type: "VsInfo",
list: await this.getLatestBattleList(),
});
}
if (this.listMethod === "all") {
return await exporter.notExported({
type: "VsInfo",
list: await this.getAllBattleList(),
});
}
if (this.listMethod === "auto") {
const latestList = await exporter.notExported({
type: "VsInfo",
list: await this.getLatestBattleList(),
});
if (latestList.length === 50) {
return await exporter.notExported({
type: "VsInfo",
list: await this.getAllBattleList(),
});
}
return latestList;
}
throw new TypeError(`Unknown listMethod: ${this.listMethod}`);
}
async fetch(exporter: GameExporter) {
return [...await this.innerFetch(exporter)].reverse();
}
}
class CoopListFetcher implements GameListFetcher {
constructor(
protected splatnet: Splatnet3,
) {}
async fetch(exporter: GameExporter) {
return [
...await exporter.notExported({
type: "CoopInfo",
list: await this.splatnet.getBattleList(BattleListType.Coop),
}),
].reverse();
}
}
function progress({ total, currentUrl, done }: StepProgress): Progress { function progress({ total, currentUrl, done }: StepProgress): Progress {
return { return {
total, total,
@ -66,6 +165,7 @@ function progress({ total, currentUrl, done }: StepProgress): Progress {
export class App { export class App {
profile: Profile; profile: Profile;
env: Env; env: Env;
splatoon3PreviouslyActive = false;
constructor(public opts: Opts) { constructor(public opts: Opts) {
const stateBackend = opts.stateBackend ?? const stateBackend = opts.stateBackend ??
@ -75,6 +175,12 @@ export class App {
env: opts.env, env: opts.env,
}); });
this.env = opts.env; this.env = opts.env;
if (
opts.listMethod && !["all", "auto", "latest"].includes(opts.listMethod)
) {
throw new TypeError(`Unknown listMethod: ${opts.listMethod}`);
}
} }
getSkipMode(): ("vs" | "coop")[] { getSkipMode(): ("vs" | "coop")[] {
@ -118,25 +224,6 @@ export class App {
out.push(new FileExporter(state.fileExportPath)); out.push(new FileExporter(state.fileExportPath));
} }
if (exporters.includes("mongodb")) {
if (!state.mongoDbUri) {
const uri = (await this.env.prompts.prompt(
"MongoDB URI is not set. Please enter below.",
)).trim();
if (!uri) {
this.env.logger.error("MongoDB URI is required.");
Deno.exit(1);
}
await this.profile.writeState({
...state,
mongoDbUri: uri,
});
}
out.push(
new MongoDBExporter(this.profile.state.mongoDbUri!),
);
}
return out; return out;
} }
exporterProgress(title: string) { exporterProgress(title: string) {
@ -184,8 +271,10 @@ export class App {
if (skipMode.includes("vs") || exporters.length === 0) { if (skipMode.includes("vs") || exporters.length === 0) {
this.env.logger.log("Skip exporting VS games."); this.env.logger.log("Skip exporting VS games.");
} else { } else {
this.env.logger.log("Fetching battle list..."); const gameListFetcher = new BattleListFetcher(
const gameList = await splatnet.getBattleList(); this.opts.listMethod ?? "auto",
splatnet,
);
const { redraw, endBar } = this.exporterProgress("Export vs games"); const { redraw, endBar } = this.exporterProgress("Export vs games");
const fetcher = new GameFetcher({ const fetcher = new GameFetcher({
@ -204,7 +293,7 @@ export class App {
type: "VsInfo", type: "VsInfo",
fetcher, fetcher,
exporter: e, exporter: e,
gameList, gameListFetcher,
stepProgress: stats[e.name], stepProgress: stats[e.name],
onStep: () => { onStep: () => {
redraw(e.name, progress(stats[e.name])); redraw(e.name, progress(stats[e.name]));
@ -238,10 +327,7 @@ export class App {
if (skipMode.includes("coop") || exporters.length === 0) { if (skipMode.includes("coop") || exporters.length === 0) {
this.env.logger.log("Skip exporting coop games."); this.env.logger.log("Skip exporting coop games.");
} else { } else {
this.env.logger.log("Fetching coop battle list..."); const gameListFetcher = new CoopListFetcher(splatnet);
const coopBattleList = await splatnet.getBattleList(
BattleListType.Coop,
);
const { redraw, endBar } = this.exporterProgress("Export coop games"); const { redraw, endBar } = this.exporterProgress("Export coop games");
const fetcher = new GameFetcher({ const fetcher = new GameFetcher({
@ -258,7 +344,7 @@ export class App {
type: "CoopInfo", type: "CoopInfo",
fetcher, fetcher,
exporter: e, exporter: e,
gameList: coopBattleList, gameListFetcher,
stepProgress: stats[e.name], stepProgress: stats[e.name],
onStep: () => { onStep: () => {
redraw(e.name, progress(stats[e.name])); redraw(e.name, progress(stats[e.name]));
@ -312,34 +398,36 @@ export class App {
throw errors[0]; throw errors[0];
} }
} }
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;
} }
}) async monitorWithNxapi() {
.catch((err) => { this.env.logger.debug("Monitoring with nxapi presence");
errors.push(err); const fetcher = this.env.newFetcher();
this.env.logger.error(`\nFailed to export to ${e.name}:`, err); 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();
}
if (isSplatoon3Active !== this.splatoon3PreviouslyActive) {
this.env.logger.debug(
"Splatoon 3 status has changed from",
this.splatoon3PreviouslyActive,
"to",
isSplatoon3Active,
); );
} }
this.splatoon3PreviouslyActive = isSplatoon3Active;
}
} }
async monitor() { async monitor() {
while (true) { while (true) {
@ -378,7 +466,9 @@ export class App {
}); });
} }
if (this.opts.monitor) { if (this.opts.nxapiPresenceUrl) {
await this.monitorWithNxapi();
} else if (this.opts.monitor) {
await this.monitor(); await this.monitor();
} else { } else {
await this.exportOnce(); await this.exportOnce();
@ -392,30 +482,24 @@ export class App {
* @param gameList ID list of games, sorted by date, newest first * @param gameList ID list of games, sorted by date, newest first
* @param onStep Callback function called when a game is exported * @param onStep Callback function called when a game is exported
*/ */
async exportGameList({ private async exportGameList({
type, type,
fetcher, fetcher,
exporter, exporter,
gameList, gameListFetcher,
stepProgress, stepProgress,
onStep, onStep,
}: { }: {
type: Game["type"]; type: Game["type"];
exporter: GameExporter; exporter: GameExporter;
fetcher: GameFetcher; fetcher: GameFetcher;
gameList: string[]; gameListFetcher: GameListFetcher;
stepProgress: StepProgress; stepProgress: StepProgress;
onStep: () => void; onStep: () => void;
}): Promise<StepProgress> { }): Promise<StepProgress> {
onStep?.(); onStep?.();
const workQueue = [ const workQueue = await gameListFetcher.fetch(exporter);
...await exporter.notExported({
type,
list: gameList,
}),
]
.reverse();
const step = async (id: string) => { const step = async (id: string) => {
const detail = await fetcher.fetch(type, id); const detail = await fetcher.fetch(type, id);

View File

@ -1,31 +1,42 @@
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts"; import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "splashcat / s3si.ts"; export const AGENT_NAME = "s3si.ts";
export const AGENT_VERSION = "1.1.1"; export const S3SI_VERSION = "0.4.12";
export const S3SI_VERSION = "0.4.1"; export const NSOAPP_VERSION = "2.8.1";
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`; export const WEB_VIEW_VERSION = "6.0.0-daea5c11";
export const NSOAPP_VERSION = "2.5.1";
export const WEB_VIEW_VERSION = "4.0.0-d5178440";
export enum Queries { export enum Queries {
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871", HomeQuery =
LatestBattleHistoriesQuery = "0d90c7576f1916469b2ae69f64292c02", "51fc56bbf006caf37728914aa8bc0e2c86a80cf195b4d4027d6822a3623098a8",
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de", LatestBattleHistoriesQuery =
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803", "b24d22fd6cb251c515c2b90044039698aa27bc1fab15801d83014d919cd45780",
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5", RegularBattleHistoriesQuery =
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28", "2fe6ea7a2de1d6a888b7bd3dbeb6acc8e3246f055ca39b80c4531bbcd0727bba",
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd", BankaraBattleHistoriesQuery =
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15", "9863ea4744730743268e2940396e21b891104ed40e2286789f05100b45a0b0fd",
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b", XBattleHistoriesQuery =
"eb5996a12705c2e94813a62e05c0dc419aad2811b8d49d53e5732290105559cb",
EventBattleHistoriesQuery =
"e47f9aac5599f75c842335ef0ab8f4c640e8bf2afe588a3b1d4b480ee79198ac",
PrivateBattleHistoriesQuery =
"fef94f39b9eeac6b2fac4de43bc0442c16a9f2df95f4d367dd8a79d7c5ed5ce7",
VsHistoryDetailQuery =
"f893e1ddcfb8a4fd645fd75ced173f18b2750e5cfba41d2669b9814f6ceaec46",
CoopHistoryQuery =
"0f8c33970a425683bb1bdecca50a0ca4fb3c3641c0b2a1237aedfde9c0cb2b8f",
CoopHistoryDetailQuery =
"42262d241291d7324649e21413b29da88c0314387d8fdf5f6637a2d9d29954ae",
myOutfitCommonDataFilteringConditionQuery = myOutfitCommonDataFilteringConditionQuery =
"d02ab22c9dccc440076055c8baa0fa7a", "ac20c44a952131cb0c9d00eda7bc1a84c1a99546f0f1fc170212d5a6bb51a426",
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8", myOutfitCommonDataEquipmentsQuery =
HistoryRecordQuery = "d9246baf077b2a29b5f7aac321810a77", "45a4c343d973864f7bb9e9efac404182be1d48cf2181619505e9b7cd3b56a6e8",
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd", HistoryRecordQuery =
StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3", "0a62c0152f27c4218cf6c87523377521c2cff76a4ef0373f2da3300079bf0388",
ConfigureAnalyticsQuery =
"2a9302bdd09a13f8b344642d4ed483b9464f20889ac17401e993dfa5c2bb3607",
} }
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts"; export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`; export const USERAGENT = `${AGENT_NAME}/${S3SI_VERSION} (${S3SI_LINK})`;
export const DEFAULT_APP_USER_AGENT = export const DEFAULT_APP_USER_AGENT =
"Mozilla/5.0 (Linux; Android 11; Pixel 5) " + "Mozilla/5.0 (Linux; Android 11; Pixel 5) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " + "AppleWebKit/537.36 (KHTML, like Gecko) " +
@ -94,6 +105,10 @@ export const SPLATNET3_STATINK_MAP: {
"sameride", "sameride",
"380e541b5bc5e49d77ff1a616f1343aeba01d500fee36aaddf8f09d74bd3d3bc": "380e541b5bc5e49d77ff1a616f1343aeba01d500fee36aaddf8f09d74bd3d3bc":
"tripletornado", "tripletornado",
"8a7ee88a06407f4be1595ef8af4d2d2ac22bbf213a622cd19bbfaf4d0f36bcd7":
"teioika",
"a75eac34675bc0d4bd9ca9977cf22472848f89e28e08ee986b4461a3f2af28fc":
"ultra_chakuchi",
}, },
WATER_LEVEL_MAP: { WATER_LEVEL_MAP: {
0: "low", 0: "low",
@ -101,3 +116,5 @@ export const SPLATNET3_STATINK_MAP: {
2: "high", 2: "high",
}, },
}; };
export const SPLATOON3_TITLE_ID = "0100c2500fc20000";

View File

@ -1,133 +0,0 @@
import { MongoDB } from "../../deps.ts";
import { AGENT_VERSION, NSOAPP_VERSION, S3SI_VERSION } from "../constant.ts";
import {
CoopHistoryDetail,
ExportResult,
Game,
GameExporter,
Queries,
RespMap,
Summary,
VsHistoryDetail,
} from "../types.ts";
import { parseHistoryDetailId } from "../utils.ts";
export class MongoDBExporter implements GameExporter {
name = "mongodb";
mongoDbClient: MongoDB.MongoClient;
mongoDb: MongoDB.Db;
battlesCollection: MongoDB.Collection;
jobsCollection: MongoDB.Collection;
summariesCollection: MongoDB.Collection;
constructor(private mongoDbUri: string) {
this.mongoDbClient = new MongoDB.MongoClient(mongoDbUri);
this.mongoDb = this.mongoDbClient.db("splashcat");
this.battlesCollection = this.mongoDb.collection("battles");
this.jobsCollection = this.mongoDb.collection("jobs");
this.summariesCollection = this.mongoDb.collection("summaries");
}
static getGameId(id: string) { // very similar to the file exporter
const { uid, timestamp } = parseHistoryDetailId(id);
return `${uid}_${timestamp}Z`;
}
async notExported(
{ type, list }: { type: Game["type"]; list: string[] },
): Promise<string[]> {
const out: string[] = [];
const collection = type === "CoopInfo"
? this.jobsCollection
: this.battlesCollection;
for (const id of list) {
const uniqueId = MongoDBExporter.getGameId(id);
const countNewStorage = await collection.countDocuments({
gameId: uniqueId,
});
if (countNewStorage === 0) {
out.push(id);
}
}
return out;
}
async exportGame(game: Game): Promise<ExportResult> {
const uniqueId = MongoDBExporter.getGameId(game.detail.id);
const common = {
// this seems like useful data to store...
// loosely modeled after FileExporterTypeCommon
nsoVersion: NSOAPP_VERSION,
agentVersion: AGENT_VERSION,
s3siVersion: S3SI_VERSION,
exportDate: new Date(),
};
const splatNetData = {
...game.detail,
playedTime: new Date(game.detail.playedTime),
};
const body: {
data: Game;
splatNetData:
& Omit<(VsHistoryDetail | CoopHistoryDetail), "playedTime">
& { playedTime: Date };
gameId: string;
} & typeof common = {
...common,
data: game,
splatNetData,
gameId: uniqueId,
};
const isJob = game.type === "CoopInfo";
const collection = isJob ? this.jobsCollection : this.battlesCollection;
const result = await collection.insertOne(body);
const objectId = result.insertedId;
return {
status: "success",
url: `https://new.splatoon.catgirlin.space/battle/${objectId.toString()}`,
};
}
async exportSummary(summary: Summary): Promise<ExportResult> {
const id = summary.uid;
await this.summariesCollection.insertOne({
summaryId: id,
...summary,
});
return {
status: "success",
};
}
async exportStages(
stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"],
): Promise<ExportResult> {
for (const stage of stages) {
await this.mongoDb.collection("stages").updateOne({
"stage.id": stage.id,
}, {
$set: stage,
}, {
upsert: true,
});
}
return {
status: "success",
};
}
}

View File

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

View File

@ -213,20 +213,20 @@ export async function getGToken(
const idToken2: string = respJson?.result?.webApiServerCredential const idToken2: string = respJson?.result?.webApiServerCredential
?.accessToken; ?.accessToken;
const coralUserId: number = respJson?.result?.user?.id; const coralUserId: string = respJson?.result?.user?.id?.toString();
if (!idToken2 || !coralUserId) { if (!idToken2 || !coralUserId) {
throw new APIError({ throw new APIError({
response: resp, response: resp,
json: respJson, json: respJson,
message: message:
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`, `No idToken2 or coralUserId found. Please try again later. (${idToken2.length}, ${coralUserId.length})`,
}); });
} }
return [idToken2, coralUserId] as const; return [idToken2, coralUserId] as const;
}; };
const getGToken = async (idToken: string, coralUserId: number) => { const getGToken = async (idToken: string, coralUserId: string) => {
const { f, request_id: requestId, timestamp } = await callImink({ const { f, request_id: requestId, timestamp } = await callImink({
step: 2, step: 2,
idToken, idToken,
@ -414,7 +414,7 @@ async function callImink(
step: number; step: number;
idToken: string; idToken: string;
userId: string; userId: string;
coralUserId?: number; coralUserId?: string;
env: Env; env: Env;
}, },
): Promise<IminkResponse> { ): Promise<IminkResponse> {

View File

@ -15,7 +15,7 @@ import {
} from "./types.ts"; } from "./types.ts";
import { DEFAULT_ENV, Env } from "./env.ts"; import { DEFAULT_ENV, Env } from "./env.ts";
import { getBulletToken, getGToken } from "./iksm.ts"; import { getBulletToken, getGToken } from "./iksm.ts";
import { parseHistoryDetailId } from "./utils.ts"; import { battleTime, parseHistoryDetailId } from "./utils.ts";
export class Splatnet3 { export class Splatnet3 {
protected profile: Profile; protected profile: Profile;
@ -137,6 +137,12 @@ export class Splatnet3 {
[BattleListType.Bankara]: () => [BattleListType.Bankara]: () =>
this.request(Queries.BankaraBattleHistoriesQuery) this.request(Queries.BankaraBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.bankaraBattleHistories)), .then((r) => getIdsFromGroups(r.bankaraBattleHistories)),
[BattleListType.XBattle]: () =>
this.request(Queries.XBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.xBattleHistories)),
[BattleListType.Event]: () =>
this.request(Queries.EventBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.eventBattleHistories)),
[BattleListType.Private]: () => [BattleListType.Private]: () =>
this.request(Queries.PrivateBattleHistoriesQuery) this.request(Queries.PrivateBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.privateBattleHistories)), .then((r) => getIdsFromGroups(r.privateBattleHistories)),
@ -168,6 +174,29 @@ export class Splatnet3 {
return await this.BATTLE_LIST_TYPE_MAP[battleListType](); return await this.BATTLE_LIST_TYPE_MAP[battleListType]();
} }
// Get all id from all battle list, sort by time, [0] is the latest
async getAllBattleList() {
const ALL_TYPE: BattleListType[] = [
BattleListType.Regular,
BattleListType.Bankara,
BattleListType.XBattle,
BattleListType.Event,
BattleListType.Private,
];
const ids: string[] = [];
for (const type of ALL_TYPE) {
ids.push(...await this.getBattleList(type));
}
const timeMap = new Map<string, Date>(
ids.map((id) => [id, battleTime(id)] as const),
);
return ids.sort((a, b) =>
timeMap.get(b)!.getTime() - timeMap.get(a)!.getTime()
);
}
getBattleDetail( getBattleDetail(
id: string, id: string,
) { ) {
@ -257,12 +286,6 @@ export class Splatnet3 {
CoopHistoryQuery, CoopHistoryQuery,
}; };
} }
async getStageRecords() {
const resp = await this.request(Queries.StageRecordQuery);
return resp;
}
} }
function getIdsFromGroups<T extends { id: string }>( function getIdsFromGroups<T extends { id: string }>(

View File

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

View File

@ -1,4 +1,3 @@
import { splatNet3Types } from "../deps.ts";
import { RankState } from "./state.ts"; import { RankState } from "./state.ts";
import { Queries } from "./constant.ts"; import { Queries } from "./constant.ts";
export { Queries }; export { Queries };
@ -9,6 +8,7 @@ export type VarsMap = {
[Queries.RegularBattleHistoriesQuery]: []; [Queries.RegularBattleHistoriesQuery]: [];
[Queries.BankaraBattleHistoriesQuery]: []; [Queries.BankaraBattleHistoriesQuery]: [];
[Queries.XBattleHistoriesQuery]: []; [Queries.XBattleHistoriesQuery]: [];
[Queries.EventBattleHistoriesQuery]: [];
[Queries.PrivateBattleHistoriesQuery]: []; [Queries.PrivateBattleHistoriesQuery]: [];
[Queries.VsHistoryDetailQuery]: [{ [Queries.VsHistoryDetailQuery]: [{
vsResultId: string; vsResultId: string;
@ -21,7 +21,6 @@ export type VarsMap = {
[Queries.myOutfitCommonDataEquipmentsQuery]: []; [Queries.myOutfitCommonDataEquipmentsQuery]: [];
[Queries.HistoryRecordQuery]: []; [Queries.HistoryRecordQuery]: [];
[Queries.ConfigureAnalyticsQuery]: []; [Queries.ConfigureAnalyticsQuery]: [];
[Queries.StageRecordQuery]: [];
}; };
export type Image = { export type Image = {
@ -146,6 +145,7 @@ export type VsPlayer = {
} | null; } | null;
paint: number; paint: number;
crown: boolean; crown: boolean;
festDragonCert: "NONE" | "DRAGON" | "DOUBLE_DRAGON";
headGear: PlayerGear; headGear: PlayerGear;
clothingGear: PlayerGear; clothingGear: PlayerGear;
@ -237,6 +237,9 @@ export type VsHistoryDetail = {
bankaraMatch: { bankaraMatch: {
earnedUdemaePoint: null | number; earnedUdemaePoint: null | number;
mode: "OPEN" | "CHALLENGE"; mode: "OPEN" | "CHALLENGE";
bankaraPower?: null | {
power?: null | number;
};
} | null; } | null;
festMatch: { festMatch: {
dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON"; dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
@ -268,6 +271,8 @@ export type CoopHistoryPlayerResult = {
name: string; name: string;
id: string; id: string;
}; };
isMyself: boolean;
species: "INKLING" | "OCTOLING";
}; };
weapons: { name: string; image: Image | null }[]; weapons: { name: string; image: Image | null }[];
specialWeapon: null | { specialWeapon: null | {
@ -372,9 +377,6 @@ export type GameExporter = {
) => Promise<string[]>; ) => Promise<string[]>;
exportGame: (game: Game) => Promise<ExportResult>; exportGame: (game: Game) => Promise<ExportResult>;
exportSummary?: (summary: Summary) => Promise<ExportResult>; exportSummary?: (summary: Summary) => Promise<ExportResult>;
exportStages?: (
stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"],
) => Promise<ExportResult>;
}; };
export type BankaraBattleHistories = { export type BankaraBattleHistories = {
@ -420,6 +422,11 @@ export type RespMap = {
}; };
[Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories; [Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories;
[Queries.XBattleHistoriesQuery]: XBattleHistories; [Queries.XBattleHistoriesQuery]: XBattleHistories;
[Queries.EventBattleHistoriesQuery]: {
eventBattleHistories: {
historyGroups: HistoryGroups<BattleListNode>;
};
};
[Queries.PrivateBattleHistoriesQuery]: { [Queries.PrivateBattleHistoriesQuery]: {
privateBattleHistories: { privateBattleHistories: {
historyGroups: HistoryGroups<BattleListNode>; historyGroups: HistoryGroups<BattleListNode>;
@ -554,7 +561,6 @@ export type RespMap = {
xMatchMaxLf: SimpleXRank; xMatchMaxLf: SimpleXRank;
} | null; } | null;
}; };
[Queries.StageRecordQuery]: splatNet3Types.StageRecordResult;
}; };
export type WeaponWithRatio = { export type WeaponWithRatio = {
weapon: { weapon: {
@ -607,10 +613,14 @@ export enum BattleListType {
Latest, Latest,
Regular, Regular,
Bankara, Bankara,
Event,
XBattle,
Private, Private,
Coop, Coop,
} }
export type ListMethod = "latest" | "all" | "auto";
export type StatInkUuidList = { export type StatInkUuidList = {
status: number; status: number;
code: number; code: number;
@ -630,7 +640,7 @@ export type StatInkWeapon = {
}[]; }[];
export type StatInkGear = { export type StatInkGear = {
primary_ability: string; primary_ability: string | null;
secondary_abilities: (string | null)[]; secondary_abilities: (string | null)[];
}; };
@ -656,7 +666,9 @@ export type StatInkPlayer = {
special?: number; special?: number;
gears?: StatInkGears; gears?: StatInkGears;
crown?: "yes" | "no"; crown?: "yes" | "no";
crown_type?: "x" | "100x" | "333x";
disconnected: "yes" | "no"; disconnected: "yes" | "no";
species: "inkling" | "octoling";
}; };
export type StatInkStage = { export type StatInkStage = {
@ -706,6 +718,7 @@ export type StatInkCoopPlayer = {
rescued: number; rescued: number;
defeat_boss: number; defeat_boss: number;
disconnected: "yes" | "no"; disconnected: "yes" | "no";
species: "inkling" | "octoling";
}; };
export type StatInkCoopBoss = { export type StatInkCoopBoss = {
@ -815,6 +828,8 @@ export type StatInkPostBody = {
challenge_lose?: number; challenge_lose?: number;
x_power_before?: number | null; x_power_before?: number | null;
x_power_after?: number | null; x_power_after?: number | null;
bankara_power_before?: number | null;
bankara_power_after?: number | null;
fest_power?: number; // Splatfest Power (Pro) fest_power?: number; // Splatfest Power (Pro)
fest_dragon?: fest_dragon?:
| "10x" | "10x"

View File

@ -188,3 +188,14 @@ export function urlSimplify(url: string): { pathname: string } | string {
return url; return url;
} }
} }
export const battleTime = (id: string) => {
const { timestamp } = parseHistoryDetailId(id);
const dateStr = timestamp.replace(
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
"$1-$2-$3T$4:$5:$6Z",
);
return new Date(dateStr);
};