Compare commits

...

35 Commits

Author SHA1 Message Date
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
42 changed files with 27774 additions and 2400 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
@@ -39,9 +42,9 @@ jobs:
uses: docker/metadata-action@master
with:
images: |
docker.io/ethanaobrien/ew
docker.io/ethanaobrien/ew
flavor: |
latest=false
latest=false
tags: |
type=raw,value=latest
@@ -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,6 +11,8 @@ jobs:
steps:
- name: Checking out branch
uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust
shell: bash
@@ -20,9 +23,15 @@ jobs:
with:
node-version: '18'
- name: Install android NDK
id: setup-ndk
uses: https://github.com/nttld/setup-ndk@v1
with:
ndk-version: r29
- name: Create out directory
run: |
mkdir out
mkdir out
- name: Build webui
run: |
@@ -30,21 +39,75 @@ jobs:
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

3
.gitmodules vendored Normal file
View File

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

1912
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,48 @@
[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.12"
rusqlite = { version = "0.37.0", features = ["bundled"] }
clap = { version = "4.5.37", features = ["derive"]}
base64 = "0.22.1"
json = "0.12.4"
rand = "0.8.5"
rand = "0.9.2"
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.18.1", features = ["v7"] }
rsa = { version = "0.9.9", 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.1.4"
include_dir = {version = "0.7.4", optional = true }
[target.aarch64-linux-android.dependencies]
jni = { version = "0.21.1", 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.0"
# To enable this library you MUST comment out lib block below and add --features library
[features]
library = ["jni", "objc2", "objc2-foundation", "include_dir"]
#[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

12
build.rs Normal file
View File

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

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 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 encrypted = Aes256CbcEnc::new(KEY.as_bytes().into(), encryption_iv.as_slice().into())
.encrypt_padded_vec_mut::<Pkcs7>(input.as_bytes());
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 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();
}
}

85
src/lib.rs Normal file
View File

@@ -0,0 +1,85 @@
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::css)
.service(static_handlers::maintenance)
.service(static_handlers::js)
.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.");
}

65
src/options.rs Normal file
View File

@@ -0,0 +1,65 @@
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
}
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") {
@@ -212,6 +212,7 @@ 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),

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

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

@@ -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_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 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()
if args.en_android_asset_hash != String::new() {
&args.en_android_asset_hash
} else if get_easter_mode() {
ASSET_HASH_ANDROID_EASTER_GL
} else {
ASSET_HASH_ANDROID_GL
}
}
} else if android {
if args.en_android_asset_hash != String::new() {
args.en_android_asset_hash
} else {
ASSET_HASH_ANDROID.to_string()
}
} 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(),
@@ -317,12 +308,12 @@ pub fn migration_code(req: HttpRequest) -> HttpResponse {
uid = uid_str.to_string();
}
}
let user = userdata::get_acc(&uid);
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)
@@ -340,12 +331,12 @@ pub fn migration_password_register(req: HttpRequest, body: String) -> HttpRespon
uid = uid_str.to_string();
}
}
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

@@ -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("");
@@ -110,6 +122,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 {
@@ -230,7 +244,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

@@ -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

@@ -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 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 (
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 (
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 (
user_id BIGINT NOT NULL PRIMARY KEY,
userhome TEXT NOT NULL
)");
conn.create_store_v2("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 (
user_id BIGINT NOT NULL PRIMARY KEY,
loginbonus TEXT NOT NULL
)");
conn.create_store_v2("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 (
user_id BIGINT NOT NULL PRIMARY KEY,
friends TEXT NOT NULL
)");
conn.create_store_v2("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 (
user_id BIGINT NOT NULL PRIMARY KEY,
event TEXT NOT NULL
)");
conn.create_store_v2("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 (
user_id BIGINT NOT NULL PRIMARY KEY,
server_data TEXT NOT NULL
)");
conn.create_store_v2("CREATE TABLE IF NOT EXISTS webui (
user_id BIGINT NOT NULL PRIMARY KEY,
token TEXT NOT NULL,
last_login BIGINT NOT NULL
)");
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
);
CREATE TABLE IF NOT EXISTS userdata (
user_id BIGINT NOT NULL PRIMARY KEY,
userdata TEXT NOT NULL,
friend_request_disabled INT NOT NULL
);
CREATE TABLE IF NOT EXISTS userhome (
user_id BIGINT NOT NULL PRIMARY KEY,
userhome TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS missions (
user_id BIGINT NOT NULL PRIMARY KEY,
missions TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS loginbonus (
user_id BIGINT NOT NULL PRIMARY KEY,
loginbonus TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sifcards (
user_id BIGINT NOT NULL PRIMARY KEY,
sifcards TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS friends (
user_id BIGINT NOT NULL PRIMARY KEY,
friends TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS chats (
user_id BIGINT NOT NULL PRIMARY KEY,
chats TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS event (
user_id BIGINT NOT NULL PRIMARY KEY,
event TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS eventloginbonus (
user_id BIGINT NOT NULL PRIMARY KEY,
eventloginbonus TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS server_data (
user_id BIGINT NOT NULL PRIMARY KEY,
server_data TEXT NOT NULL
);
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."));
}
@@ -504,13 +445,12 @@ 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
})
}
@@ -639,7 +579,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::Rng;
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")
}

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) {
@@ -62,29 +54,9 @@ impl SQLite {
let res = val?;
match res.clone().parse::<i64>() {
Ok(v) => rv.push(v).unwrap(),
Err(_) => rv.push(res).unwrap()
Err(_) => rv.push(res).unwrap()
};
}
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!());
}
}

73
src/static_handlers.rs Normal file
View File

@@ -0,0 +1,73 @@
use actix_web::{
get,
HttpResponse,
HttpRequest,
http::header::ContentType
};
use std::fs;
use crate::include_file;
#[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"))
}
#[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)
}