Compare commits

...

10 Commits

Author SHA1 Message Date
mokurin000
125091c76d fix: GetUserDataApi 2025-07-31 01:40:30 +08:00
mokurin000
1943b5b1f6 todo: fix UserData decryption 2025-07-31 01:13:00 +08:00
mokurin000
c4860b812b docs: add more comments for fields 2025-07-30 23:13:30 +08:00
mokurin000
be2e430fcc chore: add getUserDataApi 2025-07-30 22:40:41 +08:00
mokurin000
f23c92628d log: notice ctrl-c received 2025-07-30 19:25:48 +08:00
mokurin000
d35372b20a feat: correctly handle interrupt 2025-07-30 19:15:48 +08:00
mokurin000
5ad0135deb perf: cache for players scraping 2025-07-30 19:06:17 +08:00
mokurin000
19a0d53624 build: allow to switch async runtime 2025-07-30 17:55:19 +08:00
mokurin000
7670e6f3ae perf: add benchmark for title api encode/decode 2025-07-30 17:02:43 +08:00
mokurin000
789a3566bf refactor: rename api_request to api_call 2025-07-30 16:46:31 +08:00
18 changed files with 1104 additions and 48 deletions

5
.gitignore vendored
View File

@@ -1 +1,6 @@
/target
/*.txt
/players.redb
/players.json

465
Cargo.lock generated
View File

@@ -97,6 +97,26 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "2.9.1"
@@ -203,13 +223,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713c6293af093c202ad318e8f7bdc1de1a36d7a793bb77f7fc6bd6f1788659a9"
dependencies = [
"compio-buf",
"compio-dispatcher",
"compio-driver",
"compio-fs",
"compio-io",
"compio-log",
"compio-macros",
"compio-net",
"compio-process",
"compio-quic",
"compio-runtime",
"compio-signal",
]
[[package]]
@@ -223,6 +247,18 @@ dependencies = [
"libc",
]
[[package]]
name = "compio-dispatcher"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cdf8c613be826be410d8744ab30acc49cc5134a78e2aa25efae9efa44bed6a7"
dependencies = [
"compio-driver",
"compio-runtime",
"flume",
"futures-channel",
]
[[package]]
name = "compio-driver"
version = "0.8.1"
@@ -237,10 +273,13 @@ dependencies = [
"crossbeam-channel",
"crossbeam-queue",
"futures-util",
"io-uring",
"io_uring_buf_ring",
"libc",
"once_cell",
"paste",
"polling",
"slab",
"socket2",
"windows-sys 0.52.0",
]
@@ -314,6 +353,43 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "compio-process"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3867cfe7b23eaae89ff815aba4fdde61cb6fd55f81fd368128300c6b7e645016"
dependencies = [
"cfg-if",
"compio-buf",
"compio-driver",
"compio-io",
"compio-runtime",
"futures-util",
"windows-sys 0.52.0",
]
[[package]]
name = "compio-quic"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f107e044329f1e171930801b09bfc6e764c5e171e45c7a3e382f98561da619a"
dependencies = [
"cfg_aliases",
"compio-buf",
"compio-io",
"compio-log",
"compio-net",
"compio-runtime",
"flume",
"futures-util",
"libc",
"quinn-proto",
"rustc-hash",
"rustls",
"thiserror 2.0.12",
"windows-sys 0.52.0",
]
[[package]]
name = "compio-runtime"
version = "0.8.1"
@@ -330,10 +406,27 @@ dependencies = [
"libc",
"once_cell",
"scoped-tls",
"slab",
"socket2",
"windows-sys 0.52.0",
]
[[package]]
name = "compio-signal"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d2931880b03b33d4df7d2b8a008e93731366d185358c7442fc8d24d5f9c1bd"
dependencies = [
"compio-buf",
"compio-driver",
"compio-runtime",
"libc",
"once_cell",
"os_pipe",
"slab",
"windows-sys 0.52.0",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -407,6 +500,16 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctrlc"
version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"nix",
"windows-sys 0.59.0",
]
[[package]]
name = "curl"
version = "0.4.48"
@@ -493,6 +596,18 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"spin",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -562,6 +677,33 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -651,6 +793,28 @@ dependencies = [
"generic-array",
]
[[package]]
name = "io-uring"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "io_uring_buf_ring"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a8867874ff5758b47c1dac069e6e86541432f9da8be9111c5e94154134f07d0"
dependencies = [
"bytes",
"io-uring",
"rustix",
]
[[package]]
name = "is-terminal"
version = "0.4.16"
@@ -718,6 +882,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "md5"
version = "0.8.0"
@@ -745,6 +915,38 @@ dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -986,6 +1188,15 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
@@ -1004,6 +1215,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quinn-proto"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand",
"ring",
"rustc-hash",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quote"
version = "1.0.40"
@@ -1013,6 +1244,50 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "redb"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef838cd981b5c46e9e91e20e4623e43b29b5c251eb245b34da0cbd2da09ab27"
dependencies = [
"libc",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
@@ -1033,12 +1308,32 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -1061,6 +1356,40 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "rustls"
version = "0.23.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.21"
@@ -1099,9 +1428,11 @@ name = "sdgb-api"
version = "0.1.0"
dependencies = [
"aes",
"bincode",
"cbc",
"chrono",
"cipher",
"compio",
"digest",
"flate2",
"hmac-sha256",
@@ -1110,7 +1441,9 @@ dependencies = [
"serde",
"serde_json",
"snafu",
"spdlog-rs",
"strum 0.27.2",
"tokio",
]
[[package]]
@@ -1118,13 +1451,17 @@ name = "sdgb-cli"
version = "0.1.0"
dependencies = [
"compio",
"ctrlc",
"futures-util",
"nyquest-preset",
"palc",
"redb",
"sdgb-api",
"serde_json",
"snafu",
"spdlog-rs",
"strum 0.27.2",
"tokio",
]
[[package]]
@@ -1308,6 +1645,12 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
@@ -1370,6 +1713,47 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "tinyvec"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
dependencies = [
"backtrace",
"io-uring",
"libc",
"mio",
"pin-project-lite",
"slab",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
@@ -1402,6 +1786,9 @@ name = "tracing-core"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
]
[[package]]
name = "typenum"
@@ -1415,6 +1802,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -1427,6 +1826,27 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@@ -1485,6 +1905,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "widestring"
version = "1.2.0"
@@ -1788,3 +2218,38 @@ checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

View File

@@ -3,11 +3,18 @@ members = ["sdgb-api", "sdgb-cli"]
resolver = "3"
[workspace.dependencies]
sdgb-api = { path = "./sdgb-api" }
sdgb-api = { path = "./sdgb-api", default-features = false }
spdlog-rs = { version = "0.4.3", default-features = false, features = [
"level-debug",
"release-level-info",
] }
snafu = { version = "0.8.6", features = ["backtrace", "rust_1_81"] }
serde_json = "1.0.141"
strum = { version = "0.27.2", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread"] }
compio = { version = "0.15.0", features = ["runtime"] }
[profile.release]
lto = true

View File

@@ -5,10 +5,19 @@ edition = "2024"
license = "GPL-3.0"
[features]
default = ["compio", "bincode"]
compio = ["dep:compio"]
tokio = ["dep:tokio"]
bincode = ["dep:bincode"]
[dependencies]
snafu = { workspace = true }
serde_json = { workspace = true }
strum = {workspace = true}
strum = { workspace = true }
tokio = { workspace = true, optional = true }
compio = { workspace = true, optional = true }
spdlog-rs = { workspace = true }
# hashing
digest = "0.10.7"
@@ -26,6 +35,7 @@ serde = { version = "1.0.219", features = ["derive"] }
# compression / encryption
flate2 = "1.1.2"
cbc = "0.1.2"
cbc = { version = "0.1.2", features = ["alloc"] }
aes = "0.8.4"
cipher = { version = "0.4.4", features = ["block-padding"] }
bincode = { version = "2.0.1", optional = true }

View File

@@ -0,0 +1,22 @@
#![feature(test)]
extern crate test;
use sdgb_api::title::{MaiVersionExt, Sdgb1_50};
#[bench]
pub fn sdgb_150_enc_short(b: &mut test::Bencher) {
b.iter(|| _ = Sdgb1_50::encode(b"Hello world"));
}
#[bench]
pub fn sdgb_150_enc_4k(b: &mut test::Bencher) {
let data = [1u8; 4096];
b.iter(|| _ = Sdgb1_50::encode(data));
}
#[bench]
pub fn sdgb_150_dec_4k(b: &mut test::Bencher) {
let data = [1u8; 4096];
let enc_data = Sdgb1_50::encode(data).unwrap();
b.iter(|| _ = Sdgb1_50::decode(enc_data.clone()));
}

View File

@@ -3,24 +3,36 @@ use snafu::Snafu;
#[derive(Debug, Snafu)]
pub enum ApiError {
JoinError,
#[snafu(display("api returned nothing!"))]
EmptyResponse,
#[snafu(display("encrypt data: {error}"))]
Pad { error: PadError },
Pad {
error: PadError,
},
#[snafu(display("decrypt data: {error}"))]
Unpad { error: UnpadError },
Unpad {
error: UnpadError,
},
#[snafu(display("io error: {source}"))]
#[snafu(context(false))]
IO { source: std::io::Error },
IO {
source: std::io::Error,
},
#[snafu(display("json error: {source}"))]
#[snafu(context(false))]
JSON { source: serde_json::Error },
JSON {
source: serde_json::Error,
},
#[snafu(display("request error: {source}"))]
#[snafu(context(false))]
Request { source: nyquest::Error },
Request {
source: nyquest::Error,
},
}
impl From<UnpadError> for ApiError {

View File

@@ -4,3 +4,5 @@ pub mod title;
mod error;
pub use error::ApiError;
pub use bincode;

View File

@@ -1,19 +1,19 @@
use std::io::{Read, Write as _};
use std::io::Write as _;
use aes::cipher::{
BlockDecryptMut, BlockEncryptMut, BlockSizeUser, KeyIvInit, block_padding::Pkcs7,
};
use digest::generic_array::GenericArray;
use flate2::write::ZlibEncoder;
use flate2::{Compression, read::ZlibDecoder};
use flate2::Compression;
use flate2::write::{ZlibDecoder, ZlibEncoder};
use spdlog::debug;
use crate::error::ApiError;
use crate::title::{MaiVersion, MaiVersionExt, Sdgb1_40, Sdgb1_50};
impl MaiVersionExt for Sdgb1_40 {
fn decode(mut data: impl AsMut<[u8]>) -> Result<Vec<u8>, ApiError> {
let mut decompressed = decompress(data.as_mut());
fn decode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError> {
let mut decompressed = decompress(data.as_ref());
if decompressed.is_empty() {
return Err(ApiError::EmptyResponse);
}
@@ -41,14 +41,20 @@ impl MaiVersionExt for Sdgb1_40 {
}
impl MaiVersionExt for Sdgb1_50 {
fn decode(mut data: impl AsMut<[u8]>) -> Result<Vec<u8>, ApiError> {
if data.as_mut().is_empty() {
fn decode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError> {
let mut data = data.as_ref().to_vec();
if data.is_empty() {
return Err(ApiError::EmptyResponse);
}
let decrypted = decrypt(&mut data, Self::AES_KEY, Self::AES_IV)?;
let decompressed = decompress(decrypted);
Ok(decompressed)
if data.len() % 16 != 0 {
let pad = 16 - (data.len() % 16);
data.resize(data.len() + pad, pad as _);
}
debug!("data size: {}", data.len());
let decrypted = decrypt_vec(&data, Self::AES_KEY, Self::AES_IV)?;
Ok(decompress(decrypted))
}
fn encode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError> {
@@ -72,8 +78,9 @@ fn compress(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError> {
fn decompress(data: impl AsRef<[u8]>) -> Vec<u8> {
let mut buf = Vec::with_capacity(data.as_ref().len() * 2);
let mut decode = ZlibDecoder::new(data.as_ref());
_ = decode.read_to_end(&mut buf);
let mut decode = ZlibDecoder::new(&mut buf);
_ = decode.write_all(data.as_ref());
_ = decode.finish();
buf
}
@@ -104,6 +111,13 @@ fn decrypt<'ct>(
Ok(result)
}
fn decrypt_vec(data: impl AsRef<[u8]>, key: &[u8; 32], iv: &[u8; 16]) -> Result<Vec<u8>, ApiError> {
let key = GenericArray::from_slice(key);
let iv = GenericArray::from_slice(iv);
let decryptor = Aes256CbcDec::new(key, iv);
Ok(decryptor.decrypt_padded_vec_mut::<Pkcs7>(data.as_ref())?)
}
#[cfg(test)]
mod _tests {
@@ -148,4 +162,62 @@ mod _tests {
assert_eq!(enc, data);
Ok(())
}
// FIXME: user data decryption
#[test]
fn test_user_data_dec() -> Result<(), ApiError> {
let data = [
112, 133, 192, 229, 116, 195, 219, 220, 56, 176, 98, 148, 246, 73, 179, 157, 181, 251,
9, 7, 190, 113, 101, 8, 144, 58, 23, 196, 16, 176, 78, 96, 106, 53, 191, 118, 86, 247,
50, 250, 168, 155, 164, 108, 7, 152, 251, 123, 186, 121, 113, 41, 104, 79, 29, 71, 47,
228, 214, 97, 223, 29, 27, 163, 159, 113, 82, 45, 29, 41, 176, 246, 33, 235, 22, 202,
1, 61, 133, 126, 8, 20, 9, 214, 153, 11, 203, 207, 5, 195, 129, 172, 70, 81, 58, 156,
240, 87, 203, 137, 110, 183, 245, 177, 210, 63, 231, 136, 82, 97, 201, 184, 236, 117,
89, 85, 22, 29, 2, 238, 250, 148, 158, 151, 139, 179, 9, 42, 47, 220, 88, 137, 135,
139, 57, 35, 0, 50, 123, 133, 103, 15, 87, 86, 208, 203, 235, 153, 214, 87, 236, 46,
78, 50, 72, 50, 8, 46, 30, 242, 44, 184, 72, 118, 51, 120, 99, 158, 247, 255, 168, 181,
119, 156, 214, 143, 253, 185, 21, 76, 117, 201, 38, 38, 79, 171, 8, 251, 90, 53, 59,
89, 30, 136, 69, 168, 57, 34, 115, 145, 159, 110, 182, 5, 126, 208, 202, 216, 92, 200,
168, 63, 114, 119, 129, 38, 139, 189, 101, 215, 102, 76, 29, 228, 219, 46, 79, 166,
127, 194, 60, 183, 169, 167, 210, 120, 77, 219, 58, 16, 231, 233, 189, 66, 215, 202,
28, 209, 59, 192, 141, 91, 65, 17, 187, 88, 189, 149, 139, 48, 237, 152, 161, 87, 120,
99, 2, 50, 12, 120, 179, 50, 235, 255, 223, 162, 216, 84, 13, 135, 196, 131, 121, 97,
171, 106, 240, 189, 112, 92, 41, 59, 204, 24, 72, 91, 14, 220, 249, 10, 166, 4, 254,
183, 194, 227, 53, 163, 35, 165, 253, 149, 83, 253, 191, 138, 236, 208, 146, 242, 31,
185, 152, 226, 100, 191, 2, 2, 82, 101, 141, 31, 71, 106, 2, 83, 1, 231, 140, 20, 16,
156, 171, 108, 109, 14, 93, 168, 203, 50, 20, 21, 142, 135, 97, 7, 80, 61, 110, 76,
152, 106, 231, 100, 78, 187, 28, 39, 191, 10, 206, 78, 127, 79, 247, 192, 164, 51, 237,
9, 63, 201, 7, 27, 81, 243, 88, 30, 244, 205, 57, 14, 126, 60, 61, 173, 21, 84, 15,
105, 38, 239, 249, 82, 202, 245, 219, 88, 195, 112, 113, 40, 60, 76, 10, 243, 232, 52,
27, 0, 84, 247, 85, 140, 99, 140, 165, 145, 140, 96, 55, 0, 174, 155, 241, 166, 252,
150, 87, 106, 42, 58, 33, 154, 222, 83, 69, 172, 226, 216, 108, 115, 203, 38, 133, 43,
171, 172, 78, 142, 70, 78, 186, 146, 24, 126, 203, 106, 221, 144, 17, 32, 42, 186, 125,
134, 186, 174, 214, 137, 212, 234, 202, 79, 241, 28, 222, 98, 83, 76, 254, 90, 210, 12,
141, 40, 191, 123, 143, 170, 154, 39, 137, 222, 224, 241, 61, 136, 184, 104, 106, 209,
184, 128, 30, 95, 36, 250, 163, 47, 82, 19, 121, 123, 134, 142, 31, 170, 23, 148, 20,
80, 157, 252, 103, 192, 204, 229, 10, 66, 84, 49, 21, 197, 110, 208, 202, 124, 217,
117, 19, 190, 241, 154, 178, 83, 37, 175, 209, 52, 228, 219, 137, 238, 146, 111, 228,
254, 89, 219, 49, 85, 30, 214, 162, 234, 138, 122, 9, 93, 164, 133, 136, 160, 75, 118,
87, 14, 170, 92, 109, 244, 40, 234, 40, 216, 72, 207, 81, 161, 252, 252, 0, 38, 206,
123, 212, 93, 252, 225, 205, 16, 5, 197, 59, 93, 100, 56, 93, 125, 214, 150, 133, 208,
12, 0, 226, 246, 94, 62, 235, 164, 48, 134, 205, 77, 14, 107, 162, 60, 23, 150, 47,
198, 5, 214, 125, 12, 150, 63, 128, 95, 237, 209, 55, 5, 11, 59, 187, 0, 254, 180, 226,
126, 88, 87, 172, 38, 169, 27, 25, 92, 204, 24, 103, 78, 226, 65, 163, 114, 16, 202,
31, 160, 182, 100, 226, 15, 64, 5, 71, 117, 237, 31, 145, 250, 97, 105, 103, 67, 243,
2, 208, 60, 72, 16, 199, 57, 170, 99, 151, 62, 100, 53, 23, 45, 123, 225, 170, 149, 65,
26, 142, 240, 82, 214, 88, 93, 100, 158, 84, 42, 5, 112, 165, 194, 1, 160, 149, 103,
238, 63, 75, 3, 134, 113, 197, 251, 251, 90, 34, 184, 248, 214, 183, 168, 135, 4, 169,
134, 194, 106, 83, 108, 176, 26, 159, 80, 143, 105, 111, 53, 4, 1, 240, 44, 240, 149,
118, 189, 208, 190, 235, 145, 166, 163, 231, 158, 219, 221, 208, 61, 158, 132, 39, 75,
235, 36, 199, 169, 34, 119, 150, 223, 74, 5, 107, 123, 132, 116, 97, 241, 53, 43, 238,
115, 189, 195, 124, 127, 172, 5, 109, 112, 149, 190, 19, 202, 253, 171, 53, 105, 123,
173, 50, 50, 145, 56, 232, 13, 169, 47, 60, 112, 35, 100, 205, 35, 142, 5, 198, 235,
206, 112, 145, 99, 21, 214, 1, 184, 57, 125, 87, 245, 204, 162, 167, 124, 18, 154, 49,
25, 144, 181, 58, 184, 212, 59, 252, 72, 167, 228, 60, 118, 113, 65, 50, 150, 235, 163,
121, 215, 82, 91, 100, 78, 54, 199, 238, 93, 21, 21, 29, 215, 18, 201, 205, 106, 211,
78, 141, 155,
];
let _ = Sdgb1_50::decode(data)?;
Ok(())
}
}

View File

@@ -59,5 +59,9 @@ mod _test {
Sdgb1_50::api_hash(APIMethod::GetUserPreviewApi),
"004cf848f96d393a5f2720101e30b93d"
);
assert_eq!(
Sdgb1_50::api_hash(APIMethod::GetUserDataApi),
"3af1e5b298bb5b7379c94934b2e038c5"
);
}
}

View File

@@ -14,6 +14,7 @@ use nyquest::{
header::{ACCEPT_ENCODING, CONTENT_ENCODING, EXPECT, USER_AGENT},
};
use serde::{Deserialize, Serialize};
use spdlog::debug;
pub trait MaiVersion {
const AES_KEY: &[u8; 32];
@@ -24,7 +25,7 @@ pub trait MaiVersion {
pub trait MaiVersionExt: MaiVersion {
fn encode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError>;
fn decode(data: impl AsMut<[u8]>) -> Result<Vec<u8>, ApiError>;
fn decode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError>;
fn api_hash(api: APIMethod) -> String {
let api_name: &str = api.into();
@@ -37,11 +38,7 @@ pub trait MaiVersionExt: MaiVersion {
format!("{digest:x}")
}
fn api_request<D>(
api: APIMethod,
agent_extra: impl Display,
data: D,
) -> Result<Request, ApiError>
fn api_call<D>(api: APIMethod, agent_extra: impl Display, data: D) -> Result<Request, ApiError>
where
D: Serialize,
{
@@ -66,16 +63,28 @@ pub trait MaiVersionExt: MaiVersion {
fn request_raw<D>(
client: &AsyncClient,
api: APIMethod,
agent_extra: impl Display,
agent_extra: impl Display + Send + 'static,
data: D,
) -> impl Future<Output = Result<Vec<u8>, ApiError>>
where
D: Serialize,
D: Serialize + Send + 'static,
{
#[cfg(feature = "compio")]
use compio::runtime::spawn_blocking;
#[cfg(feature = "tokio")]
use tokio::task::spawn_blocking;
async {
let req = Self::api_request(api, agent_extra, data)?;
let req = spawn_blocking(move || Self::api_call(api, agent_extra, data))
.await
.map_err(|_| ApiError::JoinError)??;
let data = client.request(req).await?.bytes().await?;
let decoded = Self::decode(data)?;
debug!("received: {data:?}");
let decoded = spawn_blocking(move || Self::decode(data))
.await
.map_err(|_| ApiError::JoinError)??;
Ok(decoded)
}
}
@@ -83,11 +92,11 @@ pub trait MaiVersionExt: MaiVersion {
fn request<D, R>(
client: &AsyncClient,
api: APIMethod,
agent_extra: impl Display,
agent_extra: impl Display + Send + 'static,
data: D,
) -> impl Future<Output = Result<R, ApiError>>
where
D: Serialize,
D: Serialize + Send + 'static,
R: for<'a> Deserialize<'a>,
{
async {

View File

@@ -0,0 +1,153 @@
use serde::Deserialize;
use serde::Serialize;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetUserDataApi {
pub user_id: u32,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetUserDataApiResp {
pub user_id: u32,
pub user_data: UserData,
pub ban_state: u32,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserData {
/// your username, in full-width character
pub user_name: String,
/// should be `1`
pub is_net_member: i64,
/// maimile, 舞里程
pub point: i64,
/// 总获取过舞里程
pub total_point: i64,
/// DX RATING
pub player_rating: i64,
pub music_rating: i64,
/// B35
pub player_old_rating: i64,
/// B15
pub player_new_rating: i64,
/// highest total dx rating
pub highest_rating: i64,
pub grade_rating: i64,
/// 级别认定
pub grade_rank: i64,
/// 段位认定
pub course_rank: i64,
/// 友人对战段位
pub class_rank: i64,
pub nameplate_id: i64,
pub frame_id: i64,
pub icon_id: i64,
pub trophy_id: i64,
pub plate_id: i64,
pub title_id: i64,
pub partner_id: i64,
pub chara_slot: Vec<i64>,
pub chara_lock_slot: Vec<i64>,
pub content_bit: i64,
pub select_map_id: i64,
/// 总游玩次数
pub play_count: i64,
/// 当前版本游玩次数
pub current_play_count: i64,
pub play_vs_count: i64,
pub play_sync_count: i64,
pub win_count: i64,
pub help_count: i64,
pub combo_count: i64,
/// 总 DX 分数
pub total_deluxscore: i64,
/// 绿 DX 分数
pub total_basic_deluxscore: i64,
/// 黄 DX 分数
pub total_advanced_deluxscore: i64,
/// 红 DX 分数
pub total_expert_deluxscore: i64,
/// 紫 DX 分数
pub total_master_deluxscore: i64,
/// 白 DX 分数
pub total_re_master_deluxscore: i64,
/// 总 Sync
pub total_sync: i64,
pub total_basic_sync: i64,
pub total_advanced_sync: i64,
pub total_expert_sync: i64,
pub total_master_sync: i64,
pub total_re_master_sync: i64,
pub total_achievement: i64,
pub total_basic_achievement: i64,
pub total_advanced_achievement: i64,
pub total_expert_achievement: i64,
pub total_master_achievement: i64,
pub total_re_master_achievement: i64,
pub event_watched_date: String,
/// 最后游玩ROM版本
pub last_rom_version: String,
/// 最后游玩数据版本
pub last_data_version: String,
/// 上次登陆日期
pub last_login_date: String,
/// 上次游玩结束日期
pub last_play_date: String,
/// 上次双人登陆日期
pub last_pair_login_date: String,
/// 上次免费游玩日期
pub last_trial_play_date: String,
pub last_play_credit: i64,
pub last_play_mode: i64,
/// 上次游玩位置 ID
pub last_place_id: i64,
/// 上次游玩位置
pub last_place_name: Option<String>,
pub last_all_net_id: i64,
/// 上次游玩地区 ID
pub last_region_id: i64,
/// 上次游玩地区
pub last_region_name: String,
pub last_country_code: String,
#[serde(rename = "lastSelectEMoney")]
pub last_select_emoney: i64,
/// 上次功能票
pub last_select_ticket: i64,
/// 上次挑战段位
pub last_select_course: i64,
/// 上次段位计数
pub last_count_course: i64,
pub first_game_id: String,
/// 首次游玩游戏版本
pub first_rom_version: String,
/// 首次游玩数据版本
pub first_data_version: String,
/// 首次游玩
pub first_play_date: String,
pub compatible_cm_version: String,
/// 总觉醒数
pub total_awake: i64,
pub daily_bonus_date: String,
pub daily_course_bonus_date: String,
pub map_stock: i64,
pub rename_credit: i64,
pub friend_regist_skip: i64,
pub cm_last_emoney_credit: i64,
pub cm_last_emoney_brand: i64,
/// 访问密码(国区无)
pub access_code: Option<String>,
/// 好友代码(国区无)
pub friend_code: Option<u32>,
/// 上次游玩 ID
pub last_game_id: Option<u32>,
/// 上次登入狗号
pub last_client_id: Option<String>,
/// 时间戳
pub date_time: Option<u64>,
}

View File

@@ -1,5 +1,6 @@
use std::fmt::Display;
use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
@@ -8,10 +9,10 @@ pub struct GetUserPreviewApi {
pub user_id: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct GetUserPreviewApiResp {
pub user_id: i64,
pub user_id: u32,
pub user_name: String,
pub is_login: bool,
pub last_rom_version: String,

View File

@@ -6,3 +6,9 @@ pub use get_user_preview_api::{GetUserPreviewApi, GetUserPreviewApiResp};
mod user_logout_api;
pub use user_logout_api::{UserLogoutApi, UserLogoutApiResp};
mod user_login_api;
pub use user_login_api::{UserLoginApi, UserLoginApiResp};
mod get_user_data_api;
pub use get_user_data_api::{GetUserDataApi, GetUserDataApiResp};

View File

@@ -0,0 +1,54 @@
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserLoginApi {
pub user_id: u32,
pub region_id: u32,
pub date_time: u64,
pub acsess_code: String,
pub place_id: String,
pub client_id: String,
/// set to `false` is fine
pub is_continue: bool,
/// fixed to 0
pub generic_flag: u8,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserLoginApiResp {
/// - `1`: success
/// - `100`: logged
/// - `102`: QRCode expired
pub return_code: i32,
/// format: yyyy-mm-dd HH:MM:SS
pub last_login_date: Option<String>,
pub login_count: Option<u64>,
pub consecutive_login_count: Option<u64>,
/// needed for some operation
pub login_id: Option<u64>,
}
impl UserLoginApi {
pub fn new(user_id: u32) -> Self {
let date_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|t| t.as_secs())
.unwrap_or_default();
// 爱玩星球焦作解放店
UserLoginApi {
user_id,
date_time,
region_id: 13,
acsess_code: "".to_owned(),
place_id: 3223.to_string(),
is_continue: false,
generic_flag: 0,
client_id: "A63E01E6170".into(),
}
}
}

View File

@@ -5,20 +5,27 @@ edition = "2024"
authors = ["mokurin000"]
description = "CLI tool for SDGB protocol"
[features]
default = ["compio", "cache"]
compio = ["dep:compio", "sdgb-api/compio"]
tokio = ["dep:tokio", "sdgb-api/tokio"]
cache = ["dep:redb"]
[dependencies]
sdgb-api = { workspace = true, features = ["bincode"] }
spdlog-rs = { workspace = true }
snafu = { workspace = true }
sdgb-api = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true, features = ["macros"], optional = true }
compio = { workspace = true, features = ["macros"], optional = true }
nyquest-preset = { version = "0.2.0", features = ["async"] }
compio = { version = "0.15.0", default-features = false, features = [
"runtime",
"macros",
] }
palc = { version = "0.0.1", features = ["derive"] }
spdlog-rs = { version = "0.4.3", default-features = false, features = [
"level-info",
"release-level-info",
] }
futures-util = "0.3.31"
redb = { version = "2.6.1", optional = true }
ctrlc = { version = "3.4.7", features = ["termination"] }

25
sdgb-cli/src/cache/mod.rs vendored Normal file
View File

@@ -0,0 +1,25 @@
use std::sync::LazyLock;
use redb::{Table, TableDefinition, WriteTransaction};
static DATABASE: LazyLock<redb::Database> = LazyLock::new(|| {
redb::Database::builder()
.create("players.redb")
.expect("failed to open database")
});
const DIFINITION: TableDefinition<'_, u32, Vec<u8>> = redb::TableDefinition::new("players");
pub fn write_txn() -> Result<WriteTransaction, redb::Error> {
Ok(DATABASE.begin_write()?)
}
pub fn open_table(write: &WriteTransaction) -> Result<Table<'_, u32, Vec<u8>>, redb::Error> {
Ok(write.open_table(DIFINITION)?)
}
pub fn init_db() -> Result<(), redb::Error> {
let write_txn = DATABASE.begin_write()?;
write_txn.open_table(DIFINITION)?;
write_txn.commit()?;
Ok(())
}

View File

@@ -36,6 +36,13 @@ pub enum Commands {
#[arg(short, long)]
user_id: u32,
},
Userdata {
#[arg(short, long)]
user_id: u32,
},
ListAllUser,
Logout {
#[arg(short, long)]
user_id: u32,

View File

@@ -1,30 +1,58 @@
use std::{
fs::OpenOptions,
io::{self, BufRead},
sync::atomic::{AtomicBool, Ordering},
};
use futures_util::StreamExt;
use nyquest_preset::nyquest::ClientBuilder;
use palc::Parser;
use spdlog::{Level, LevelFilter::MoreSevereEqual};
use sdgb_api::{
ApiError,
all_net::QRCode,
auth_lite::{SDGB, SDHJ, delivery_raw},
title::{
MaiVersionExt, Sdgb1_40, Sdgb1_50,
methods::APIMethod,
model::{
GetUserPreviewApi, GetUserPreviewApiResp, Ping, PingResp, UserLogoutApi,
UserLogoutApiResp,
GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp, Ping,
PingResp, UserLoginApi, UserLoginApiResp, UserLogoutApi, UserLogoutApiResp,
},
},
};
use spdlog::{error, info};
use spdlog::{error, info, warn};
use crate::commands::Cli;
#[cfg(feature = "cache")]
mod cache;
mod commands;
#[compio::main]
static EARLY_QUIT: AtomicBool = AtomicBool::new(false);
#[cfg_attr(feature = "compio", compio::main)]
#[cfg_attr(feature = "tokio", tokio::main)]
async fn main() -> Result<(), Box<dyn snafu::Error>> {
nyquest_preset::register();
if cfg!(debug_assertions) {
spdlog::default_logger().set_level_filter(MoreSevereEqual(Level::Debug));
} else {
spdlog::default_logger().set_level_filter(MoreSevereEqual(Level::Info));
}
ctrlc::set_handler(|| {
warn!("received early-quit request! will abort soon");
EARLY_QUIT.store(true, Ordering::Relaxed);
})?;
let cmd = <Cli as Parser>::parse();
let client = ClientBuilder::default().build_async().await?;
// TODO: refactor via enum_dispatch
match cmd.command {
commands::Commands::Logout { user_id } => {
let logout: UserLogoutApiResp = Sdgb1_50::request(
@@ -78,6 +106,173 @@ async fn main() -> Result<(), Box<dyn snafu::Error>> {
};
println!("{}", String::from_utf8_lossy(&resp));
}
commands::Commands::ListAllUser => {
let mut stdin = io::stdin().lock();
let mut buf = String::new();
let mut user_ids = Vec::new();
while stdin.read_line(&mut buf).is_ok_and(|size| size != 0) {
if buf.is_empty() {
continue;
}
let user_id: u32 = buf.trim().parse()?;
buf.clear();
user_ids.push(user_id);
}
#[cfg(feature = "cache")]
let _ = cache::init_db();
#[cfg(feature = "cache")]
let write = cache::write_txn()?;
#[cfg(feature = "cache")]
let config = sdgb_api::bincode::config::Configuration::<
sdgb_api::bincode::config::LittleEndian,
>::default()
.with_no_limit();
let players = futures_util::stream::iter(user_ids)
.map(async |user_id| {
if EARLY_QUIT.load(Ordering::Relaxed) {
return Err("early skip due to ctrl-c")?;
}
#[cfg(feature = "cache")]
{
use redb::ReadableTable;
use sdgb_api::bincode::borrow_decode_from_slice;
let cache_table = cache::open_table(&write)?;
let data = cache_table.get(user_id)?;
if let Some(data) = data {
let decoded: (GetUserPreviewApiResp, _) =
borrow_decode_from_slice(&data.value(), config)?;
return Ok(decoded.0);
}
}
let resp = Sdgb1_50::request::<_, GetUserPreviewApiResp>(
&client,
APIMethod::GetUserPreviewApi,
user_id,
GetUserPreviewApi { user_id },
)
.await;
match &resp {
Ok(resp) => {
info!("preview: {user_id} succeed");
#[cfg(feature = "cache")]
{
use sdgb_api::bincode::encode_to_vec;
if let Ok(mut table) = cache::open_table(&write)
&& let Ok(encoded) = encode_to_vec(resp, config)
{
_ = table.insert(resp.user_id, encoded);
}
}
}
Err(ApiError::JSON { .. }) => {
warn!("account unregistered: {user_id}");
}
Err(e) => {
error!("preview failed: {e}");
}
}
Result::<_, Box<dyn snafu::Error>>::Ok(resp?)
})
.buffer_unordered(20)
.filter_map(async |r| r.ok())
.collect::<Vec<_>>()
.await;
#[cfg(feature = "cache")]
let _ = write.commit();
let output = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open("players.json")?;
serde_json::to_writer_pretty(output, &players)?;
}
commands::Commands::Userdata { user_id } => {
let login = UserLoginApi::new(user_id);
let date_time = login.date_time;
let Ok(login_resp): Result<UserLoginApiResp, _> =
Sdgb1_50::request(&client, APIMethod::UserLoginApi, user_id, login).await
else {
let logout_resp: UserLogoutApiResp = Sdgb1_50::request(
&client,
APIMethod::UserLogoutApi,
user_id,
UserLogoutApi {
user_id,
date_time,
..Default::default()
},
)
.await?;
info!("logout: {logout_resp:?}");
return Ok(());
};
match login_resp.return_code {
1 => info!("login succeed"),
100 => {
error!("user already logged");
return Ok(());
}
102 => {
error!("QRCode expired");
return Ok(());
}
103 => {
error!("Unregistered userId");
return Ok(());
}
e @ _ => {
error!("unknown login error: {e}");
return Ok(());
}
}
match Sdgb1_50::request::<_, GetUserDataApiResp>(
&client,
APIMethod::GetUserDataApi,
user_id,
GetUserDataApi { user_id },
)
.await
{
Ok(udata) => {
info!("{udata:#?}");
}
Err(e) => {
error!("failed to get userdata: {e}");
}
}
let logout_resp: UserLogoutApiResp = Sdgb1_50::request(
&client,
APIMethod::UserLogoutApi,
user_id,
UserLogoutApi {
user_id,
date_time,
..Default::default()
},
)
.await?;
info!("logout: {logout_resp:?}");
}
}
Ok(())