Compare commits

56 Commits

Author SHA1 Message Date
Ethan O'Brien
956634d8ab Add ability to host webui card image files 2026-02-19 14:04:31 -06:00
Ethan O'Brien
8a1d33bd2b Downgrade jni for now 2026-02-19 12:20:08 -06:00
Ethan O'Brien
aef2c4986d Upgrade usage of JNI 2026-02-19 11:09:55 -06:00
Ethan O'Brien
b35aa35b1a Upgrade dependencies 2026-02-19 10:33:15 -06:00
Ethan O'Brien
0ef0a83685 Remove nodejs from build 2026-02-19 10:18:30 -06:00
Ethan O'Brien
586f4612ed Update readme 2026-02-19 10:18:19 -06:00
Ethan O'Brien
f0183370be Add error message when webui submodules are not pulled 2026-02-19 09:59:16 -06:00
Ethan O'Brien
f4fe727a0e New webui 2026-02-19 09:51:51 -06:00
Ethan O'Brien
d61b27af8f Add coins/primogems and item limits 2026-02-10 14:51:51 -06:00
Ethan O'Brien
8e4300a7ea Fix cheat endpoint and jp client 2026-02-10 14:35:19 -06:00
Ethan O'Brien
8a666a56b5 Add endpoint to cheat 2026-02-06 14:39:49 -06:00
Ethan O'Brien
f4fcbf30ed Add ability to get item list 2026-02-06 14:03:05 -06:00
Ethan O'Brien
db46013ee0 Remove duplicated logic 2026-02-06 13:50:48 -06:00
Ethan O'Brien
0b532ee191 Change behaviour for item limits 2026-02-06 13:32:59 -06:00
Ethan O'Brien
95c253b65a Add ability to pull login bonus list 2026-02-04 11:52:58 -06:00
Ethan O'Brien
c4ba03b289 Add ability to pull all cards from card webui endpoint 2026-02-02 22:27:31 -06:00
Ethan O'Brien
2effc61292 Endpoint fixes 2026-02-02 15:49:24 -06:00
Ethan O'Brien
54e84fbaac Fixes 2026-02-01 22:26:54 -06:00
Ethan O'Brien
41b3ea2c7f Add more stuff 2026-02-01 21:27:22 -06:00
Ethan O'Brien
4836f332f0 Put missing gl keys into exchange json 2026-01-31 18:41:39 -06:00
Ethan O'Brien
da49d06209 Add new webui endpoint 2026-01-30 22:20:23 -06:00
Ethan O'Brien
6688655370 Add super cool Live Clear Rates webui 2025-12-24 21:10:37 -06:00
Ethan O'Brien
955e3819b5 Add music masterdata 2025-12-24 21:09:34 -06:00
Ethan O'Brien
591990a040 Track rage-quits as failed lives 2025-12-24 19:46:30 -06:00
Ethan O'Brien
0fc7f1d36e Redo migration table 2025-12-24 17:42:41 -06:00
Ethan O'Brien
15155bd8df Move migration related userdata functions to a new file 2025-12-24 14:34:13 -06:00
Ethan O'Brien
816589ae22 Fix deck size for new accounts 2025-12-24 14:18:31 -06:00
Ethan O'Brien
d14533d966 Tell workflow to checkout submodules 2025-12-24 13:21:49 -06:00
Ethan O'Brien
697f4188c2 Ship files for offline android/ios servers 2025-12-24 13:13:46 -06:00
Ethan O'Brien
01f3a42613 Add more targets when build 2025-12-03 20:58:07 -06:00
Ethan O'Brien
6d541a81ef Add actions/checkout 2025-11-30 18:29:09 -06:00
Ethan O'Brien
b9f9929ea9 Print version to stdout 2025-11-30 18:27:30 -06:00
Ethan O'Brien
2cc2138eda Update docker build to push version tags 2025-11-30 18:16:03 -06:00
Ethan O'Brien
bab9868355 Update license 2025-11-30 18:09:47 -06:00
Ethan O'Brien
ad0d222c96 Add the (currently android) easter mode 2025-11-30 17:26:26 -06:00
Ethan O'Brien
60eb7d469b Add ability to set datapath outside of cli args 2025-11-30 16:51:08 -06:00
Ethan O'Brien
66f97c48ee Fix ios lib build 2025-11-30 12:01:25 -06:00
Ethan O'Brien
d4a5f0dd11 Add wip android/ios lib builds 2025-11-30 11:55:22 -06:00
Ethan O'Brien
9a072823a0 Move asset server to be integrated with main server 2025-11-30 11:06:59 -06:00
Ethan O'Brien
74a00769aa Move sql get_live_data function to clear_rate.rs 2025-11-30 10:59:07 -06:00
Ethan O'Brien
fd47262f52 Don't lock database operations to a single thread 2025-11-30 10:48:16 -06:00
Ethan O'Brien
b9b344b50c Divide lib a little more 2025-11-30 10:19:22 -06:00
Ethan O'Brien
99da87095b Move code into a lib 2025-11-30 10:08:38 -06:00
Ethan O'Brien
d1ee220edb Remove unused dependency 2025-11-22 19:58:36 -06:00
Ethan O'Brien
3343d4fdd6 Remove reqwest
It uses too many dependencies
2025-11-20 23:38:24 -06:00
Ethan O'Brien
6b34316bad Remove serde_json
While it's better practice, id rather keep it consistent. Maybe we'll get a rewrite someday
2025-11-20 23:37:24 -06:00
Ethan O'Brien
9686f3a6ef Update dependencies 2025-11-20 23:07:00 -06:00
Ethan O'Brien
1172b4bd11 Replace openssl usage in gree 2025-11-20 23:01:35 -06:00
Ethan O'Brien
8da6938fb3 Begin to remove openssl dependency 2025-11-17 23:48:53 -06:00
Ethan O'Brien
76c870547c Add maintenance and files redirect to server endpoints 2025-11-17 23:39:58 -06:00
ethanaobrien
038a651202 I forgot to define user_id..... 2025-05-04 01:14:55 -05:00
ethanaobrien
4aa00fd3d8 Fix for fried list
Man I am hungry and I have no food....
2025-05-03 23:16:02 -05:00
Ethan O'Brien
222021382f Add flags to use max time or not 2025-05-01 21:11:00 -05:00
Ethan O'Brien
1a736155e3 Update dependencies 2025-05-01 12:14:17 -05:00
Ethan O'Brien
e27a020cfb Fix timestamp in friend pages 2025-05-01 12:05:18 -05:00
Ethan O'Brien
b082def65c Check for consume type 2 (paid gems) 2025-01-20 12:38:45 -06:00
69 changed files with 44087 additions and 7922 deletions

View File

@@ -8,6 +8,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checking out branch
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v2
with:
@@ -26,7 +29,7 @@ jobs:
- name: Extract version from Cargo.toml
shell: bash
run: |
echo "EW_VERSION=$(grep "^version" Cargo.toml | sed -E 's/version\s*=\s*["]([^"]*)["]/\1/')" >> $GITHUB_ENV
echo "APP_VERSION=$(grep "^version" Cargo.toml | sed -E 's/version\s*=\s*["]([^"]*)["]/\1/')" >> $GITHUB_ENV
- name: Log in to DockerHub
uses: docker/login-action@v3
@@ -52,10 +55,12 @@ jobs:
push: true
labels: |
gitsha1=${{ github.sha }}
org.opencontainers.image.version=${{ env.EW_VERSION }}
tags: "${{ steps.set-tag.outputs.tags }}"
org.opencontainers.image.version=${{ env.APP_VERSION }}
tags: |
${{ steps.set-tag.outputs.tags }}
docker.io/ethanaobrien/ew:${{ env.APP_VERSION }}
file: "docker/Dockerfile"
platforms: linux/amd64 #,linux/arm64
platforms: linux/amd64, linux/arm64
# arm64 builds OOM without the git fetch setting. c.f.
# https://github.com/rust-lang/cargo/issues/10583

View File

@@ -2,7 +2,8 @@ name: Build release binaries
on:
push:
tags: 'v*'
branches: '**'
tags: '*'
jobs:
build:
@@ -10,41 +11,92 @@ jobs:
steps:
- name: Checking out branch
uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust
shell: bash
run: curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
- name: Set up Node.js
uses: actions/setup-node@v2
- name: Install android NDK
id: setup-ndk
uses: https://github.com/nttld/setup-ndk@v1
with:
node-version: '18'
ndk-version: r29
- name: Create out directory
run: |
mkdir out
- name: Build webui
run: |
cd webui
npm install
npm run build
- name: Build x86_64-unknown-linux-gnu
- name: Install cargo ndk
shell: bash
run: |
. "$HOME/.cargo/env"
cargo build --target x86_64-unknown-linux-gnu --release || echo "Failed to build"
mv target/x86_64-unknown-linux-gnu/release/ew out/ew-x86_64-unknown-linux-gnu
cargo install cargo-ndk
rustup target add aarch64-linux-android
- name: Build x86_64-pc-windows-gnu
- name: Write cargo linker config
shell: bash
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
mkdir -p .cargo
echo "[target.aarch64-linux-android]" > .cargo/config.toml
echo "linker = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang\"" >> .cargo/config.toml
echo "ar = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"" >> .cargo/config.toml
echo "" >> .cargo/config.toml
echo "[target.aarch64-unknown-linux-gnu]" >> .cargo/config.toml
echo "linker = \"aarch64-linux-gnu-gcc\"" >> .cargo/config.toml
echo "ar = \"aarch64-linux-gnu-ar\"" >> .cargo/config.toml
echo "" >> .cargo/config.toml
echo "[target.x86_64-linux-android]" >> .cargo/config.toml
echo "linker = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang\"" >> .cargo/config.toml
echo "ar = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"" >> .cargo/config.toml
- name: Build Stuff
shell: bash
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
. "$HOME/.cargo/env"
sudo apt update && sudo apt install gcc-mingw-w64 -y
rustup target add x86_64-pc-windows-gnu
cargo build --target x86_64-pc-windows-gnu --release || echo "Failed to build"
mv target/x86_64-pc-windows-gnu/release/ew.exe out/ew-x86_64-pc-windows-gnu.exe
export PATH=$PATH:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/
sudo apt update && sudo apt install gcc-mingw-w64 gcc-aarch64-linux-gnu -y
arr=("aarch64-linux-android" "aarch64-unknown-linux-gnu" "x86_64-linux-android" "x86_64-pc-windows-gnu" "x86_64-unknown-linux-gnu")
for target in "${arr[@]}"; do
rustup target add $target
cargo build --target $target --release || echo "Failed to build"
if [[ "$target" == *"windows"* ]]; then
mv target/$target/release/ew.exe out/ew-$target.exe
else
mv target/$target/release/ew out/ew-$target
fi
done
- name: Patch for library build
run: |
sed -i 's|#\[lib\]|\[lib\]|g' Cargo.toml
sed -i 's|#crate-type|crate-type|g' Cargo.toml
sed -i 's|#required-features|required-features|g' Cargo.toml
- name: Build jnilibs
shell: bash
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
. "$HOME/.cargo/env"
cargo ndk -t arm64-v8a build --release --features library
mv target/aarch64-linux-android/release/libew.so out/libew-aarch64-linux-android.so
- name: Upload artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v3
with:
name: output
path: ./out/
- name: Publish release
if: startsWith(github.ref, 'refs/tags/')
uses: actions/forgejo-release@v2
with:
direction: upload

3
.gitignore vendored
View File

@@ -7,3 +7,6 @@ webui/node_modules/
webui/dist/
config.json
docker/data/
.idea/
ndk/
.DS_Store

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "assets"]
path = assets
url = https://git.ethanthesleepy.one/ethanaobrien/sif2-runtime-files
[submodule "webui"]
path = webui
url = https://git.ethanthesleepy.one/ethanaobrien/ew-webui

2198
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,49 @@
[package]
name = "ew"
version = "1.0.0"
edition = "2021"
edition = "2024"
[dependencies]
actix-web = { version = "4.9.0" }
rusqlite = { version = "0.32.1", features = ["bundled"] }
openssl = { version = "0.10", features = ["vendored"] }
reqwest = { version = "0.12", features = ["blocking"] }
clap = { version = "4.5.21", features = ["derive"]}
actix-web = "4.13"
rusqlite = { version = "0.38.0", features = ["bundled"] }
clap = { version = "4.5.59", features = ["derive"]}
base64 = "0.22.1"
json = "0.12.4"
rand = "0.8.5"
rand = "0.10.0"
lazy_static = "1.5.0"
chrono = "0.4.38"
hex = "0.4.3"
hmac = "0.12.1"
md5 = "0.7.0"
md5 = "0.8.0"
urlencoding = "2.1.3"
sha1 = "0.10.6"
substring = "1.4.5"
uuid = { version = "1.11.0", features = ["v7"] }
rsa = "0.9.6"
uuid = { version = "1.21.0", features = ["v7"] }
rsa = { version = "0.9.10", features = ["sha1"] }
mime = "0.3.17"
sha2 = "0.10.8"
include-flate-codegen = "0.3.0"
libflate = "2.1.0"
serde_json = "1.0.133"
serde = { version = "1.0.215", features = ["derive"] }
sha2 = "0.10.9"
include-flate-codegen = "0.3.1"
libflate = "2.2.1"
cbc = { version = "0.1.2", features = ["alloc"] }
aes = "0.8.4"
pem = "3.0.6"
ureq = "3.2.0"
mime_guess = "2.0.5"
include_dir = "0.7.4"
[target.aarch64-linux-android.dependencies]
jni = { version = "0.21.0", features = ["invocation", "default"], optional = true }
[target.aarch64-apple-ios.dependencies]
objc2 = { version = "0.6.3", optional = true }
objc2-foundation = { version = "0.3.2", features = ["NSFileManager"], optional = true }
[build-dependencies]
cc = "1.2"
# To enable this library you MUST comment out lib block below and add --features library
[features]
library = ["jni", "objc2", "objc2-foundation"]
#[lib]
#crate-type = ["cdylib"]
#required-features = ["library"]

View File

@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
ew Copyright (C) 2024-2025 Ethan O'Brien
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View File

@@ -1,56 +0,0 @@
const express = require('express')
const app = express()
const fs = require('fs');
var http = require('http');
const ip = '0.0.0.0';
const port = 8887;
//https://lovelive-schoolidolfestival2-album.akamaized.net
app.use(function (req, res, next) {
console.log(req.method, ":", req.url);
next();
})
function createDirFromFile(path) {
fs.mkdirSync(require('path').dirname(path), { recursive: true });
}
app.get('/*', function (req, res) {
const expectedPath = __dirname + "/resources"+req.url.split("?")[0];
createDirFromFile(expectedPath);
let downloading = [];
if (fs.existsSync(expectedPath)) {
res.sendFile(expectedPath)
} else {
let url;
if (req.url.split("/")[2].length !== 2) {
url = (req.url.toLowerCase().startsWith("/android") || req.url.toLowerCase().startsWith("/ios") ? "https://lovelive-schoolidolfestival2-assets.akamaized.net" : "https://lovelive-schoolidolfestival2-album.akamaized.net") + req.url;
} else {
url = (req.url.toLowerCase().startsWith("/android") || req.url.toLowerCase().startsWith("/ios") ? "https://img-sif2.lovelive-sif2.com" : "https://album-sif2.lovelive-sif2.com") + req.url;
}
const request = require('https').get(url, function(response) {
response.pipe(res);
});
if (downloading.includes(req.url)) return;
require('https').get(url, function(response) {
console.log("Downloading " + req.url);
downloading.push(req.url);
const file = fs.createWriteStream(expectedPath);
response.pipe(file);
// after download completed close filestream
file.on("finish", () => {
file.close();
console.log("Download Completed " + req.url);
});
})
}
})
var httpsServer = http.createServer(app);
httpsServer.listen(port, ip, () => {
let url = 'http://' + ip + ':' + port
console.log('Server is listening at', url)
})

View File

@@ -1,690 +0,0 @@
{
"name": "a",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
"integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
"dependencies": {
"define-data-property": "^1.1.2",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.3",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
"integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
"dependencies": {
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"express": "^4.18.2"
}
}

1
assets Submodule

Submodule assets added at dbb068e72c

16
build.rs Normal file
View File

@@ -0,0 +1,16 @@
fn main() {
let target = std::env::var("TARGET").unwrap();
if target == "aarch64-linux-android" {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rustc-link-arg=-Wl,-z,max-page-size=65536");
cc::Build::new()
.file("src/log.c")
.compile("libc_code.a");
println!("cargo:rerun-if-changed=src/log.c");
}
if !std::fs::exists("webui/index.html").unwrap_or(false) {
panic!("Could not compile crate! Missing webui! Did you pull submodules?");
}
}

View File

@@ -3,18 +3,12 @@ FROM docker.io/library/debian:latest AS builder
# First - build
RUN apt update && apt install -y curl libssl-dev perl git gcc make
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash
RUN apt update && apt install -y nodejs
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
WORKDIR /ew/
COPY ./ ./
WORKDIR /ew/webui/
RUN npm i && npm run build
WORKDIR /ew/
RUN . "$HOME/.cargo/env" && cargo build --release

View File

@@ -1,4 +1,3 @@
version: '3'
services:
sif2-ew:
image: sif2-ew:latest
@@ -14,6 +13,7 @@ services:
HIDDEN: false # Will disable the webui
DISABLE_IMPORTS: false # Will disable account imports
DISABLE_EXPORTS: false # Will disable account exports
#IMAGE_ASSET_PATH: /images/ # Images for cards in webui (will default to the public server)
# Everything below is for the "Help" page
#ANDROID_GLOBAL: "link.to/patched/android/global.apk"
@@ -25,4 +25,7 @@ services:
- 8080:8080
volumes:
- ./data:/data
# You can download from https://git.ethanthesleepy.one/ethanaobrien/ew-webui/releases/download/images/card-thumbnails.zip
# No, you should not have a nested folder EXAMPLE: `images/FILENAME.webp`
#- ./images:/images
restart: unless-stopped

View File

@@ -25,4 +25,6 @@ asset_android_en=$([ "$EN_ANDROID_ASSET_HASH" != "" ] && echo "--en-android-asse
asset_ios_en=$([ "$EN_IOS_ASSET_HASH" != "" ] && echo "--en-ios-asset-hash $EN_IOS_ASSET_HASH" || echo "")
/root/ew/ew --path $directory --port $port --npps4 $npps4 $asset_android_jp $asset_ios_jp $asset_android_en $asset_ios_en $exports $imports $purge $hidden $https --global-android "$ANDROID_GLOBAL" --japan-android "$ANDROID_JAPAN" --global-ios "$IOS_GLOBAL" --japan-ios "$IOS_JAPAN" --assets-url "$ASSET_URL" --max-time $maxTime
image_asset_path=$([ "$IMAGE_ASSET_PATH" != "" ] && echo "--image-asset-path $IMAGE_ASSET_PATH" || echo "")
/root/ew/ew --path $directory --port $port --npps4 $npps4 $asset_android_jp $asset_ios_jp $asset_android_en $asset_ios_en $exports $imports $purge $hidden $https $image_asset_path --global-android "$ANDROID_GLOBAL" --japan-android "$ANDROID_JAPAN" --global-ios "$IOS_GLOBAL" --japan-ios "$IOS_JAPAN" --assets-url "$ASSET_URL" --max-time $maxTime

View File

@@ -5,27 +5,15 @@ A (mostly functioning) server for Love Live! School idol festival 2 MIRACLE LIVE
## Building
### Linux
#### Requirements
- [perl](https://www.perl.org/get.html) (This is normally pre-installed)
- [rust](https://www.rust-lang.org/tools/install)
- [npm](https://www.npmjs.com/)
- The [libssl-dev](https://packages.debian.org/buster/libssl-dev) package. This will vary across distros.
`apt install -y npm libssl-dev perl`
### Windows
#### Requirements
- [Strawberry Perl](https://strawberryperl.com/)
### Requirements
- [rust](https://www.rust-lang.org/tools/install)
### Packaging/Running
**Build npm:**
`cd webui && npm install && npm run build`
**Build Rust:**
**Build:**
Debug: `cargo run`
Release: `cargo build --release`
**Docker:**
`docker build --tag ew --file docker/Dockerfile .`
Will create docker image with tag "ew"

60
src/android.rs Normal file
View File

@@ -0,0 +1,60 @@
use jni::JNIEnv;
use jni::objects::{JClass};
use jni::sys::{jstring, jboolean};
use std::thread;
use jni::objects::JString;
use crate::{run_server, stop_server};
use std::os::raw::c_char;
#[link(name = "c_code", kind = "static")]
unsafe extern "C" {
pub fn android_log(tag: *const c_char, message: *const c_char);
}
#[macro_export]
macro_rules! log_to_logcat {
($tag:expr, $($arg:tt)*) => {
let log_message = format!($($arg)*);
let _ = std::panic::catch_unwind(|| {
let tag = std::ffi::CString::new($tag).unwrap();
let message = std::ffi::CString::new(log_message).unwrap();
unsafe {
crate::android::android_log(tag.as_ptr(), message.as_ptr());
}
});
};
}
#[unsafe(no_mangle)]
extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_startServer<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
data_path: JString<'local>,
easter: jboolean
) -> jstring {
crate::runtime::set_easter_mode(easter != 0);
let data_path: String = env.get_string(&data_path).unwrap().into();
crate::runtime::update_data_path(&data_path);
let output = env.new_string(String::from("Azunyannnn~")).unwrap();
thread::spawn(|| {
run_server(true).unwrap();
});
log_to_logcat!("ew", "running");
output.into_raw()
}
#[unsafe(no_mangle)]
extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_stopServer<'local>(env: JNIEnv<'local>, _class: JClass<'local>) -> jstring {
stop_server();
let output = env.new_string(String::from("I like Yui!")).unwrap();
output.into_raw()
}
#[unsafe(no_mangle)]
extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_setEasterMode<'local>(_env: JNIEnv<'local>, _class: JClass<'local>, easter: jboolean) {
crate::runtime::set_easter_mode(easter != 0);
}

View File

@@ -1,53 +1,44 @@
use openssl::symm::{Cipher, Crypter, Mode};
use openssl::error::ErrorStack;
use base64::{Engine as _, engine::general_purpose};
use rand::Rng;
use rand::RngExt;
use aes::cipher::BlockEncryptMut;
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
const IV_LENGTH: usize = 16;
const KEY: &str = "3559b435f24b297a79c68b9709ef2125";
pub fn decrypt_packet(base64_input: &str) -> Result<String, ErrorStack> {
pub fn decrypt_packet(base64_input: &str) -> Result<String, String> {
if base64_input.len() < IV_LENGTH + 1 {
return Ok(String::new());
}
let base64_buffer = general_purpose::STANDARD.decode(base64_input).unwrap();
let decryption_iv = &base64_buffer[..IV_LENGTH];
let ciphertext = &base64_buffer[IV_LENGTH..];
let mut ciphertext = base64_buffer[IV_LENGTH..].to_vec();
let cipher = Cipher::aes_256_cbc();
let mut decrypter = Crypter::new(cipher, Mode::Decrypt, KEY.as_bytes(), Some(decryption_iv))?;
let decrypted_data = Aes256CbcDec::new(KEY.as_bytes().into(), decryption_iv.into())
.decrypt_padded_mut::<Pkcs7>(&mut ciphertext).ok()
.ok_or(String::from("uhoh"))?;
let mut decrypted_data = vec![0u8; ciphertext.len() + cipher.block_size()];
let mut decrypted_len = decrypter.update(ciphertext, &mut decrypted_data)?;
decrypted_len += decrypter.finalize(&mut decrypted_data[decrypted_len..])?;
decrypted_data.truncate(decrypted_len);
Ok(String::from_utf8(decrypted_data).unwrap())
Ok(String::from_utf8(decrypted_data.to_vec()).unwrap())
}
pub fn encrypt_packet(input: &str) -> Result<String, ErrorStack> {
let cipher = Cipher::aes_256_cbc();
pub fn encrypt_packet(input: &str) -> Result<String, String> {
let encryption_iv = generate_random_iv();
let mut encrypter = Crypter::new(cipher, Mode::Encrypt, KEY.as_bytes(), Some(&encryption_iv))?;
let mut encrypted_data = vec![0u8; input.len() + cipher.block_size()];
let mut encrypted_len = encrypter.update(input.as_bytes(), &mut encrypted_data)?;
encrypted_len += encrypter.finalize(&mut encrypted_data[encrypted_len..])?;
encrypted_data.truncate(encrypted_len);
let encrypted = Aes256CbcEnc::new(KEY.as_bytes().into(), encryption_iv.as_slice().into())
.encrypt_padded_vec_mut::<Pkcs7>(input.as_bytes());
let mut result = encryption_iv.to_vec();
result.extend_from_slice(&encrypted_data);
result.extend_from_slice(&encrypted);
Ok(general_purpose::STANDARD.encode(&result))
}
fn generate_random_iv() -> Vec<u8> {
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
let mut bytes = vec![0u8; IV_LENGTH];
rng.fill(&mut bytes[..]);
bytes

27
src/ios.rs Normal file
View File

@@ -0,0 +1,27 @@
#[cfg(target_os = "ios")]
#[unsafe(no_mangle)]
#[unsafe(link_section = "__DATA,__mod_init_func")]
pub static INITIALIZER: extern "C" fn() = main;
#[cfg(target_os = "ios")]
#[unsafe(no_mangle)]
pub extern "C" fn main() {
let data_path = get_bundle_path().into_os_string().into_string().unwrap();
crate::runtime::update_data_path(&data_path);
std::thread::spawn(|| {
crate::run_server(true).unwrap();
});
}
#[cfg(target_os = "ios")]
use objc2_foundation::{NSFileManager, NSSearchPathDirectory, NSSearchPathDomainMask};
#[cfg(target_os = "ios")]
pub fn get_bundle_path() -> std::path::PathBuf {
unsafe {
let manager = NSFileManager::defaultManager();
let application_support = manager.URLsForDirectory_inDomains(NSSearchPathDirectory::ApplicationSupportDirectory, NSSearchPathDomainMask::UserDomainMask);
return application_support.to_vec_unchecked()[0].to_file_path().unwrap();
}
}

83
src/lib.rs Normal file
View File

@@ -0,0 +1,83 @@
mod static_handlers;
mod options;
mod router;
mod encryption;
mod sql;
pub mod runtime;
#[macro_use]
mod macros;
#[cfg(feature = "library")]
#[cfg(target_os = "android")]
mod android;
#[cfg(feature = "library")]
#[cfg(target_os = "ios")]
mod ios;
use actix_web::{
rt,
App,
HttpServer,
web,
dev::Service
};
use std::time::Duration;
pub use options::get_args;
use runtime::get_data_path;
#[actix_web::main]
pub async fn run_server(in_thread: bool) -> std::io::Result<()> {
let args = get_args();
let port = args.port;
if args.purge {
println!("Purging accounts...");
let ct = crate::router::userdata::purge_accounts();
println!("Purged {} accounts", ct);
}
let rv = HttpServer::new(|| App::new()
.wrap_fn(|req, srv| {
println!("Request: {} {}", req.method(), req.path());
#[cfg(feature = "library")]
#[cfg(target_os = "android")]
log_to_logcat!("ew", "Request: {} {}", req.method(), req.path());
srv.call(req)
})
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
.service(static_handlers::maintenance)
.service(static_handlers::files_jp)
.service(static_handlers::files_gl)
.default_service(web::route().to(router::request))
).bind(("0.0.0.0", port))?.run();
println!("Server started: http://0.0.0.0:{}", port);
println!("Data path is set to {}", args.path);
println!("Sif1 transfer requests will attempt to contact NPPS4 at {}", args.npps4);
if args.https {
println!("Note: gree is set to https mode. http requests will fail on jp clients.");
}
if in_thread {
runtime::set_running(true);
let handle = rv.handle();
rt::spawn(rv);
while runtime::get_running() {
actix_web::rt::time::sleep(Duration::from_millis(100)).await;
}
handle.stop(false).await;
println!("Stopped");
return Ok(());
}
rv.await
}
#[actix_web::main]
pub async fn stop_server() {
runtime::set_running(false);
println!("Stopping");
}

7
src/log.c Normal file
View File

@@ -0,0 +1,7 @@
#include <android/log.h>
#define LOGI(tag, ...) __android_log_print(ANDROID_LOG_INFO, tag, __VA_ARGS__)
void android_log(const char* tag, const char* out) {
LOGI(tag, "%s", out);
}

37
src/macros.rs Normal file
View File

@@ -0,0 +1,37 @@
// include_file macro: includes a file compressed at compile time, and decompresses it on reference. Decreases binary size
#[macro_export]
macro_rules! include_file {
( $s:expr ) => {
{
let file = include_flate_codegen::deflate_file!($s);
let ret = $crate::macros::decode(file);
std::string::String::from_utf8(ret).unwrap()
}
};
}
pub fn decode(bytes: &[u8]) -> Vec<u8> {
use std::io::{Cursor, Read};
let mut dec = libflate::deflate::Decoder::new(Cursor::new(bytes));
let mut ret = Vec::new();
dec.read_to_end(&mut ret).unwrap();
ret
}
#[macro_export]
macro_rules! lock_onto_mutex {
($mutex:expr) => {{
loop {
match $mutex.lock() {
Ok(value) => {
break value;
}
Err(_) => {
$mutex.clear_poison();
std::thread::sleep(std::time::Duration::from_millis(15));
}
}
}
}};
}

View File

@@ -1,213 +1,11 @@
mod encryption;
mod router;
mod sql;
use actix_web::{
rt,
App,
HttpServer,
get,
HttpResponse,
HttpRequest,
web,
dev::Service,
http::header::ContentType
};
use clap::Parser;
use std::fs;
use std::sync::Mutex;
use std::time::Duration;
use lazy_static::lazy_static;
#[get("/index.css")]
async fn css(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.insert_header(ContentType(mime::TEXT_CSS))
.body(include_file!("webui/dist/index.css"))
}
#[get("/index.js")]
async fn js(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.insert_header(ContentType(mime::APPLICATION_JAVASCRIPT_UTF_8))
.body(include_file!("webui/dist/index.js"))
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[arg(short, long, default_value_t = 8080, help = "Port to listen on")]
port: u16,
#[arg(long, default_value = "./", help = "Path to store database files")]
path: String,
#[arg(long, default_value_t = false, help = "Serve gree headers with https. WILL NOT ACCEPT HTTPS REQUESTS")]
https: bool,
#[arg(long, default_value = "http://127.0.0.1:51376", help = "Address to NPPS4 server for sif account linking")]
npps4: String,
//below options are for the "Help" page
#[arg(long, default_value = "", help = "Link to patched android global apk for this server.")]
global_android: String,
#[arg(long, default_value = "", help = "Link to patched android japan apk for this server.")]
japan_android: String,
#[arg(long, default_value = "", help = "Link to patched iOS global apk for this server.")]
global_ios: String,
#[arg(long, default_value = "", help = "Link to patched iOS japan apk for this server.")]
japan_ios: String,
#[arg(long, default_value = "", help = "Link to asset server.")]
assets_url: String,
#[arg(long, default_value_t = 0, help = "Max time returned by the server, in the JSON \"timestamp\" key.")]
max_time: u64,
#[arg(long, default_value_t = false, help = "Disable webui, act completely like the original server")]
hidden: bool,
#[arg(long, default_value_t = false, help = "Purge dead user accounts on startup")]
purge: bool,
#[arg(long, default_value_t = false, help = "Disable user account imports")]
disable_imports: bool,
#[arg(long, default_value_t = false, help = "Disable user account exports")]
disable_exports: bool,
#[arg(long, default_value = "", help = "Asset hash for English iOS client.")]
en_ios_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for JP iOS client.")]
jp_ios_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for English Android client.")]
en_android_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for JP Android client.")]
jp_android_asset_hash: String
}
#[actix_web::main]
async fn run_server(in_thread: bool) -> std::io::Result<()> {
let args = get_args();
let port = args.port;
if args.purge {
println!("Purging accounts...");
let ct = crate::router::userdata::purge_accounts();
println!("Purged {} accounts", ct);
}
let rv = HttpServer::new(|| App::new()
.wrap_fn(|req, srv| {
println!("Request: {}", req.path());
srv.call(req)
})
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
.service(css)
.service(js)
.default_service(web::route().to(router::request))
).bind(("0.0.0.0", port))?.run();
println!("Server started: http://0.0.0.0:{}", port);
println!("Data path is set to {}", args.path);
println!("Sif1 transfer requests will attempt to contact NPPS4 at {}", args.npps4);
if args.https {
println!("Note: gree is set to https mode. http requests will fail on jp clients.");
}
if in_thread {
set_running(true).await;
let handle = rv.handle();
rt::spawn(rv);
while get_running().await {
actix_web::rt::time::sleep(Duration::from_millis(100)).await;
}
handle.stop(false).await;
println!("Stopped");
return Ok(());
}
rv.await
}
#[actix_web::main]
async fn stop_server() {
set_running(false).await;
println!("Stopping");
}
#[cfg(not(feature = "library"))]
fn main() -> std::io::Result<()> {
run_server(false)
ew::runtime::update_data_path(&ew::get_args().path);
ew::run_server(false)
}
pub fn get_args() -> Args {
Args::parse()
}
pub fn get_data_path(file_name: &str) -> String {
let args = get_args();
let mut path = args.path;
while path.ends_with('/') {
path.pop();
}
fs::create_dir_all(&path).unwrap();
format!("{}/{}", path, file_name)
}
// include_file macro: includes a file compressed at compile time, and decompresses it on reference. Decreases binary size
#[macro_export]
macro_rules! include_file {
( $s:expr ) => {
{
let file = include_flate_codegen::deflate_file!($s);
let ret = $crate::decode(file);
std::string::String::from_utf8(ret).unwrap()
}
};
}
pub fn decode(bytes: &[u8]) -> Vec<u8> {
use std::io::{Cursor, Read};
let mut dec = libflate::deflate::Decoder::new(Cursor::new(bytes));
let mut ret = Vec::new();
dec.read_to_end(&mut ret).unwrap();
ret
}
#[macro_export]
macro_rules! lock_onto_mutex {
($mutex:expr) => {{
loop {
match $mutex.lock() {
Ok(value) => {
break value;
}
Err(_) => {
$mutex.clear_poison();
actix_web::rt::time::sleep(std::time::Duration::from_millis(15)).await;
}
}
}
}};
}
lazy_static! {
static ref RUNNING: Mutex<bool> = Mutex::new(false);
}
async fn set_running(running: bool) {
let mut result = lock_onto_mutex!(RUNNING);
*result = running;
}
async fn get_running() -> bool {
let result = lock_onto_mutex!(RUNNING);
*result
#[cfg(feature = "library")]
fn main() {
panic!("Compiled with the library feature! You should load the shared object library and call the exported methods there.");
}

68
src/options.rs Normal file
View File

@@ -0,0 +1,68 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[arg(short, long, default_value_t = 8080, help = "Port to listen on")]
pub port: u16,
#[arg(long, default_value = "./", help = "Path to store database files")]
pub path: String,
#[arg(long, default_value_t = false, help = "Serve gree headers with https. WILL NOT ACCEPT HTTPS REQUESTS")]
pub https: bool,
#[arg(long, default_value = "http://127.0.0.1:51376", help = "Address to NPPS4 server for sif account linking")]
pub npps4: String,
//below options are for the "Help" page
#[arg(long, default_value = "", help = "Link to patched android global apk for this server.")]
pub global_android: String,
#[arg(long, default_value = "", help = "Link to patched android japan apk for this server.")]
pub japan_android: String,
#[arg(long, default_value = "", help = "Link to patched iOS global apk for this server.")]
pub global_ios: String,
#[arg(long, default_value = "", help = "Link to patched iOS japan apk for this server.")]
pub japan_ios: String,
#[arg(long, default_value = "", help = "Link to asset server.")]
pub assets_url: String,
#[arg(long, default_value_t = 0, help = "Max time returned by the server, in the JSON \"timestamp\" key.")]
pub max_time: u64,
#[arg(long, default_value_t = false, help = "Disable webui, act completely like the original server")]
pub hidden: bool,
#[arg(long, default_value_t = false, help = "Purge dead user accounts on startup")]
pub purge: bool,
#[arg(long, default_value_t = false, help = "Disable user account imports")]
pub disable_imports: bool,
#[arg(long, default_value_t = false, help = "Disable user account exports")]
pub disable_exports: bool,
#[arg(long, default_value = "", help = "Asset hash for English iOS client.")]
pub en_ios_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for JP iOS client.")]
pub jp_ios_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for English Android client.")]
pub en_android_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for JP Android client.")]
pub jp_android_asset_hash: String,
#[arg(long, default_value = "", help = "Path to image assets.")]
pub image_asset_path: String
}
pub fn get_args() -> Args {
Args::parse()
}

View File

@@ -170,7 +170,7 @@ async fn api_req(req: HttpRequest, body: String) -> HttpResponse {
pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
let args = crate::get_args();
let headers = req.headers();
if args.hidden && req.path().starts_with("/api/webui/") {
if args.hidden && (req.path().starts_with("/api/webui/") || req.path().starts_with("/live_clear_rate.html")) {
return not_found(headers);
}
if headers.get("aoharu-asset-version").is_none() && req.path().starts_with("/api") && !req.path().starts_with("/api/webui") {
@@ -199,6 +199,7 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
"/api/webui/startLoginbonus" => webui::start_loginbonus(req, body),
"/api/webui/import" => webui::import(req, body),
"/api/webui/set_time" => webui::set_time(req, body),
"/api/webui/cheat" => webui::cheat(req, body),
_ => api_req(req, body).await
}
} else {
@@ -212,9 +213,14 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
"/v1.0/payment/balance" => gree::balance(req),
"/web/announcement" => web::announcement(req),
"/api/webui/userInfo" => webui::user(req),
"/live_clear_rate.html" => clear_rate::clearrate_html(req).await,
"/webui/logout" => webui::logout(req),
"/api/webui/export" => webui::export(req),
"/api/webui/serverInfo" => webui::server_info(req),
"/api/webui/listCards" => webui::get_card_info(req),
"/api/webui/listMusic" => webui::get_music_info(req),
"/api/webui/listLoginBonus" => webui::list_login_bonus(req),
"/api/webui/listItems" => webui::list_items(req),
_ => api_req(req, body).await
}
}

View File

@@ -1,5 +1,5 @@
use json::{object, array, JsonValue};
use actix_web::{HttpRequest};
use actix_web::{HttpRequest, HttpResponse, http::header::ContentType};
use rusqlite::params;
use std::sync::Mutex;
use lazy_static::lazy_static;
@@ -7,10 +7,35 @@ use lazy_static::lazy_static;
use crate::encryption;
use crate::sql::SQLite;
use crate::router::{global, databases};
use crate::include_file;
trait SqlClearRate {
fn get_live_data(&self, id: i64) -> Result<Live, rusqlite::Error>;
}
impl SqlClearRate for SQLite {
fn get_live_data(&self, id: i64) -> Result<Live, rusqlite::Error> {
let conn = rusqlite::Connection::open(self.get_path()).unwrap();
let mut stmt = conn.prepare("SELECT * FROM lives WHERE live_id=?1")?;
stmt.query_row(params!(id), |row| {
Ok(Live {
live_id: row.get(0)?,
normal_failed: row.get(1)?,
normal_pass: row.get(2)?,
hard_failed: row.get(3)?,
hard_pass: row.get(4)?,
expert_failed: row.get(5)?,
expert_pass: row.get(6)?,
master_failed: row.get(7)?,
master_pass: row.get(8)?,
})
})
}
}
lazy_static! {
static ref DATABASE: SQLite = SQLite::new("live_statistics.db", setup_tables);
static ref CACHED_DATA: Mutex<Option<JsonValue>> = Mutex::new(None);
static ref CACHED_HTML_DATA: Mutex<Option<JsonValue>> = Mutex::new(None);
}
pub struct Live {
@@ -25,8 +50,8 @@ pub struct Live {
pub master_pass: i64,
}
fn setup_tables(conn: &SQLite) {
conn.lock_and_exec("CREATE TABLE IF NOT EXISTS lives (
fn setup_tables(conn: &rusqlite::Connection) {
conn.execute_batch("CREATE TABLE IF NOT EXISTS lives (
live_id INT NOT NULL PRIMARY KEY,
normal_failed BIGINT NOT NULL,
normal_pass BIGINT NOT NULL,
@@ -36,11 +61,11 @@ fn setup_tables(conn: &SQLite) {
expert_pass BIGINT NOT NULL,
master_failed BIGINT NOT NULL,
master_pass BIGINT NOT NULL
)", params!());
conn.lock_and_exec("CREATE TABLE IF NOT EXISTS scores (
);
CREATE TABLE IF NOT EXISTS scores (
live_id INT NOT NULL PRIMARY KEY,
score_data TEXT NOT NULL
)", params!());
);").unwrap();
}
fn update_live_score(id: i64, uid: i64, score: i64) {
@@ -127,6 +152,18 @@ pub fn live_completed(id: i64, level: i32, failed: bool, score: i64, uid: i64) {
};
}
fn get_song_title(live_id: i32, english: bool) -> String {
let details = if english {
databases::MUSIC_EN[live_id.to_string()].clone()
} else {
databases::MUSIC[live_id.to_string()].clone()
};
if !details.is_null() {
return details["name"].to_string();
}
String::from("Unknown Song")
}
fn get_pass_percent(failed: i64, pass: i64) -> String {
let total = (failed + pass) as f64;
if failed + pass == 0 {
@@ -213,3 +250,101 @@ pub fn ranking(_req: HttpRequest, body: String) -> Option<JsonValue> {
"ranking_list": rank
})
}
fn get_html() -> JsonValue {
let lives = DATABASE.lock_and_select_all("SELECT live_id FROM lives", params!()).unwrap();
let mut table = String::new();
for id in lives.members() {
let live_id = id.as_i64().unwrap();
let info = match DATABASE.get_live_data(live_id) {
Ok(i) => i,
Err(_) => continue,
};
let calc_rate = |pass: i64, fail: i64| -> f64 {
let total = pass + fail;
if total == 0 { 0.0 } else { pass as f64 / total as f64 }
};
let title_jp = get_song_title(info.live_id, false);
let title_en = get_song_title(info.live_id, true);
let normal_txt = get_pass_percent(info.normal_failed, info.normal_pass);
let hard_txt = get_pass_percent(info.hard_failed, info.hard_pass);
let expert_txt = get_pass_percent(info.expert_failed, info.expert_pass);
let master_txt = get_pass_percent(info.master_failed, info.master_pass);
let normal_plays = info.normal_pass + info.normal_failed;
let hard_plays = info.hard_pass + info.hard_failed;
let expert_plays = info.expert_pass + info.expert_failed;
let master_plays = info.master_pass + info.master_failed;
let normal_rate_sort = calc_rate(info.normal_pass, info.normal_failed);
let hard_rate_sort = calc_rate(info.hard_pass, info.hard_failed);
let expert_rate_sort = calc_rate(info.expert_pass, info.expert_failed);
let master_rate_sort = calc_rate(info.master_pass, info.master_failed);
table.push_str(&format!(
r#"<tr>
<td class="title-cell"
data-val="{title_jp}"
data-title-en="{title_en}"
data-title-jp="{title_jp}">
{title_jp}
</td>
<td data-plays="{normal_plays}" data-rate="{normal_rate_sort}">
<span class="rate-text">{normal_txt}</span>
<span class="meta-text">{normal_plays} plays</span>
</td>
<td data-plays="{hard_plays}" data-rate="{hard_rate_sort}">
<span class="rate-text">{hard_txt}</span>
<span class="meta-text">{hard_plays} plays</span>
</td>
<td data-plays="{expert_plays}" data-rate="{expert_rate_sort}">
<span class="rate-text">{expert_txt}</span>
<span class="meta-text">{expert_plays} plays</span>
</td>
<td data-plays="{master_plays}" data-rate="{master_rate_sort}">
<span class="rate-text">{master_txt}</span>
<span class="meta-text">{master_plays} plays</span>
</td>
</tr>"#
));
}
let html = include_file!("src/router/clear_rate_template.html").replace("{{TABLEBODY}}", &table);
object!{
"cache": html,
"last_updated": global::timestamp()
}
}
async fn get_clearrate_html() -> String {
let cache = {
let mut result = crate::lock_onto_mutex!(CACHED_HTML_DATA);
if result.is_none() {
result.replace(get_html());
}
result.as_ref().unwrap().clone()
};
if cache["last_updated"].as_u64().unwrap() + (60 * 60) < global::timestamp() {
let mut result = crate::lock_onto_mutex!(CACHED_HTML_DATA);
result.replace(get_html());
}
cache["cache"].to_string()
}
pub async fn clearrate_html(_req: HttpRequest) -> HttpResponse {
let html = get_clearrate_html().await;
HttpResponse::Ok()
.content_type(ContentType::html())
.body(html)
}

View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Live Clear Rates</title>
<style>
:root {
--bg-color: #f4f4f9;
--text-color: #333;
--table-bg: #ffffff;
--primary-color: #6c5ce7;
--primary-hover: #5649c0;
--header-text: #ffffff;
--border-color: #dddddd;
--row-even: #f8f9fa;
--row-hover: #eef2ff;
--meta-text: #666;
--shadow: rgba(0, 0, 0, 0.1);
}
body.dark-mode {
--bg-color: #18181b;
--text-color: #e4e4e7;
--table-bg: #27272a;
--primary-color: #818cf8;
--primary-hover: #6366f1;
--header-text: #ffffff;
--border-color: #3f3f46;
--row-even: #2f2f35;
--row-hover: #353540;
--meta-text: #a1a1aa;
--shadow: rgba(0, 0, 0, 0.4);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
padding: 20px;
transition: background-color 0.3s, color 0.3s;
}
h1 { text-align: center; margin-bottom: 20px; }
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
border: 2px solid var(--primary-color);
background: transparent;
color: var(--text-color);
border-radius: 20px;
cursor: pointer;
font-weight: 600;
transition: 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover, .btn.active {
background-color: var(--primary-color);
color: var(--header-text);
}
.table-container {
overflow-x: auto;
box-shadow: 0 0 20px var(--shadow);
border-radius: 8px;
background: var(--table-bg);
}
table { width: 100%; border-collapse: collapse; min-width: 600px; }
th, td {
padding: 12px 15px;
text-align: center;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: var(--primary-color);
color: var(--header-text);
cursor: pointer;
user-select: none;
position: sticky;
top: 0;
white-space: nowrap;
}
th:hover { background-color: var(--primary-hover); }
tr:nth-of-type(even) { background-color: var(--row-even); }
tr:last-of-type { border-bottom: 2px solid var(--primary-color); }
tr:hover { background-color: var(--row-hover); transition: 0.2s; }
th::after { content: ' \2195'; opacity: 0.3; font-size: 0.8em; margin-left: 5px; }
th.asc::after { content: ' \2191'; opacity: 1; }
th.desc::after { content: ' \2193'; opacity: 1; }
.meta-text { font-size: 0.85em; color: var(--meta-text); display: block; margin-top: 4px; }
.rate-text { font-weight: bold; font-size: 1.1em; }
.title-cell { text-align: left; font-weight: 600; }
</style>
</head>
<body>
<h1>Live Clear Rates</h1>
<div class="controls">
<button class="btn" id="btnLang" onclick="toggleLanguage()">Language: JP</button>
<button class="btn active" id="btnPlays" onclick="setSortMode('plays')">Sort by Plays</button>
<button class="btn" id="btnRate" onclick="setSortMode('rate')">Sort by Rate %</button>
<button class="btn" onclick="toggleTheme()">Dark Mode 🌓</button>
</div>
<div class="table-container">
<table id="rateTable">
<thead>
<tr>
<th onclick="sortTable(0)">Song Title</th>
<th onclick="sortTable(1)">Normal</th>
<th onclick="sortTable(2)">Hard</th>
<th onclick="sortTable(3)">Expert</th>
<th onclick="sortTable(4)">Master</th>
</tr>
</thead>
<tbody>
{{TABLEBODY}}
</tbody>
</table>
</div>
<script>
let currentSortMode = 'plays';
let currentLang = localStorage.getItem('lang') || 'jp';
updateLanguageDisplay();
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const isDark = document.body.classList.contains('dark-mode');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
if (localStorage.getItem('theme') === 'dark') {
document.body.classList.add('dark-mode');
}
function toggleLanguage() {
currentLang = currentLang === 'jp' ? 'en' : 'jp';
localStorage.setItem('lang', currentLang);
updateLanguageDisplay();
}
function updateLanguageDisplay() {
const btn = document.getElementById('btnLang');
btn.innerText = 'Language: ' + (currentLang === 'jp' ? 'JP' : 'EN');
const titleCells = document.querySelectorAll('.title-cell');
titleCells.forEach(td => {
const newTitle = td.getAttribute(`data-title-${currentLang}`);
if (newTitle) {
td.innerText = newTitle;
td.setAttribute('data-val', newTitle);
}
});
}
function setSortMode(mode) {
currentSortMode = mode;
document.getElementById('btnPlays').classList.toggle('active', mode === 'plays');
document.getElementById('btnRate').classList.toggle('active', mode === 'rate');
}
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("rateTable");
switching = true;
dir = "desc";
var headers = table.getElementsByTagName("TH");
for (var h of headers) { h.classList.remove("asc", "desc"); }
while (switching) {
switching = false;
rows = table.rows;
for (i = 1; i < (rows.length - 1); i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
let attrName = (n === 0) ? 'data-val' : (currentSortMode === 'plays' ? 'data-plays' : 'data-rate');
var xVal = x.getAttribute(attrName);
var yVal = y.getAttribute(attrName);
var xNum = parseFloat(xVal);
var yNum = parseFloat(yVal);
if (!isNaN(xNum) && !isNaN(yNum)) {
if (dir == "asc") {
if (xNum > yNum) { shouldSwitch = true; break; }
} else if (dir == "desc") {
if (xNum < yNum) { shouldSwitch = true; break; }
}
} else {
// String sorting (for titles)
if (dir == "asc") {
if (xVal.toLowerCase() > yVal.toLowerCase()) { shouldSwitch = true; break; }
} else if (dir == "desc") {
if (xVal.toLowerCase() < yVal.toLowerCase()) { shouldSwitch = true; break; }
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount ++;
} else {
if (switchcount == 0 && dir == "desc") {
dir = "asc";
switching = true;
}
}
}
headers[n].classList.add(dir);
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,40 @@ lazy_static! {
}
info
};
pub static ref MUSIC: JsonValue = {
let mut info = object!{};
let items = json::parse(&include_file!("src/router/databases/json/music.json")).unwrap();
for live in LIVE_LIST.entries() {
info[live.1["id"].to_string()] = loop {
let mut val = object!{};
for data in items.members() {
if live.1["masterMusicId"] == data["id"] {
val = data.clone();
break;
}
}
break val;
};
};
info
};
pub static ref MUSIC_EN: JsonValue = {
let mut info = object!{};
let items = json::parse(&include_file!("src/router/databases/json/global/music.json")).unwrap();
for live in LIVE_LIST.entries() {
info[live.1["id"].to_string()] = loop {
let mut val = object!{};
for data in items.members() {
if live.1["masterMusicId"] == data["id"] {
val = data.clone();
break;
}
}
break val;
};
};
info
};
pub static ref RANKS: JsonValue = {
json::parse(&include_file!("src/router/databases/json/user_rank.json")).unwrap()
};

View File

@@ -1,6 +1,6 @@
use json::{JsonValue, object, array};
use actix_web::HttpRequest;
use rand::Rng;
use rand::RngExt;
use crate::encryption;
use crate::include_file;
@@ -51,8 +51,8 @@ fn save_event_data(key: &str, event_id: u32, data: JsonValue) {
}
fn get_random_song() -> JsonValue {
let mut rng = rand::thread_rng();
let random_number = rng.gen_range(0..=databases::LIVES.len());
let mut rng = rand::rng();
let random_number = rng.random_range(0..=databases::LIVES.len());
object!{
song: databases::LIVES[random_number]["masterMusicId"].clone(),
score: (databases::LIVES[random_number]["scoreC"].as_f64().unwrap() * 1.75).round() as i64
@@ -101,16 +101,17 @@ pub fn event(req: HttpRequest, body: String) -> Option<JsonValue> {
let key = global::get_login(req.headers(), &body);
let body = &encryption::decrypt_packet(&body).unwrap();
let body: EventGet = serde_json::from_str(body).unwrap();
let body = json::parse(&body).unwrap();
let mut event = get_event_data(&key, body.master_event_id);
let master_event_id = body["master_event_id"].as_u32().unwrap();
let mut event = get_event_data(&key, master_event_id);
let is_star_event = STAR_EVENT_IDS.contains(&body.master_event_id);
let is_star_event = STAR_EVENT_IDS.contains(&master_event_id);
if is_star_event {
let user = userdata::get_acc(&key);
let old = event["star_event"]["star_level"].as_i64().unwrap();
event["star_event"]["star_level"] = get_star_rank(get_points(body.master_event_id, &user)).into();
event["star_event"]["star_level"] = get_star_rank(get_points(master_event_id, &user)).into();
let leveled = old != event["star_event"]["star_level"].as_i64().unwrap();
let mut all_clear = 1;
@@ -126,18 +127,18 @@ pub fn event(req: HttpRequest, body: String) -> Option<JsonValue> {
switch_music(&mut event, 3);
switch_music(&mut event, 4);
switch_music(&mut event, 5);
save_event_data(&key, body.master_event_id, event.clone());
save_event_data(&key, master_event_id, event.clone());
}
event["point_ranking"]["point"] = get_points(body.master_event_id, &user).into();
event["point_ranking"]["rank"] = get_rank(body.master_event_id, user["user"]["id"].as_u64().unwrap()).into();
event["point_ranking"]["point"] = get_points(master_event_id, &user).into();
event["point_ranking"]["rank"] = get_rank(master_event_id, user["user"]["id"].as_u64().unwrap()).into();
if leveled {
save_event_data(&key, body.master_event_id, event.clone());
save_event_data(&key, master_event_id, event.clone());
event["star_event"]["is_star_event_update"] = 1.into();
} else {
save_event_data(&key, body.master_event_id, event.clone());
save_event_data(&key, master_event_id, event.clone());
}
}
@@ -149,17 +150,18 @@ pub fn star_event(req: HttpRequest, body: String) -> Option<JsonValue> {
let user = userdata::get_acc(&key);
let body = &encryption::decrypt_packet(&body).unwrap();
let body: StarEvent = serde_json::from_str(body).unwrap();
let body = json::parse(&body).unwrap();
let master_event_id = body["master_event_id"].as_u32().unwrap();
let mut event = get_event_data(&key, body.master_event_id);
let mut event = get_event_data(&key, master_event_id);
let mut star_event = event["star_event"].clone();
star_event["is_inherited_level_reward"] = 0.into();
event["star_event"]["star_level"] = get_star_rank(get_points(body.master_event_id, &user)).into();
event["star_event"]["star_level"] = get_star_rank(get_points(master_event_id, &user)).into();
star_event["is_star_level_up"] = 1.into();
save_event_data(&key, body.master_event_id, event.clone());
save_event_data(&key, master_event_id, event.clone());
Some(object!{
star_event: star_event,
@@ -172,15 +174,16 @@ pub fn change_target_music(req: HttpRequest, body: String) -> Option<JsonValue>
let key = global::get_login(req.headers(), &body);
let body = &encryption::decrypt_packet(&body).unwrap();
let body: StarEventChangeTargetMusic = serde_json::from_str(body).unwrap();
let body = json::parse(&body).unwrap();
let master_event_id = body["master_event_id"].as_u32().unwrap();
let mut event = get_event_data(&key, body.master_event_id);
let mut event = get_event_data(&key, master_event_id);
event["star_event"]["music_change_count"] = (event["star_event"]["music_change_count"].as_i32().unwrap() + 1).into();
switch_music(&mut event, body.position as i32);
switch_music(&mut event, body["position"].as_i32().unwrap());
save_event_data(&key, body.master_event_id, event.clone());
save_event_data(&key, master_event_id, event.clone());
Some(event["star_event"].clone())
}
@@ -189,17 +192,18 @@ pub fn set_member(req: HttpRequest, body: String) -> Option<JsonValue> {
let key = global::get_login(req.headers(), &body);
let body = &encryption::decrypt_packet(&body).unwrap();
let body: EventSetMember = serde_json::from_str(body).unwrap();
let body = json::parse(&body).unwrap();
let master_event_id = body["master_event_id"].as_u32().unwrap();
let mut event = get_event_data(&key, body.master_event_id);
let mut event = get_event_data(&key, master_event_id);
event["member_ranking"] = object!{
master_character_id: body.master_character_id,
master_character_id: body["master_character_id"].clone(),
rank: 0,
point: 0
};
save_event_data(&key, body.master_event_id, event.clone());
save_event_data(&key, master_event_id, event.clone());
Some(object!{
event_member: event["member_ranking"].clone()
@@ -221,17 +225,18 @@ fn get_rank(event: u32, user_id: u64) -> u32 {
pub async fn ranking(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = &encryption::decrypt_packet(&body).unwrap();
let body: EventRankingGet = serde_json::from_str(body).unwrap();
let scores = crate::router::event_ranking::get_scores_json().await[body.master_event_id.to_string()].clone();
let body = json::parse(&body).unwrap();
let master_event_id = body["master_event_id"].as_u32().unwrap();
let scores = crate::router::event_ranking::get_scores_json().await[master_event_id as usize].clone();
let mut rv = array![];
let mut i=1;
let start = if body.user_id == 0 { body.start_rank } else { get_rank(body.master_event_id, body.user_id) };
let start = if body["user_id"] == 0 { body["start_rank"].as_u32().unwrap() } else { get_rank(master_event_id, body["user_id"].as_u64().unwrap()) };
for score in scores.members() {
if i >= start && start + body.count >= i {
if i >= start && start + body["count"].as_u32().unwrap() >= i {
rv.push(score.clone()).unwrap();
i += 1;
}
if start + body.count >= i {
if start + body["count"].as_u32().unwrap() >= i {
break;
}
}
@@ -365,41 +370,3 @@ pub fn event_end(req: HttpRequest, body: String) -> Option<JsonValue> {
pub fn event_skip(req: HttpRequest, body: String) -> Option<JsonValue> {
event_live(req, body, true)
}
// Start request structs
// These start with CJsonSendParam in the source
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct StarEventChangeTargetMusic {
master_event_id: u32,
position: u32
}
#[derive(Serialize, Deserialize)]
struct EventGet {
master_event_id: u32
}
#[derive(Serialize, Deserialize)]
struct EventSetMember {
master_event_id: u32,
master_character_id: u32
}
#[derive(Serialize, Deserialize)]
struct StarEvent {
master_event_id: u32
}
#[derive(Serialize, Deserialize)]
struct EventRankingGet {
master_event_id: u32,
ranking_type: i32,
ranking_group_type: i32,
user_id: u64,
start_rank: u32,
count: u32,
group_id: u64
}

View File

@@ -11,11 +11,11 @@ lazy_static! {
static ref CACHED_DATA: Mutex<Option<JsonValue>> = Mutex::new(None);
}
fn setup_tables(conn: &SQLite) {
conn.lock_and_exec("CREATE TABLE IF NOT EXISTS scores (
fn setup_tables(conn: &rusqlite::Connection) {
conn.execute_batch("CREATE TABLE IF NOT EXISTS scores (
event_id INT NOT NULL PRIMARY KEY,
score_data TEXT NOT NULL
)", params!());
);").unwrap();
}
pub fn live_completed(event_id: u32, uid: i64, score: i64, star_level: i64) {

View File

@@ -9,6 +9,7 @@ pub const FRIEND_LIMIT: usize = 40;
pub fn friend(req: HttpRequest, body: String) -> Option<JsonValue> {
let key = global::get_login(req.headers(), &body);
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let user_id = userdata::get_acc(&key)["user"]["id"].as_i64().unwrap();
let friends = userdata::get_acc_friends(&key);
let mut rv = array![];
@@ -24,7 +25,9 @@ pub fn friend(req: HttpRequest, body: String) -> Option<JsonValue> {
};
for uid in rv_data.members() {
rv.push(global::get_user(uid.as_i64().unwrap(), &friends, false)).unwrap();
let mut user = global::get_user(uid.as_i64().unwrap(), &friends, false);
user["user"]["last_login_time"] = global::set_time(user["user"]["last_login_time"].as_u64().unwrap_or(0), user_id, false).into();
rv.push(user).unwrap();
}
Some(object!{
@@ -52,10 +55,11 @@ pub fn recommend(req: HttpRequest, body: String) -> Option<JsonValue> {
let mut rv = array![];
for uid in random.members() {
let user = global::get_user(uid.as_i64().unwrap(), &friends, false);
let mut user = global::get_user(uid.as_i64().unwrap(), &friends, false);
if user["user"]["friend_request_disabled"] == 1 || user.is_empty() {
continue;
}
user["user"]["last_login_time"] = global::set_time(user["user"]["last_login_time"].as_u64().unwrap_or(0), user_id, false).into();
rv.push(user).unwrap();
}

View File

@@ -9,40 +9,54 @@ use uuid::Uuid;
use crate::encryption;
use crate::router::{userdata, gree, items};
use crate::runtime::get_easter_mode;
pub const ASSET_VERSION: &str = "5260ff15dff8ba0c00ad91400f515f55";
pub const ASSET_HASH_ANDROID: &str = "d210b28037885f3ef56b8f8aa45ac95b";
pub const ASSET_HASH_IOS: &str = "dd7175e4bcdab476f38c33c7f34b5e4d";
pub const ASSET_VERSION_GL: &str = "5260ff15dff8ba0c00ad91400f515f55";
pub const ASSET_HASH_ANDROID_GL: &str = "d210b28037885f3ef56b8f8aa45ac95b";
pub const ASSET_HASH_IOS_GL: &str = "dd7175e4bcdab476f38c33c7f34b5e4d";
pub const ASSET_VERSION_JP: &str = "4c921d2443335e574a82e04ec9ea243c";
pub const ASSET_HASH_ANDROID_JP: &str = "67f8f261c16b3cca63e520a25aad6c1c";
pub const ASSET_HASH_IOS_JP: &str = "b8975be8300013a168d061d3fdcd4a16";
pub const ASSET_HASH_ANDROID_EASTER_GL: &str = "da7ae831381c3f29337caa9891db7e6a";
pub const ASSET_HASH_ANDROID_EASTER_JP: &str = "eac0cad61c82bf2e31fc596555747d11";
pub fn get_asset_hash(asset_version: String, android: bool) -> String {
let args = crate::get_args();
if asset_version == ASSET_VERSION_JP {
if android {
if asset_version == ASSET_VERSION_JP {
if args.jp_android_asset_hash != String::new() {
args.jp_android_asset_hash
&args.jp_android_asset_hash
} else if get_easter_mode() {
ASSET_HASH_ANDROID_EASTER_JP
} else {
ASSET_HASH_ANDROID_JP.to_string()
ASSET_HASH_ANDROID_JP
}
} else if args.jp_ios_asset_hash != String::new() {
args.jp_ios_asset_hash
} else {
ASSET_HASH_IOS_JP.to_string()
}
} else if android {
if args.en_android_asset_hash != String::new() {
args.en_android_asset_hash
&args.en_android_asset_hash
} else if get_easter_mode() {
ASSET_HASH_ANDROID_EASTER_GL
} else {
ASSET_HASH_ANDROID.to_string()
ASSET_HASH_ANDROID_GL
}
}
} else if args.en_ios_asset_hash != String::new() {
args.en_ios_asset_hash
} else {
ASSET_HASH_IOS.to_string()
if asset_version == ASSET_VERSION_JP {
if args.jp_ios_asset_hash != String::new() {
&args.jp_ios_asset_hash
} else {
ASSET_HASH_IOS_JP
}
} else {
if args.en_ios_asset_hash != String::new() {
&args.en_ios_asset_hash
} else {
ASSET_HASH_IOS_GL
}
}
}.to_string()
}
pub fn create_token() -> String {
@@ -106,11 +120,11 @@ pub fn timestamp_since_midnight() -> u64 {
unix_timestamp.as_secs() - midnight
}
fn init_time(data: &JsonValue, server_data: &mut JsonValue, token: &str, max_time: u64) {
fn init_time(current_time: u64, server_data: &mut JsonValue, token: &str, max_time: u64, max: bool) {
let mut edited = false;
let default_time = 1709272800;
if max_time > 10 && max_time < data["server_time"].as_u64().unwrap_or(0) && server_data["server_time"].as_u64().unwrap_or(0) < max_time {
if max_time > 10 && max_time < current_time && server_data["server_time"].as_u64().unwrap_or(0) < max_time && max {
server_data["server_time_set"] = timestamp().into();
edited = true;
}
@@ -123,36 +137,37 @@ fn init_time(data: &JsonValue, server_data: &mut JsonValue, token: &str, max_tim
server_data["server_time"] = default_time.into();
edited = true;
}
if edited {
if edited && max {
userdata::save_server_data(token, server_data.clone());
}
}
fn set_time(data: &mut JsonValue, uid: i64) {
pub fn set_time(current_time: u64, uid: i64, max: bool) -> u64 {
let max_time = crate::get_args().max_time;
if uid == 0 {
if max_time > 10 && max_time < data["server_time"].as_u64().unwrap_or(0) {
data["server_time"] = max_time.into();
if max_time > 10 && max_time < current_time {
return max_time;
} else {
return timestamp();
}
return;
}
let token = userdata::get_login_token(uid);
let mut server_data = userdata::get_server_data(&token);
init_time(data, &mut server_data, &token, max_time);
init_time(current_time, &mut server_data, &token, max_time, max);
let time_set = server_data["server_time_set"].as_u64().unwrap_or(timestamp());
let server_time = server_data["server_time"].as_u64().unwrap_or(0);//1711741114
if server_time == 0 {
return;
return current_time;
}
let time_since_set = timestamp() - time_set;
data["server_time"] = (server_time + time_since_set).into();
let time_since_set = current_time - time_set;
return server_time + time_since_set;
}
pub fn send(mut data: JsonValue, uid: i64, headers: &HeaderMap) -> HttpResponse {
//println!("{}", json::stringify(data.clone()));
set_time(&mut data, uid);
data["server_time"] = set_time(data["server_time"].as_u64().unwrap_or(0), uid, true).into();
if !data["data"]["item_list"].is_empty() || !data["data"]["updated_value_list"]["item_list"].is_empty() {
items::check_for_region(&mut data, headers);

View File

@@ -8,27 +8,25 @@ use hmac::{Hmac, Mac};
use rusqlite::params;
use lazy_static::lazy_static;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::hash::MessageDigest;
use openssl::sign::Verifier;
use sha1::Digest;
use rsa::{RsaPublicKey, Pkcs1v15Sign};
use rsa::pkcs8::DecodePublicKey;
use crate::router::global;
use crate::router::userdata;
use crate::encryption;
use crate::router::user::{code_to_uid, uid_to_code};
use crate::sql::SQLite;
lazy_static! {
static ref DATABASE: SQLite = SQLite::new("gree.db", setup_tables);
}
fn setup_tables(conn: &SQLite) {
conn.create_store_v2("CREATE TABLE IF NOT EXISTS users (
fn setup_tables(conn: &rusqlite::Connection) {
conn.execute_batch("CREATE TABLE IF NOT EXISTS users (
cert TEXT NOT NULL,
uuid TEXT NOT NULL,
user_id BIGINT NOT NULL PRIMARY KEY
)");
);").unwrap();
}
fn update_cert(uid: i64, cert: &str) {
@@ -56,19 +54,14 @@ fn create_acc(cert: &str) -> String {
uuid
}
fn verify_signature(signature: &[u8], message: &[u8], public_key: &[u8]) -> bool {
let rsa_public_key = match Rsa::public_key_from_pem(public_key) {
Ok(key) => key,
Err(_) => return false,
};
let pkey = match PKey::from_rsa(rsa_public_key) {
Ok(pkey) => pkey,
Err(_) => return false,
};
let mut verifier = Verifier::new(MessageDigest::sha1(), &pkey).unwrap();
verifier.update(message).unwrap();
fn verify_signature(signature: &[u8], message: &[u8], public_key: &str) -> bool {
let pem = pem::parse(public_key).unwrap();
let public_key = RsaPublicKey::from_public_key_der(&pem.contents()).unwrap();
let digest = Sha1::digest(message);
verifier.verify(signature).is_ok()
public_key
.verify(Pkcs1v15Sign::new::<Sha1>(), &digest, signature)
.is_ok()
}
pub fn delete_uuid(user_id: i64) {
@@ -97,7 +90,7 @@ pub fn get_uuid(headers: &HeaderMap, body: &str) -> String {
let decoded = general_purpose::STANDARD.decode(login).unwrap_or_default();
if verify_signature(&decoded, encoded.as_bytes(), cert.as_bytes()) {
if verify_signature(&decoded, encoded.as_bytes(), &cert) {
DATABASE.lock_and_select("SELECT uuid FROM users WHERE user_id=?1;", params!(uid)).unwrap()
} else {
String::new()
@@ -237,11 +230,9 @@ pub fn migration_verify(req: HttpRequest, body: String) -> HttpResponse {
let body = json::parse(&body).unwrap();
let password = decrypt_transfer_password(&body["migration_password"].to_string());
let uid = code_to_uid(body["migration_code"].to_string()).parse::<i64>().unwrap_or(0);
let user = userdata::user::migration::get_acc_transfer(&body["migration_code"].to_string(), &password);
let user = userdata::get_acc_transfer(uid, &body["migration_code"].to_string(), &password);
let resp = if !user["success"].as_bool().unwrap() || uid == 0 {
let resp = if !user["success"].as_bool().unwrap() || user["user_id"] == 0 {
object!{
result: "ERR",
messsage: "User Not Found"
@@ -251,7 +242,7 @@ pub fn migration_verify(req: HttpRequest, body: String) -> HttpResponse {
object!{
result: "OK",
src_uuid: user["login_token"].clone(),
src_x_uid: uid.to_string(),
src_x_uid: user["user_id"].to_string(),
migration_token: user["login_token"].clone(),
balance_charge_gem: data_user["gem"]["charge"].to_string(),
balance_free_gem: data_user["gem"]["free"].to_string(),
@@ -322,7 +313,7 @@ pub fn migration_code(req: HttpRequest) -> HttpResponse {
let resp = object!{
result: "OK",
migration_code: uid_to_code(user["user"]["id"].to_string())
migration_code: userdata::user::migration::get_acc_token(user["user"]["id"].as_i64().unwrap())
};
send(req, resp)
@@ -342,10 +333,10 @@ pub fn migration_password_register(req: HttpRequest, body: String) -> HttpRespon
}
let user = userdata::get_acc(&uid);
let code = uid_to_code(user["user"]["id"].to_string());
let pass = decrypt_transfer_password(&body["migration_password"].to_string());
userdata::save_acc_transfer(&code, &pass);
userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &pass);
let resp = object!{
result: "OK"

View File

@@ -1,5 +1,5 @@
use json::{array, object, JsonValue};
use rand::Rng;
use rand::RngExt;
use actix_web::{HttpRequest, http::header::{HeaderMap, HeaderValue}};
use crate::encryption;
@@ -24,6 +24,18 @@ pub fn remove_gems(user: &mut JsonValue, amount: i64) {
user["gem"]["total"] = (free + paid).into();
}
pub fn remove_paid_gems(user: &mut JsonValue, amount: i64) {
let free = user["gem"]["free"].as_i64().unwrap();
let mut paid = user["gem"]["charge"].as_i64().unwrap();
paid -= amount;
if paid < 0 {
paid = 0;
}
user["gem"]["charge"] = paid.into();
user["gem"]["total"] = (free + paid).into();
}
pub fn get_region(headers: &HeaderMap) -> bool {
let blank_header = HeaderValue::from_static("");
let asset_version = headers.get("aoharu-asset-version").unwrap_or(&blank_header).to_str().unwrap_or("");
@@ -31,78 +43,92 @@ pub fn get_region(headers: &HeaderMap) -> bool {
}
pub fn check_for_region(user: &mut JsonValue, headers: &HeaderMap) {
let items = if user["data"]["updated_value_list"]["item_list"].is_empty() {user["data"]["item_list"].clone()} else {user["data"]["updated_value_list"]["item_list"].clone()};
let is_jp = get_region(headers);
if !is_jp || items.is_empty() {
// returns true if jp
if !get_region(headers) {
return;
}
let mut id = 0;
for (i, data) in items.members().enumerate() {
if data["master_item_id"] == 15570008 {
id = i + 1;
break;
}
}
if id > 0 {
if user["data"]["updated_value_list"]["item_list"].is_empty() {
user["data"]["item_list"].array_remove(id - 1);
let targets = [
15570001, 15570002, 15570003, 15570004,
15570005, 15570006, 15570007, 15570008
];
let list = if !user["data"]["updated_value_list"]["item_list"].is_empty() {
&mut user["data"]["updated_value_list"]["item_list"]
} else {
user["data"]["updated_value_list"]["item_list"].array_remove(id - 1);
}
&mut user["data"]["item_list"]
};
let mut to_remove: Vec<usize> = list
.members()
.enumerate()
.filter(|(_, item)| {
targets.contains(&item["master_item_id"].as_i64().unwrap_or(0))
})
.map(|(i, _)| i)
.collect();
to_remove.reverse();
for i in to_remove {
list.array_remove(i);
}
}
// true - limit reached
// false - all good
const GIFT_LIMIT: usize = 100000;
const LIMIT_ITEMS: i64 = 200000000;
const LIMIT_COINS: i64 = 2000000000;
const LIMIT_PRIMOGEMS: i64 = 1000000;
pub const GIFT_LIMIT: usize = 100000;
pub const LIMIT_ITEMS: i64 = 9999999;
pub const LIMIT_COINS: i64 = 999999999;
pub const LIMIT_PRIMOGEMS: i64 = 9999999;
pub fn give_shop(master_item_id: i64, count: i64, user: &mut JsonValue) -> bool {
let mut has = false;
for dataa in user["shop_list"].members_mut() {
if dataa["master_shop_item_id"].as_i64().unwrap() == master_item_id {
has = true;
let new_amount = dataa["count"].as_i64().unwrap() + count;
if new_amount > LIMIT_ITEMS {
fn give(array: &mut JsonValue, shop_id: &str, master_item_id: i64, limit: i64, count_id: &str, count: i64, default_push: JsonValue) -> bool {
for data in array.members_mut() {
if data[shop_id].as_i64().unwrap() == master_item_id {
if data[count_id].as_i64().unwrap() >= limit {
return true;
}
dataa["count"] = new_amount.into();
break;
let mut new_amount = data[count_id].as_i64().unwrap() + count;
if new_amount > limit {
new_amount = limit;
}
data[count_id] = new_amount.into();
return false;
}
}
if !has {
user["shop_list"].push(object!{
master_shop_item_id: master_item_id,
count: count
}).unwrap();
}
array.push(default_push).unwrap();
false
}
pub fn give_shop(master_item_id: i64, count: i64, user: &mut JsonValue) -> bool {
give(
&mut user["shop_list"],
"master_shop_item_id",
master_item_id,
LIMIT_ITEMS,
"count",
count,
object!{
master_shop_item_id: master_item_id,
count: count
}
)
}
pub fn give_item(master_item_id: i64, amount: i64, user: &mut JsonValue) -> bool {
let mut has = false;
for dataa in user["item_list"].members_mut() {
if dataa["master_item_id"].as_i64().unwrap() == master_item_id {
has = true;
let new_amount = dataa["amount"].as_i64().unwrap() + amount;
if new_amount > LIMIT_ITEMS {
return true;
}
dataa["amount"] = new_amount.into();
break;
}
}
if !has {
user["item_list"].push(object!{
give(
&mut user["item_list"],
"master_item_id",
master_item_id,
LIMIT_ITEMS,
"amount",
amount,
object!{
id: master_item_id,
master_item_id: master_item_id,
amount: amount,
expire_date_time: null
}).unwrap();
}
false
)
}
pub fn use_item(item: &JsonValue, multiplier: i64, user: &mut JsonValue) {
@@ -110,6 +136,8 @@ pub fn use_item(item: &JsonValue, multiplier: i64, user: &mut JsonValue) {
// Is anything really ever free...?
} else if item["consumeType"] == 1 {
remove_gems(user, item["amount"].as_i64().unwrap());
} else if item["consumeType"] == 2 {
remove_paid_gems(user, item["amount"].as_i64().unwrap());
} else if item["consumeType"] == 4 {
use_itemm(item["value"].as_i64().unwrap(), item["amount"].as_i64().unwrap() * multiplier, user);
} else {
@@ -161,25 +189,18 @@ pub fn give_points(master_item_id: i64, amount: i64, user: &mut JsonValue, missi
}
}
}
let mut has = false;
for data in user["point_list"].members_mut() {
if data["type"].as_i64().unwrap() == master_item_id {
has = true;
let new_amount = data["amount"].as_i64().unwrap() + amount;
if new_amount > LIMIT_COINS {
return true;
}
data["amount"] = new_amount.into();
break;
}
}
if !has {
user["point_list"].push(object!{
give(
&mut user["point_list"],
"type",
master_item_id,
LIMIT_COINS,
"amount",
amount,
object!{
type: master_item_id,
amount: amount
}).unwrap();
}
false
)
}
pub fn use_itemm(master_item_id: i64, amount: i64, user: &mut JsonValue) {
@@ -196,10 +217,13 @@ pub fn use_itemm(master_item_id: i64, amount: i64, user: &mut JsonValue) {
}
pub fn give_primogems(amount: i64, user: &mut JsonValue) -> bool {
let new_amount = user["gem"]["free"].as_i64().unwrap() + amount;
if new_amount > LIMIT_PRIMOGEMS {
if user["gem"]["free"].as_i64().unwrap() >= LIMIT_PRIMOGEMS {
return true;
}
let new_amount = user["gem"]["free"].as_i64().unwrap() + amount;
if user["gem"]["free"].as_i64().unwrap() > LIMIT_PRIMOGEMS {
user["gem"]["free"] = LIMIT_PRIMOGEMS.into();
}
user["gem"]["free"] = new_amount.into();
false
@@ -230,7 +254,7 @@ fn random_number(lowest: usize, highest: usize) -> usize {
}
assert!(lowest < highest);
rand::thread_rng().gen_range(lowest..highest + 1)
rand::rng().random_range(lowest..highest + 1)
}
pub fn gift_item_basic(id: i32, value: i64, ty_pe: i32, reason: &str, user: &mut JsonValue) -> JsonValue {

View File

@@ -1,6 +1,6 @@
use json::{object, array, JsonValue};
use actix_web::{HttpRequest};
use rand::Rng;
use rand::RngExt;
use lazy_static::lazy_static;
use crate::router::{global, userdata, items, databases};
@@ -34,7 +34,7 @@ fn random_number(lowest: usize, highest: usize) -> usize {
}
assert!(lowest < highest);
rand::thread_rng().gen_range(lowest..highest + 1)
rand::rng().random_range(lowest..highest + 1)
}
pub fn guest(req: HttpRequest, body: String) -> Option<JsonValue> {
@@ -182,6 +182,10 @@ fn check_for_stale_data(server_data: &mut JsonValue, live_id: i64) {
if live["expire_date_time"].as_u64().unwrap() < curr_time || live["master_live_id"] == live_id {
expired.push(i).unwrap();
}
if live["expire_date_time"].as_u64().unwrap() < curr_time {
// User closed game after losing. Count this as a fail.
live_completed(live["master_live_id"].as_i64().unwrap(), live["level"].as_i32().unwrap(), true, 0, 0);
}
}
for i in expired.members() {
server_data["last_live_started"].array_remove(i.as_usize().unwrap());
@@ -225,8 +229,8 @@ fn start_live(login_token: &str, body: &JsonValue) {
}
check_for_stale_data(&mut server_data, body["master_live_id"].as_i64().unwrap());
let mut to_save = body.clone();
// The user has 24 hours to complete a live
to_save["expire_date_time"] = (global::timestamp() + (24 * 60 * 60)).into();
// The user has 1 hour to complete a live
to_save["expire_date_time"] = (global::timestamp() + (1 * 60 * 60)).into();
server_data["last_live_started"].push(to_save).unwrap();
userdata::save_server_data(login_token, server_data);

View File

@@ -1,6 +1,6 @@
use json::{array, object, JsonValue};
use actix_web::{HttpRequest};
use rand::Rng;
use rand::RngExt;
use crate::router::{global, userdata, items, databases};
use crate::encryption;
@@ -46,7 +46,7 @@ fn get_random_card(item: &JsonValue, rv: &mut JsonValue, rng: &mut rand::rngs::T
let mut random_id = 0;
while random_id == 0 {
let card = rng.gen_range(1..databases::POOL[lottery_id.to_string()][databases::POOL[lottery_id.to_string()].len() - 1].as_i64().unwrap() + 1);
let card = rng.random_range(1..databases::POOL[lottery_id.to_string()][databases::POOL[lottery_id.to_string()].len() - 1].as_i64().unwrap() + 1);
if get_card_master_id(lottery_id.to_string(), card.to_string()).is_some() {
random_id = card;
break;
@@ -63,7 +63,7 @@ fn get_random_card(item: &JsonValue, rv: &mut JsonValue, rng: &mut rand::rngs::T
fn get_random_cards(id: i64, mut count: usize) -> JsonValue {
let total_ratio: i64 = databases::RARITY[id.to_string()].members().map(|item| if item["ensured"].as_i32().unwrap() == 1 { 0 } else { item["ratio"].as_i64().unwrap() }).sum();
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
let mut rv = array![];
let mut promised = false;
@@ -80,7 +80,7 @@ fn get_random_cards(id: i64, mut count: usize) -> JsonValue {
count -= 1;
}
for _i in 0..count {
let random_number: i64 = rng.gen_range(1..total_ratio + 1);
let random_number: i64 = rng.random_range(1..total_ratio + 1);
let mut cumulative_ratio = 0;
for item in databases::RARITY[id.to_string()].members() {
cumulative_ratio += item["ratio"].as_i64().unwrap();
@@ -118,12 +118,7 @@ pub fn lottery_post(req: HttpRequest, body: String) -> Option<JsonValue> {
consumeType: price["consumeType"].clone()
}, 1, &mut user);
let mut count = price["count"].as_usize().unwrap();
// This is a temporary easter egg, not meant to stay
if lottery_id == 4110044 {
count = 30;
}
let count = price["count"].as_usize().unwrap();
let cardstogive = get_random_cards(lottery_id, count);

View File

@@ -5,7 +5,7 @@ use crate::encryption;
use crate::router::{userdata, global};
fn get_asset_hash(req: &HttpRequest, body: &JsonValue) -> String {
if body["asset_version"] != global::ASSET_VERSION && body["asset_version"] != global::ASSET_VERSION_JP {
if body["asset_version"] != global::ASSET_VERSION_GL && body["asset_version"] != global::ASSET_VERSION_JP {
println!("Warning! Asset version is not what was expected. (Did the app update?)");
}

View File

@@ -169,41 +169,10 @@ pub fn announcement(req: HttpRequest) -> Option<JsonValue> {
})
}
pub fn uid_to_code(uid: String) -> String {
//just replace uid with numbers because im too lazy to have a real database and this is close enough anyways
uid
.replace('1', "A")
.replace('2', "G")
.replace('3', "W")
.replace('4', "Q")
.replace('5', "Y")
.replace('6', "6")
.replace('7', "I")
.replace('8', "P")
.replace('9', "U")
.replace('0', "M")
+ "7"
}
pub fn code_to_uid(code: String) -> String {
//just replace uid with numbers because im too lazy to have a real database and this is close enough anyways
code
.replace('7', "")
.replace('A', "1")
.replace('G', "2")
.replace('W', "3")
.replace('Q', "4")
.replace('Y', "5")
.replace('6', "6")
.replace('I', "7")
.replace('P', "8")
.replace('U', "9")
.replace('M', "0")
}
pub fn get_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let code = uid_to_code(body["user_id"].to_string());
let code = userdata::user::migration::get_acc_token(body["user_id"].as_i64()?);
Some(object!{
"migrationCode": code
@@ -215,9 +184,8 @@ pub fn register_password(req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let user = userdata::get_acc(&key);
let code = uid_to_code(user["user"]["id"].to_string());
userdata::save_acc_transfer(&code, &body["pass"].to_string());
userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &body["pass"].to_string());
Some(array![])
}
@@ -225,18 +193,16 @@ pub fn register_password(req: HttpRequest, body: String) -> Option<JsonValue> {
pub fn verify_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let uid = code_to_uid(body["migrationCode"].to_string()).parse::<i64>().unwrap_or(0);
let user = userdata::user::migration::get_acc_transfer(&body["migrationCode"].to_string(), &body["pass"].to_string());
let user = userdata::get_acc_transfer(uid, &body["migrationCode"].to_string(), &body["pass"].to_string());
if !user["success"].as_bool().unwrap() || uid == 0 {
if !user["success"].as_bool().unwrap() || user["user_id"] == 0 {
return None;
}
let data_user = userdata::get_acc(&user["login_token"].to_string());
Some(object!{
"user_id": uid,
"user_id": user["user_id"].clone(),
"uuid": user["login_token"].to_string(),
"charge": data_user["gem"]["charge"].clone(),
"free": data_user["gem"]["free"].clone()
@@ -245,11 +211,9 @@ pub fn verify_migration_code(_req: HttpRequest, body: String) -> Option<JsonValu
pub fn request_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let uid = code_to_uid(body["migrationCode"].to_string()).parse::<i64>().unwrap_or(0);
let user = userdata::user::migration::get_acc_transfer(&body["migrationCode"].to_string(), &body["pass"].to_string());
let user = userdata::get_acc_transfer(uid, &body["migrationCode"].to_string(), &body["pass"].to_string());
if !user["success"].as_bool().unwrap() || uid == 0 {
if !user["success"].as_bool().unwrap() || user["user_id"] == 0 {
return None;
}
@@ -331,10 +295,12 @@ async fn npps4_req(sha_id: String) -> Option<JsonValue> {
let url = format!("{}/ewexport?sha1={}", host, sha_id);
println!("Polling NPPS4 at {}", host);
let client = reqwest::Client::new();
let response = client.get(url);
let response_body = response.send().await.ok()?.text().await.ok()?;
json::parse(&response_body).ok()
let body = ureq::get(&url)
.call().ok()?
.body_mut()
.read_to_string().ok()?;
json::parse(&body).ok()
}
fn clean_sif_data(current: &JsonValue) -> JsonValue {
@@ -438,7 +404,7 @@ pub fn initialize(req: HttpRequest, body: String) -> Option<JsonValue> {
for (i, data) in cardstoreward.members().enumerate() {
items::give_character(data.as_i64().unwrap(), &mut user, &mut missions, &mut array![], &mut array![]);
if i < 10 {
if i < 9 {
user["deck_list"][0]["main_card_ids"][i] = data.clone();
}
}

View File

@@ -1,9 +1,9 @@
pub mod user;
use rusqlite::params;
use lazy_static::lazy_static;
use json::{JsonValue, array, object};
use rand::Rng;
use sha2::{Digest, Sha256};
use base64::{Engine as _, engine::general_purpose};
use rand::RngExt;
use crate::router::global;
use crate::router::items;
@@ -17,62 +17,64 @@ lazy_static! {
};
}
fn setup_tables(conn: &SQLite) {
conn.create_store_v2("CREATE TABLE IF NOT EXISTS tokens (
fn get_userdata_database() -> &'static SQLite {
&DATABASE
}
fn setup_tables(conn: &rusqlite::Connection) {
user::migration::setup_sql(conn).unwrap();
conn.execute_batch("
CREATE TABLE IF NOT EXISTS tokens (
user_id BIGINT NOT NULL PRIMARY KEY,
token TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS migration (
token TEXT NOT NULL PRIMARY KEY,
password TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS userdata (
);
CREATE TABLE IF NOT EXISTS userdata (
user_id BIGINT NOT NULL PRIMARY KEY,
userdata TEXT NOT NULL,
friend_request_disabled INT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS userhome (
);
CREATE TABLE IF NOT EXISTS userhome (
user_id BIGINT NOT NULL PRIMARY KEY,
userhome TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS missions (
);
CREATE TABLE IF NOT EXISTS missions (
user_id BIGINT NOT NULL PRIMARY KEY,
missions TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS loginbonus (
);
CREATE TABLE IF NOT EXISTS loginbonus (
user_id BIGINT NOT NULL PRIMARY KEY,
loginbonus TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS sifcards (
);
CREATE TABLE IF NOT EXISTS sifcards (
user_id BIGINT NOT NULL PRIMARY KEY,
sifcards TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS friends (
);
CREATE TABLE IF NOT EXISTS friends (
user_id BIGINT NOT NULL PRIMARY KEY,
friends TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS chats (
);
CREATE TABLE IF NOT EXISTS chats (
user_id BIGINT NOT NULL PRIMARY KEY,
chats TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS event (
);
CREATE TABLE IF NOT EXISTS event (
user_id BIGINT NOT NULL PRIMARY KEY,
event TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS eventloginbonus (
);
CREATE TABLE IF NOT EXISTS eventloginbonus (
user_id BIGINT NOT NULL PRIMARY KEY,
eventloginbonus TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS server_data (
);
CREATE TABLE IF NOT EXISTS server_data (
user_id BIGINT NOT NULL PRIMARY KEY,
server_data TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS webui (
);
CREATE TABLE IF NOT EXISTS webui (
user_id BIGINT NOT NULL PRIMARY KEY,
token TEXT NOT NULL,
last_login BIGINT NOT NULL
)");
);
").unwrap();
}
fn acc_exists(uid: i64) -> bool {
@@ -94,8 +96,8 @@ fn get_key(auth_key: &str) -> i64 {
}
fn generate_uid() -> i64 {
let mut rng = rand::thread_rng();
let random_number = rng.gen_range(100_000_000_000_000..=999_999_999_999_999);
let mut rng = rand::rng();
let random_number = rng.random_range(100_000_000_000_000..=999_999_999_999_999);
//the chances of this...?
if acc_exists(random_number) {
return generate_uid();
@@ -300,67 +302,6 @@ pub fn save_acc_sif(auth_key: &str, data: JsonValue) {
save_data(auth_key, "sifcards", data);
}
fn generate_salt() -> Vec<u8> {
let mut rng = rand::thread_rng();
let mut bytes = vec![0u8; 16];
rng.fill(&mut bytes[..]);
bytes
}
fn hash_password(password: &str) -> String {
let salt = &generate_salt();
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let hashed_password = hasher.finalize();
let salt_hash = [&salt[..], &hashed_password[..]].concat();
general_purpose::STANDARD.encode(salt_hash)
}
fn verify_password(password: &str, salted_hash: &str) -> bool {
if password.is_empty() || salted_hash.is_empty() {
return false;
}
let bytes = general_purpose::STANDARD.decode(salted_hash);
if bytes.is_err() {
return password == salted_hash;
}
let bytes = bytes.unwrap();
if bytes.len() < 17 {
return password == salted_hash;
}
let (salt, hashed_password) = bytes.split_at(16);
let hashed_password = &hashed_password[0..32];
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let input_hash = hasher.finalize();
input_hash.as_slice() == hashed_password
}
pub fn get_acc_transfer(uid: i64, token: &str, password: &str) -> JsonValue {
let data = DATABASE.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(token));
if data.is_err() {
return object!{success: false};
}
if verify_password(password, &data.unwrap()) {
let login_token = get_login_token(uid);
if login_token == String::new() {
return object!{success: false};
}
return object!{success: true, login_token: login_token};
}
object!{success: false}
}
pub fn save_acc_transfer(token: &str, password: &str) {
DATABASE.lock_and_exec("DELETE FROM migration WHERE token=?1", params!(token));
DATABASE.lock_and_exec("INSERT INTO migration (token, password) VALUES (?1, ?2)", params!(token, hash_password(password)));
}
pub fn get_name_and_rank(uid: i64) -> JsonValue {
let login_token = get_login_token(uid);
if login_token == String::new() {
@@ -478,8 +419,8 @@ fn create_webui_token() -> String {
}
pub fn webui_login(uid: i64, password: &str) -> Result<String, String> {
let pass = DATABASE.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(crate::router::user::uid_to_code(uid.to_string()))).unwrap_or_default();
if !verify_password(password, &pass) {
let pass = DATABASE.lock_and_select("SELECT password FROM migration WHERE user_id=?1", params!(uid)).unwrap_or_default();
if !user::migration::verify_password(password, &pass) {
if acc_exists(uid) && pass.is_empty() {
return Err(String::from("Migration token not set. Set token in game settings."));
}
@@ -489,7 +430,8 @@ pub fn webui_login(uid: i64, password: &str) -> Result<String, String> {
let new_token = create_webui_token();
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(uid));
DATABASE.lock_and_exec("INSERT INTO webui (user_id, token, last_login) VALUES (?1, ?2, ?3)", params!(uid, new_token, global::timestamp()));
// This could overflow given enough time... though rusqlite doesnt currently support ToSql on a u64...... maybe someday
DATABASE.lock_and_exec("INSERT INTO webui (user_id, token, last_login) VALUES (?1, ?2, ?3)", params!(uid, new_token, global::timestamp() as i64));
Ok(new_token)
}
@@ -504,17 +446,16 @@ pub fn webui_import_user(user: JsonValue) -> Result<JsonValue, String> {
let token = global::create_token();
DATABASE.lock_and_exec("INSERT INTO tokens (user_id, token) VALUES (?1, ?2)", params!(uid, token));
let mig = crate::router::user::uid_to_code(uid.to_string());
save_acc_transfer(&mig, &user["password"].to_string());
let token = user::migration::save_acc_transfer(uid, &user["password"].to_string());
Ok(object!{
uid: uid,
migration_token: mig
migration_token: token
})
}
fn webui_login_token(token: &str) -> Option<String> {
pub fn webui_login_token(token: &str) -> Option<String> {
let uid = DATABASE.lock_and_select("SELECT user_id FROM webui WHERE token=?1", params!(token)).unwrap_or_default();
if uid == String::new() || token.is_empty() {
return None;
@@ -639,7 +580,7 @@ pub fn purge_accounts() -> usize {
DATABASE.lock_and_exec("DELETE FROM server_data WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM tokens WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM migration WHERE token=?1", params!(crate::router::user::uid_to_code(user_id.to_string())));
DATABASE.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(user_id));
}
DATABASE.lock_and_exec("VACUUM", params!());
crate::router::gree::vacuum_database();

View File

@@ -0,0 +1 @@
pub mod migration;

View File

@@ -0,0 +1,157 @@
use rusqlite::params;
use json::{JsonValue, object};
use crate::router::userdata;
use rand::RngExt;
use sha2::{Digest, Sha256};
use base64::{Engine as _, engine::general_purpose};
fn generate_token() -> String {
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::rng();
let random_string: String = (0..16)
.map(|_| {
let idx = rng.random_range(0..charset.len());
charset.chars().nth(idx).unwrap()
})
.collect();
random_string
}
pub fn get_acc_transfer(token: &str, password: &str) -> JsonValue {
let database = userdata::get_userdata_database();
let data = database.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(token));
if data.is_err() {
return object!{success: false};
}
if verify_password(password, &data.unwrap()) {
let uid: i64 = database.lock_and_select_type("SELECT user_id FROM migration WHERE token=?1", params!(token)).unwrap();
let login_token = userdata::get_login_token(uid);
if login_token == String::new() {
return object!{success: false};
}
return object!{success: true, login_token: login_token, user_id: uid};
}
object!{success: false}
}
pub fn save_acc_transfer(uid: i64, password: &str) -> String {
let database = userdata::get_userdata_database();
let token = if let Ok(value) = database.lock_and_select("SELECT token FROM migration WHERE user_id=?1", params!(uid)) {
value
} else {
generate_token()
};
database.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(uid));
database.lock_and_exec("INSERT INTO migration (user_id, token, password) VALUES (?1, ?2, ?3)", params!(uid, &token, hash_password(password)));
token
}
pub fn get_acc_token(uid: i64) -> String {
let database = userdata::get_userdata_database();
if let Ok(value) = database.lock_and_select("SELECT token FROM migration WHERE user_id=?1", params!(uid)) {
value
} else {
save_acc_transfer(uid, "")
}
}
fn generate_salt() -> Vec<u8> {
let mut rng = rand::rng();
let mut bytes = vec![0u8; 16];
rng.fill(&mut bytes[..]);
bytes
}
fn hash_password(password: &str) -> String {
if password.is_empty() { return String::new(); };
let salt = &generate_salt();
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let hashed_password = hasher.finalize();
let salt_hash = [&salt[..], &hashed_password[..]].concat();
general_purpose::STANDARD.encode(salt_hash)
}
pub fn verify_password(password: &str, salted_hash: &str) -> bool {
if password.is_empty() || salted_hash.is_empty() {
return false;
}
let bytes = general_purpose::STANDARD.decode(salted_hash);
if bytes.is_err() {
return password == salted_hash;
}
let bytes = bytes.unwrap();
if bytes.len() < 17 {
return password == salted_hash;
}
let (salt, hashed_password) = bytes.split_at(16);
let hashed_password = &hashed_password[0..32];
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let input_hash = hasher.finalize();
input_hash.as_slice() == hashed_password
}
pub fn setup_sql(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
conn.execute("
CREATE TABLE IF NOT EXISTS migration (
user_id BIGINT NOT NULL,
token TEXT NOT NULL,
password TEXT NOT NULL,
PRIMARY KEY (user_id, token)
);
", [])?;
let is_updated = conn.prepare("SELECT user_id FROM migration LIMIT 1;").is_ok();
if is_updated { return Ok(()); }
println!("Upgrading migration table");
conn.execute("DROP TABLE IF EXISTS migration_new;", [])?;
conn.execute("
CREATE TABLE migration_new (
user_id BIGINT NOT NULL,
token TEXT NOT NULL,
password TEXT NOT NULL,
PRIMARY KEY (user_id, token)
);
", [])?;
let mut stmt = conn.prepare("SELECT token, password FROM migration")?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
))
})?;
let mut insert_new_row = conn.prepare("INSERT INTO migration_new (user_id, token, password) VALUES (?1, ?2, ?3)")?;
for row in rows {
let (token, password) = row?;
let user_id = code_to_uid(&token);
insert_new_row.execute(params![user_id, token, password])?;
}
conn.execute("DROP TABLE migration;", params!())?;
conn.execute("ALTER TABLE migration_new RENAME TO migration;", params!())?;
Ok(())
}
fn code_to_uid(code: &str) -> String {
code
.replace('7', "")
.replace('A', "1")
.replace('G', "2")
.replace('W', "3")
.replace('Q', "4")
.replace('Y', "5")
.replace('6', "6")
.replace('I', "7")
.replace('P', "8")
.replace('U', "9")
.replace('M', "0")
}

View File

@@ -5,6 +5,9 @@ use actix_web::{
http::header::ContentType
};
use json::{JsonValue, object};
use lazy_static::lazy_static;
use include_dir::{include_dir, Dir};
use std::fs;
use crate::include_file;
use crate::router::{userdata, items};
@@ -32,10 +35,8 @@ fn error(msg: &str) -> HttpResponse {
message: msg
};
HttpResponse::Ok()
.insert_header(("Access-Control-Allow-Origin", "*"))
.insert_header(ContentType::json())
.body(json::stringify(resp))
}
pub fn login(_req: HttpRequest, body: String) -> HttpResponse {
@@ -137,30 +138,61 @@ pub fn logout(req: HttpRequest) -> HttpResponse {
HttpResponse::Found()
.insert_header(ContentType::json())
.insert_header(("Set-Cookie", "ew_token=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT"))
.insert_header(("Location", "/"))
.insert_header(("Location", "/login.html"))
.body(json::stringify(resp))
}
static WEBUI_ASSETS: Dir<'_> = include_dir!("webui/");
pub fn main(req: HttpRequest) -> HttpResponse {
if req.path() == "/" {
let path = if req.path().ends_with("/") { format!("{}index.html", req.path()) } else { req.path().to_string() };
let mut chars = path.chars();
chars.next();
let path = chars.as_str();
if path == "login.html" {
let token = get_login_token(&req);
if token.is_some() {
let data = userdata::webui_get_user(&token.unwrap());
if data.is_some() {
return HttpResponse::Found()
.insert_header(("Location", "/home/"))
.insert_header(("Location", "/account.html"))
.body("");
}
}
}
if req.path() != "/" && req.path() != "/home/" && req.path() != "/import/" && req.path() != "/help/" {
return HttpResponse::Found()
if let Some(file) = WEBUI_ASSETS.get_file(&path) {
let body = file.contents();
let mime = mime_guess::from_path(path).first_or_octet_stream();
return HttpResponse::Ok()
.insert_header(ContentType(mime))
.insert_header(("content-length", body.len()))
.body(body);
} else if path.starts_with("webui/images/card-thumbnails") {
let args = crate::get_args();
let file_name = path.split("/").last().unwrap_or("");
let file_path = format!("{}/{}", args.image_asset_path, file_name).replace("//", "/");
if args.image_asset_path != "" && let Ok(body) = fs::read(file_path) {
let mime = mime_guess::from_path(path).first_or_octet_stream();
return HttpResponse::Ok()
.insert_header(ContentType(mime))
.insert_header(("content-length", body.len()))
.body(body);
} else {
if args.image_asset_path != "" {
println!("File '{file_name}' was requested, but no file was found on the disk!");
}
return HttpResponse::SeeOther()
.insert_header(("location", format!("https://sif2-api.ethanthesleepy.one{}", req.path())))
.body("");
}
}
HttpResponse::Found()
.insert_header(("Location", "/"))
.body("");
}
HttpResponse::Ok()
.insert_header(ContentType::html())
.body(include_file!("webui/dist/index.html"))
.body("")
}
pub fn export(req: HttpRequest) -> HttpResponse {
@@ -201,5 +233,165 @@ pub fn server_info(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.insert_header(ContentType::json())
.body(json::stringify(resp))
}
fn get_query_str(req: &HttpRequest, key: &str, def: &str) -> String {
let query_str = req.query_string();
query_str
.split('&')
.find(|s| s.starts_with(&format!("{key}=")))
.and_then(|s| s.split('=').nth(1))
.unwrap_or(def).to_string()
}
pub fn get_card_info(req: HttpRequest) -> HttpResponse {
let page = get_query_str(&req, "page", "1").parse::<usize>().unwrap_or(1) - 1;
let max = get_query_str(&req, "max", "10").parse::<usize>().unwrap_or(10);
let all = get_query_str(&req, "all", "false");
let name_query = get_query_str(&req, "query", "");
let start = page * max;
let items = json::parse(&include_file!("src/router/webui/cards.json")).unwrap();
if all == "true" {
let resp = object!{
total_pages: 1,
current: items
};
return HttpResponse::Ok()
.content_type(ContentType::json())
.body(json::stringify(resp));
}
let mut filtered_items: Vec<_> = items.members().collect();
if !name_query.is_empty() {
let lowercase_query = name_query.to_lowercase();
filtered_items.retain(|item| {
item["name"].to_string().to_lowercase().contains(&lowercase_query)
});
}
let total_len = filtered_items.len();
let page_items: Vec<_> = filtered_items
.into_iter()
.skip(start)
.take(max)
.cloned()
.collect();
if page_items.is_empty() {
return HttpResponse::NotFound()
.finish();
}
let total_pages = (total_len as f64 / max as f64).ceil() as usize;
let args = crate::get_args();
let resp = object!{
total_pages: total_pages,
current: page_items,
image_path: args.image_asset_path
};
HttpResponse::Ok()
.content_type(ContentType::json())
.body(json::stringify(resp))
}
pub fn get_music_info(req: HttpRequest) -> HttpResponse {
let page = get_query_str(&req, "page", "1").parse::<usize>().unwrap_or(1) - 1;
let max = get_query_str(&req, "max", "10").parse::<usize>().unwrap_or(10);
let lang = get_query_str(&req, "lang", "JP");
let start = page * max;
let items = if lang == "EN" {
json::parse(&include_file!("src/router/databases/json/global/music.json")).unwrap()
} else {
json::parse(&include_file!("src/router/databases/json/music.json")).unwrap()
};
let page_items: Vec<_> = items.members()
.skip(start)
.take(max)
.cloned()
.collect();
if page_items.is_empty() {
return HttpResponse::NotFound()
.finish();
}
let total_items = items.len();
let total_pages = (total_items as f64 / max as f64).ceil() as usize;
let resp = object!{
total_pages: total_pages,
current: page_items
};
HttpResponse::Ok()
.content_type(ContentType::json())
.body(json::stringify(resp))
}
lazy_static! {
static ref ITEM: JsonValue = json::parse(&include_file!("src/router/webui/item.json")).unwrap();
static ref LOGIN_BONUS: JsonValue = json::parse(&include_file!("src/router/webui/login_bonus.json")).unwrap();
}
pub fn list_login_bonus(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.content_type(ContentType::json())
.body(json::stringify(LOGIN_BONUS.clone()))
}
pub fn list_items(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.content_type(ContentType::json())
.body(json::stringify(ITEM.clone()))
}
pub fn cheat(req: HttpRequest, _body: String) -> HttpResponse {
let token = get_login_token(&req);
if token.is_none() {
return error("Not logged in");
}
let key = userdata::webui_login_token(&token.unwrap());
if key.is_none() {
return error("Not logged in");
}
let key = key.unwrap();
let mut user = userdata::get_acc_home(&key);
for item in ITEM.entries() {
let id = item.0.parse::<i32>().unwrap_or(0);
let data = item.1;
if id == 0 {
continue;
}
let reward_type = data["reward_type"].as_i32().unwrap();
let limit = if reward_type == 4 {
items::LIMIT_COINS
} else if reward_type == 1 {
items::LIMIT_PRIMOGEMS
} else {
items::LIMIT_ITEMS
};
items::gift_item_basic(id, limit, reward_type, "You have cheated. Here are \"gifts\".", &mut user);
}
userdata::save_acc_home(&key, user);
let resp = object!{
result: "OK"
};
HttpResponse::Ok()
.insert_header(ContentType::json())
.body(json::stringify(resp))
}

4626
src/router/webui/cards.json Normal file

File diff suppressed because it is too large Load Diff

1054
src/router/webui/item.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

44
src/runtime.rs Normal file
View File

@@ -0,0 +1,44 @@
use lazy_static::lazy_static;
use std::sync::RwLock;
use std::fs;
lazy_static! {
static ref RUNNING: RwLock<bool> = RwLock::new(false);
static ref DATAPATH: RwLock<String> = RwLock::new(String::new());
static ref EASTER: RwLock<bool> = RwLock::new(false);
}
pub fn set_running(running: bool) {
let mut w = RUNNING.write().unwrap();
*w = running;
}
pub fn get_running() -> bool {
*RUNNING.read().unwrap()
}
pub fn get_data_path(file_name: &str) -> String {
let mut path = {
DATAPATH.read().unwrap().clone()
};
while path.ends_with('/') {
path.pop();
}
fs::create_dir_all(&path).unwrap();
format!("{}/{}", path, file_name)
}
pub fn update_data_path(path: &str) {
let mut w = DATAPATH.write().unwrap();
*w = path.to_string();
}
// Only currently editable by the android so
pub fn set_easter_mode(enabled: bool) {
let mut w = EASTER.write().unwrap();
*w = enabled;
}
pub fn get_easter_mode() -> bool {
*EASTER.read().unwrap()
}

View File

@@ -1,45 +1,30 @@
use rusqlite::{Connection, params, ToSql};
use std::sync::Mutex;
use rusqlite::{Connection, ToSql};
use json::{JsonValue, array};
use crate::router::clear_rate::Live;
macro_rules! lock_onto_mutex {
($mutex:expr) => {{
loop {
match $mutex.lock() {
Ok(value) => {
break value;
}
Err(_) => {
$mutex.clear_poison();
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
}
}};
}
pub struct SQLite {
engine: Mutex<Connection>
path: String
}
impl SQLite {
pub fn new(path: &str, setup: fn(&SQLite)) -> SQLite {
let conn = Connection::open(crate::get_data_path(path)).unwrap();
conn.execute("PRAGMA foreign_keys = ON;", ()).unwrap();
pub fn new(path: &str, setup: fn(&Connection)) -> SQLite {
let instance = SQLite {
engine: Mutex::new(conn)
path: crate::get_data_path(path)
};
setup(&instance);
let conn = Connection::open(&instance.path).unwrap();
conn.busy_timeout(std::time::Duration::from_secs(10)).unwrap();
conn.execute("PRAGMA foreign_keys = ON;", ()).unwrap();
setup(&conn);
instance
}
pub fn get_path(&self) -> &str {
&self.path
}
pub fn lock_and_exec(&self, command: &str, args: &[&dyn ToSql]) {
let conn = lock_onto_mutex!(self.engine);
let conn = Connection::open(&self.path).unwrap();
conn.execute(command, args).unwrap();
}
pub fn lock_and_select(&self, command: &str, args: &[&dyn ToSql]) -> Result<String, rusqlite::Error> {
let conn = lock_onto_mutex!(self.engine);
let conn = Connection::open(&self.path).unwrap();
let mut stmt = conn.prepare(command)?;
stmt.query_row(args, |row| {
match row.get::<usize, i64>(0) {
@@ -48,8 +33,15 @@ impl SQLite {
}
})
}
pub fn lock_and_select_type<T: rusqlite::types::FromSql>(&self, command: &str, args: &[&dyn ToSql]) -> Result<T, rusqlite::Error> {
let conn = Connection::open(&self.path).unwrap();
let mut stmt = conn.prepare(command)?;
stmt.query_row(args, |row| {
row.get(0)
})
}
pub fn lock_and_select_all(&self, command: &str, args: &[&dyn ToSql]) -> Result<JsonValue, rusqlite::Error> {
let conn = lock_onto_mutex!(self.engine);
let conn = Connection::open(&self.path).unwrap();
let mut stmt = conn.prepare(command)?;
let map = stmt.query_map(args, |row| {
match row.get::<usize, i64>(0) {
@@ -67,24 +59,4 @@ impl SQLite {
}
Ok(rv)
}
pub fn get_live_data(&self, id: i64) -> Result<Live, rusqlite::Error> {
let conn = lock_onto_mutex!(self.engine);
let mut stmt = conn.prepare("SELECT * FROM lives WHERE live_id=?1")?;
stmt.query_row(params!(id), |row| {
Ok(Live {
live_id: row.get(0)?,
normal_failed: row.get(1)?,
normal_pass: row.get(2)?,
hard_failed: row.get(3)?,
hard_pass: row.get(4)?,
expert_failed: row.get(5)?,
expert_pass: row.get(6)?,
master_failed: row.get(7)?,
master_pass: row.get(8)?,
})
})
}
pub fn create_store_v2(&self, table: &str) {
self.lock_and_exec(table, params!());
}
}

60
src/static_handlers.rs Normal file
View File

@@ -0,0 +1,60 @@
use actix_web::{
get,
HttpResponse,
HttpRequest,
http::header::ContentType
};
use std::fs;
#[get("/maintenance/maintenance.json")]
async fn maintenance(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.insert_header(ContentType(mime::APPLICATION_JSON))
.body(r#"{"opened_at":"2024-02-05 02:00:00","closed_at":"2024-02-05 04:00:00","message":":(","server":1,"gamelib":0}"#)
}
#[cfg(feature = "library")]
use include_dir::{include_dir, Dir};
#[cfg(all(feature = "library", target_os = "ios"))]
static SPART_FILES: Dir<'_> = include_dir!("assets/iOS/");
#[cfg(all(feature = "library", target_os = "android"))]
static SPART_FILES: Dir<'_> = include_dir!("assets/Android/");
fn handle_assets(req: HttpRequest) -> HttpResponse {
#[cfg(feature = "library")]
{
let lang: String = req.match_info().get("lang").unwrap_or("JP").parse().unwrap_or(String::from("JP"));
let file_name: String = req.match_info().get("file").unwrap().parse().unwrap();
let hash: String = req.match_info().get("file").unwrap().parse().unwrap();
if let Some(file) = SPART_FILES.get_file(format!("{lang}/{hash}/{file_name}")) {
let body = file.contents();
return HttpResponse::Ok()
.insert_header(ContentType(mime::APPLICATION_OCTET_STREAM))
.insert_header(("content-length", body.len()))
.body(body);
}
}
let file_path = format!("assets{}", req.path());
let exists = fs::exists(&file_path);
if exists.unwrap_or(false) {
let resp = fs::read(&file_path).unwrap();
return HttpResponse::Ok()
.body(resp)
}
HttpResponse::SeeOther()
.insert_header(("location", format!("https://sif2.sif.moe{}", req.path())))
.body("")
}
#[get("/Android/{hash}/{file}")]
async fn files_jp(req: HttpRequest) -> HttpResponse {
handle_assets(req)
}
#[get("/Android/{lang}/{hash}/{file}")]
async fn files_gl(req: HttpRequest) -> HttpResponse {
handle_assets(req)
}

1
webui Submodule

Submodule webui added at 7fba8d617e

View File

@@ -1,21 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
webui/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EW private server</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

4554
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
{
"name": "webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-bootstrap": "^2.10.2",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"vite": "^5.2.0"
}
}

View File

@@ -1,18 +0,0 @@
const serverUrl = "";
async function api(url, body) {
try {
let options = body ? {method: "POST", body: JSON.stringify(body)} : {}
const resp = await fetch(serverUrl + url, options);
const text = await resp.text();
return JSON.parse(text);
} catch(e) {
return {
result: "ERR",
message: e.message
}
}
}
export default api;

View File

@@ -1,36 +0,0 @@
body {
background-color: #616161;
}
#home {
width: 90%;
margin: 50px auto;
background-color: #43A047;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
padding: 20px 10px;
}
#logout {
border: none;
text-align: center;
text-decoration: underline;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
float: right;
background-color: yellow;
border-radius: 30px;
padding: 5px 20px;
cursor: pointer;
}
#logout:hover {
background-color: red;
}
#error p {
color: orange;
grid-template-columns: auto auto auto;
}

View File

@@ -1,89 +0,0 @@
import { useState, useParams, useEffect } from 'react'
import './Help.css'
import Request from '../Request.jsx'
let init = false;
function Help() {
const [downloadUrl, setDownloadUrl] = useState(<div>Your server admin has no pre-patched apks to download. See the question below..</div>);
const [downloadUrliOSGL, setDownloadUrliOSGL] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-gl.ipa");
const [downloadUrliOSJP, setDownloadUrliOSJP] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-jp.ipa");
const [assetUrl, setAssetUrl] = useState("https://sif2.sif.moe");
useEffect(() => {
(async () => {
if (init) return;
init = true;
let resp = await Request("/api/webui/serverInfo");
if (resp.result !== "OK") {
return;
}
if (!resp.data.links) return;
if (resp.data.links.global && resp.data.links.japan) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a> or <a href={resp.data.links.global}>Global</a></div>
);
} else if (resp.data.links.global) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.global}>Global</a></div>
);
} else if (resp.data.links.japan) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a></div>
);
}
if (resp.data.links.assets) {
setAssetUrl(resp.data.links.assets);
}
if (!resp.data.links.ios) return;
if (resp.data.links.ios.japan) {
setDownloadUrliOSJP(resp.data.links.ios.japan);
}
if (resp.data.links.ios.global) {
setDownloadUrliOSGL(resp.data.links.ios.global);
}
})();
});
return (
<div id="home">
<h1>Help/About</h1>
<h2>What is "ew"? What is this server for?</h2>
<p>"ew" is a private server, written in Rust, for the short lived game "Love Live! School idol festival 2 MIRACLE LIVE!", a Love Live! themed mobile rhythm game.</p>
<h2>I'm just trying to play on this server, how do I install the app? (Android)</h2>
<p>{downloadUrl}</p>
<h2>My server admin has no download URLs, how do I patch the apk? (Android)</h2>
<p>You can use <a href="https://arasfon.ru/sif2/patcher/">@arasfon's sif2 apk patcher</a>. Plug and the asset url, which is "{assetUrl}", into the "Assets URL" textbox, and then the server url (Which is likely "{window.location.origin}", though this may not be the case). Select your game version, set "header format" to "Lowercase", and press patch. Once that done, use <a href="https://github.com/patrickfav/uber-apk-signer">uber-apk-signer</a> to sign the apk. Then, get it onto your phone and install!</p>
<h2>How do I install the app? (iOS)</h2>
<p>Running on iOS is much simpler than Android, thanks to triangle on the discord. You first download an ipa file for <a href={downloadUrliOSGL}>global</a> or <a href={downloadUrliOSJP}>Japan</a>, and use <a href="https://sideloadly.io/">Sideloadly</a> (or your preferred application installer) to install the app. Then open settings, navigate to the app you just installed, and input the server url (Which is likely "{window.location.origin}", though this may not be the case), and the asset url, which is "{assetUrl}". If you have any errors opening the app, make sure none of the urls in settings end with a slash (/).</p>
<h2>Help! I'm trying to open the app and it shows as "unavailable" (iOS)</h2>
<p>Do not delete it, Just re-sideload the app. This is an Apple security feature.</p>
<h2>How well does this work?</h2>
<p>Works well enough. The server itself takes up not even 20mb of storage, and it's written in rust. I personally think it's pretty well written.</p>
<h2>Could my computer/laptop run a server?</h2>
<p>Very very likely. If the platform is <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">supported by rust</a>, then the answer is yes! It is recommended to manually compile the project until I get the time to setup actions. <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">ew repo</a></p>
<h2>Is the server down right now? I can't connect</h2>
<p>Assuming you have just loaded this page on the server you use, then the answer is no, otherwise please contact your server admin.</p>
<h2>Do events work?</h2>
<p>Most events do not, though most should not crash the game. Star events are partially implemented. You can get your rank up, and compete with other players in a ranking table, but no rewards are currently implemented.</p>
<h2>But then, how do I get event URs?</h2>
<p>There are serial codes for several things, one of which includes all the event URs. I don't remember what does what but it is recommended to look at the serial code file to get the latest codes.</p>
<h2>Why does the game crash when I do x?</h2>
<p>This likely means something on the server is broken. If you're self hosting, please contact me via matrix. Otherwise, contact your server admin and ask them to report the issue.</p>
</div>
);
}
export default Help;

View File

@@ -1,36 +0,0 @@
body {
background-color: #616161;
}
#home {
width: 90%;
margin: 50px auto;
background-color: #43A047;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
padding: 20px 10px;
}
#logout {
border: none;
text-align: center;
text-decoration: underline;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
float: right;
background-color: yellow;
border-radius: 30px;
padding: 5px 20px;
cursor: pointer;
}
#logout:hover {
background-color: red;
}
#error p {
color: orange;
grid-template-columns: auto auto auto;
}

View File

@@ -1,196 +0,0 @@
import { useState, useParams, useEffect } from 'react'
import './Home.css'
import Request from '../Request.jsx'
let bonusItems = [];
function getMigrationID(uid) {
return uid.toString()
.replaceAll('1', "A")
.replaceAll('2', "G")
.replaceAll('3', "W")
.replaceAll('4', "Q")
.replaceAll('5', "Y")
.replaceAll('6', "6")
.replaceAll('7', "I")
.replaceAll('8', "P")
.replaceAll('9', "U")
.replaceAll('0', "M")
+ "7";
}
function Bonus() {
const [inputValue, setInputValue] = useState('');
const error = useState("");
let itemz = [];
bonusItems.forEach((e) => {
itemz.push(e.master_login_bonus_id);
})
const [submittedItems, setSubmittedItems] = useState(itemz);
const handleSubmit = async (event) => {
event.preventDefault();
let input = parseInt(inputValue.trim());
if (isNaN(input) || submittedItems.includes(input)) return;
let resp = await Request("/api/webui/startLoginbonus", {
bonus_id: input
});
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
error[1]("");
setSubmittedItems([...submittedItems, resp.id]);
setInputValue('');
};
const handleRemoveItem = (index) => {
const updatedItems = [...submittedItems];
updatedItems.splice(index, 1);
setSubmittedItems(updatedItems);
};
// <button onClick={() => handleRemoveItem(index)}>X</button>
return (
<div>
<h2>Current login bonus list</h2>
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
<ul>
{submittedItems.map((item, index) => (
<li key={index}>
{item}
</li>
))}
</ul>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter login bonus ID"
/>
<button type="submit">Submit</button>
</form>
<p>You can find a list of available login bonus IDs <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/src/router/databases/json/login_bonus.json">here</a>. You should input the <code>id</code> field</p>
</div>
);
}
function Home() {
const [user, userdata] = useState();
const [inputValue, setInputValue] = useState('');
const [serverTime, setServerTime] = useState('');
const error = useState("");
const logout = () => {
window.location.href = "/webui/logout";
}
const downloadFile = (contents, name) => {
let a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([contents], {type: "application/json"}));
a.download = name;
a.click();
}
const expor = async (e) => {
e.preventDefault();
let resp = await Request("/api/webui/export");
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
downloadFile(resp.data.userdata, "userdata.json");
downloadFile(resp.data.userhome, "userhome.json");
downloadFile(resp.data.missions, "missions.json");
downloadFile(resp.data.sifcards, "sifcards.json");
}
const handleSubmit = async (event) => {
event.preventDefault();
let time = Math.round(new Date(inputValue.trim()).getTime() / 1000);
if (inputValue.trim() === "-1") {
time = 1711741114;
} else if (inputValue.trim() === "0") {
time = 0;
}
if (time < 0 || isNaN(time)) return;
let resp = await Request("/api/webui/set_time", {
timestamp: time
});
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
error[1]("");
if (time === 0) {
setServerTime("now");
} else {
setServerTime((new Date(time * 1000)).toString());
}
setInputValue('');
};
useEffect(() => {
if (user) return;
(async () => {
let resp = await Request("/api/webui/userInfo");
if (resp.result !== "OK") {
window.location.href = "/?message=" + encodeURIComponent(resp.message);
return;
}
let user = resp.data.userdata;
bonusItems = resp.data.loginbonus.bonus_list;
/*
bonusItems = [{"master_login_bonus_id":1,"day_counts":[1,2],"event_bonus_list":[]}];
let user = {
user: {
id: 1,
rank: 3,
exp: 10,
last_login_time: 5
},
time: new Date()
}*/
if (resp.data.time === 0) {
setServerTime("now");
} else {
setServerTime((new Date(resp.data.time * 1000)).toString());
}
userdata(
<div>
<p>User id: { user.user.id } </p>
<p>Migration id: { getMigrationID(user.user.id) } </p>
<p>Rank: { user.user.rank } ({ user.user.exp } exp)</p>
<p>Last Login: { (new Date(user.user.last_login_time * 1000)).toString() } </p>
<Bonus />
</div>
);
})();
});
return (
<div id="home">
<button id="logout" onClick={expor}>Export account</button><br/><br/><br/>
<button id="logout" onClick={logout}>Logout</button>
<h1>Home</h1>
{ user ? <div>
{ user }
<h2>Server time</h2>
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
<p>Currently set to { serverTime }. Setting to 0 will set it to now, and -1 will reset it. Time will still progress, based off of when you set this timestamp.</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter Server Time"
/>
<button type="submit">Submit</button>
</form></div>
: <p>Loading...</p> }
</div>
);
}
export default Home;

View File

@@ -1,95 +0,0 @@
body {
background-color: #616161;
}
#login-form {
width: 400px;
max-width: 100%;
margin: 50px auto;
background-color: green;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
}
#login-form h1 {
text-align: center;
margin: 0;
padding: 20px 0;
font-size: 28px;
font-weight: bold;
color: white;
}
#login-form form {
padding: 20px;
background-color: white;
border-radius: 10px;
font-family: "Poppins", sans-serif;
}
#login-form form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: black;
font-family: "Poppins", sans-serif;
}
#login-form form input[type="file"],
#login-form form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid lightgray;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 20px;
}
#login-form form input[type="submit"] {
width: 100%;
padding: 12px;
background-color: dodgerblue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#login-form form input[type="submit"]:hover {
background-color: deepskyblue;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
#sub_div p {
color: red;
grid-template-columns: auto auto auto;
}
#sub_div {
padding-top: 10px;
}
#sub_div button {
width: 40%;
padding: 12px;
background-color: blue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#sub_div button:hover {
background-color: green;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

View File

@@ -1,84 +0,0 @@
import { useState } from 'react'
import './Import.css'
import Request from '../Request.jsx'
function Login() {
const error = useState(new URL(window.location).searchParams.get("message") || "");
const status = useState("");
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
let file=[], file1=[], file2=[], file3=[], password;
let has_imported = false;
const handleSubmit = async (event) => {
event.preventDefault();
if (!file[0] || !file1[0] || has_imported || !password) return;
try {
has_imported = true;
let data = {
userdata: JSON.parse(await file[0].text()),
home: JSON.parse(await file1[0].text()),
missions: file2[0] ? JSON.parse(await file2[0].text()) : undefined,
sif_cards: file3[0] ? JSON.parse(await file3[0].text()) : undefined,
password: password,
jp: true
};
if (!data.userdata || !data.userdata.user || !data.userdata.user.id) {
error[1]("Incorrect user data file format");
return;
}
if (!data.home || !data.home.home || !data.home.home.information_list) {
error[1]("Incorrect home data file format");
return;
}
if (!Array.isArray(data.missions) && data.missions) {
error[1]("Incorrect mission data file format");
return;
}
if (!Array.isArray(data.sif_cards) && data.sif_cards) {
error[1]("Incorrect sif card data file format");
return;
}
let resp = await Request(
"/api/webui/import",
data
);
if (resp.result == "OK") {
status[1](<div><p>Account imported!</p><p>User id: {resp.uid}</p><p>Migration token: {resp.migration_token}</p></div>);
} else {
error[1](resp.message);
}
} catch(e) {
error[1](e.message);
}
};
return (
<div id="login-form">
<h1>Transfer</h1>
<form>
{ <p>{ status[0] } </p> }
<label htmlFor="id">User data file (required):</label>
<input type="file" id="id" name="id" onChange={(event) => {file = event.target.files}} accept="application/json"/>
<label htmlFor="file1">User Home data file (required):</label>
<input type="file" id="file1" name="file1" onChange={(event) => {file1 = event.target.files}} accept="application/json"/>
<label htmlFor="file2">User Missions data file (optional):</label>
<input type="file" id="file2" name="file2" onChange={(event) => {file2 = event.target.files}} accept="application/json"/>
<label htmlFor="file3">Sif cards data file (optional):</label>
<input type="file" id="file3" name="file3" onChange={(event) => {file3 = event.target.files}} accept="application/json"/>
<label htmlFor="password">Transfer passcode (game will not recognize special characters, only use letters and numbers or you will be locked out):</label>
<input type="password" id="password" name="password" onChange={(event) => {password = event.target.value}} />
<input type="submit" value="Submit" onClick={handleSubmit}/>
<div id="sub_div">
{ error[0] ? <p>Error: { error[0] }. Please reload the page and try again.</p> : <p></p> }
</div>
</form>
</div>
);
}
export default Login;

View File

@@ -1,95 +0,0 @@
body {
background-color: #616161;
}
#login-form {
width: 400px;
max-width: 100%;
margin: 50px auto;
background-color: green;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
}
#login-form h1 {
text-align: center;
margin: 0;
padding: 20px 0;
font-size: 28px;
font-weight: bold;
color: white;
}
#login-form form {
padding: 20px;
background-color: white;
border-radius: 10px;
font-family: "Poppins", sans-serif;
}
#login-form form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: black;
font-family: "Poppins", sans-serif;
}
#login-form form input[type="text"],
#login-form form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid lightgray;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 20px;
}
#login-form form input[type="submit"] {
width: 100%;
padding: 12px;
background-color: dodgerblue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#login-form form input[type="submit"]:hover {
background-color: deepskyblue;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
#sub_div p {
color: red;
grid-template-columns: auto auto auto;
}
#sub_div {
padding-top: 10px;
}
#sub_div button {
width: 40%;
padding: 12px;
background-color: blue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#sub_div button:hover {
background-color: green;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

View File

@@ -1,69 +0,0 @@
import { useState } from 'react'
import './Login.css'
import Request from '../Request.jsx'
function Login() {
const error = useState(new URL(window.location).searchParams.get("message") || "");
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
const password = useState("");
function showError(message) {
error[1](message);
}
const handleSubmit = async (event) => {
event.preventDefault();
if (!uid[0] || !password[0]) {
showError("Missing userid/password");
return;
}
if (isNaN(uid[0])) {
showError("UserID should be a number. (The \"Friend ID\" in your profile)");
return;
}
if (window.localStorage) window.localStorage.setItem("ew_uid", uid[0]);
let resp = await Request(
"/api/webui/login",
{
uid: parseInt(uid[0]),
password: password[0]
}
);
if (resp.result == "OK") {
window.location.href = "/home/";
} else {
showError(resp.message);
}
};
const import_user = (e) => {
e.preventDefault();
window.location.href = "/import/";
}
const help = (e) => {
e.preventDefault();
window.location.href = "/help/";
}
return (
<div id="login-form">
<h1>Login</h1>
<form>
<label htmlFor="id">SIF2 ID:</label>
<input type="text" id="id" name="id" onChange={(event) => {uid[1](event.target.value)}} value={uid[0]} />
<label htmlFor="password">Transfer passcode:</label>
<input type="password" id="password" name="password" onChange={(event) => {password[1](event.target.value)}} />
<input type="submit" value="Submit" onClick={handleSubmit}/>
<div id="sub_div">
<button onClick={import_user}>Import User</button><br/><br/>
<button onClick={help}>Need help?</button><br/><br/>
{ error[0] ? <p>Error: { error[0] } </p> : <p></p> }
</div>
<p>EW Version 1.0.0 - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">View source</a> - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/LICENSE">View license</a></p>
</form>
</div>
);
}
export default Login;

View File

@@ -1,32 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import Login from './login/Login.jsx'
import Home from './home/Home.jsx'
import Import from './import/Import.jsx'
import Help from './help/Help.jsx'
let Elem;
switch (window.location.pathname) {
case "/":
Elem = Login;
break;
case "/home/":
Elem = Home;
break;
case "/import/":
Elem = Import;
break;
case "/help/":
Elem = Help;
break;
default:
window.location.pathname = "/";
}
if (Elem) {
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Elem />
</React.StrictMode>,
)
}

View File

@@ -1,15 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
entryFileNames: 'index.js',
assetFileNames: 'index.css',
},
},
},
})